fix: le descripteur d-un membre repond ce que le lire repond
Trouve en rejouant l-adversaire sur le correctif precedent, et mesure : `Object.getOwnPropertyDescriptor(resultat, "data")` rendait la collection BRUTE, et `(..., "subscription")` le handle que `get` refuse. Cet appel ne passe pas par le piege `get` - ni l-etalement, ni `Object.entries`, ni `JSON.stringify`, ni une inspection console. Une deuxieme porte sur la meme piece, ouverte par un piege que personne n-avait ecrit. Le meme trou existait sur la vue de collection pour la trappe dunder. La forme ordonnee dit combien d-objets elle porte a QUATRE endroits : `length`, `Object.keys`, `in`, et le descripteur de chaque index. Un seul etait filtre, donc le compte de ce que le porteur ne peut pas lire ressortait par les trois autres, et le descripteur rendait les objets eux-memes. Les quatre pieges appellent maintenant une seule fonction `read`, pas trois copies de la regle : une frontiere faite de `switch` paralleles est une frontiere qui derive. Et l-index nomme une position dans ce qui est LISIBLE, donc la forme ordonnee sert son porteur au lieu de refuser.
This commit is contained in:
@@ -142,3 +142,47 @@ test("forEach is filtered too", () => {
|
||||
makeReadFilteredView(set, caps).forEach((i) => seen.push((i as Item).id));
|
||||
expect(seen).toEqual([]);
|
||||
});
|
||||
|
||||
// REGRESSION (third adversarial pass, 2026-09-24). `Object.getOwnPropertyDescriptor`
|
||||
// reads a property WITHOUT going through the `get` trap, so a view that filtered `get`
|
||||
// alone handed the raw value back to whoever asked for the descriptor - the dunder escape
|
||||
// hatch included, which `get` refuses by name. Everything built on that call goes the same
|
||||
// way: `Object.keys`, `Object.entries`, a spread, `JSON.stringify`, a console inspection.
|
||||
test("a descriptor cannot answer what reading the member refuses", () => {
|
||||
const set = new Set<Item>([MINE]) as any;
|
||||
set.__raw__ = set;
|
||||
const { caps, become } = setup("alice");
|
||||
const view = makeReadFilteredView(set, caps) as any;
|
||||
become("bob");
|
||||
expect(() => Object.getOwnPropertyDescriptor(view, "__raw__")).toThrow(/raw collection/i);
|
||||
});
|
||||
|
||||
// An `orderBy` config makes upstream's collection an ARRAY, and an array says how many
|
||||
// items it holds in four different places. `length` was filtered while `Object.keys`, `in`
|
||||
// and the descriptor of each index still answered from the collection underneath - so the
|
||||
// count of what a holder may NOT read came back out through the other three, and the
|
||||
// descriptor handed back the items themselves.
|
||||
test("the array form counts and addresses the readable items, by every route", () => {
|
||||
const items = [MINE, FOREIGN, LINKED];
|
||||
const { caps, become } = setup("alice");
|
||||
const view = makeReadFilteredView(items, caps) as any;
|
||||
become("bob"); // holds nothing of the three documents
|
||||
|
||||
expect(view.length).toBe(0);
|
||||
expect(Object.keys(view)).toEqual([]);
|
||||
expect(0 in view).toBe(false);
|
||||
expect(view[0]).toBeUndefined();
|
||||
expect(Object.getOwnPropertyDescriptor(view, "0")).toBeUndefined();
|
||||
expect(JSON.parse(JSON.stringify(view))).toEqual([]);
|
||||
|
||||
become("alice"); // holds her own document and the one she published
|
||||
expect(view.length).toBe(2);
|
||||
expect(Object.keys(view)).toEqual(["0", "1"]);
|
||||
expect(view[0].id).toBe("a");
|
||||
expect(view[1].id).toBe("p");
|
||||
expect(Object.getOwnPropertyDescriptor(view, "1")?.value.id).toBe("p");
|
||||
expect(2 in view).toBe(false);
|
||||
// `length` is the array's own key and is never hidden - a proxy may not drop a key the
|
||||
// target cannot configure away - but it is not a position and does not enumerate.
|
||||
expect(Object.getOwnPropertyNames(view)).toContain("length");
|
||||
});
|
||||
|
||||
@@ -262,7 +262,36 @@ test("useShape: an ordered result does not publish the unfiltered count", async
|
||||
expect((hook.raw() as BaseType[]).length).toBe(3);
|
||||
expect((data as { length: number }).length).toBe(1);
|
||||
expect([...(data as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([hers]);
|
||||
// An index hands back a raw item and there is no projection of it this file would not
|
||||
// have to invent, so it is refused rather than guessed at.
|
||||
expect(() => (data as Record<number, unknown>)[0]).toThrow(/not filtered/i);
|
||||
// An index names a position in the READABLE items, so the holder reads their own by
|
||||
// index and the positions past them do not exist at all. `Object.keys` and `in` have to
|
||||
// agree with `length`, or the count comes back out through the other three.
|
||||
expect((data as Record<number, BaseType>)[0]!["@graph"]).toBe(hers);
|
||||
expect((data as Record<number, BaseType>)[1]).toBeUndefined();
|
||||
expect(Object.keys(data as object)).toEqual(["0"]);
|
||||
expect(1 in (data as object)).toBe(false);
|
||||
expect(JSON.parse(JSON.stringify(data)) as BaseType[]).toHaveLength(1);
|
||||
});
|
||||
|
||||
// REGRESSION, found by re-running the adversary on the first fix (2026-09-24).
|
||||
// `Object.getOwnPropertyDescriptor` reads a property WITHOUT the `get` trap, so a view
|
||||
// that filtered `get` alone handed the raw value straight back to whoever asked for the
|
||||
// descriptor - including `subscription`, the one member `get` refuses outright. Everything
|
||||
// built on it goes the same way: a spread, `Object.entries`, a console inspection.
|
||||
test("useShape: the descriptor of a member answers exactly what reading it answers", async () => {
|
||||
const hook = makeUseShapeHook();
|
||||
inject(hook);
|
||||
const hers = await documentOf("alice");
|
||||
const his = await documentOf("bob");
|
||||
hook.items.push(noteIn(hers, "a1"), noteIn(his, "b1"));
|
||||
|
||||
setCurrentUser("bob");
|
||||
const result: ShapeResult = useShape(NoteShape, EVERYTHING);
|
||||
|
||||
const described = Object.getOwnPropertyDescriptor(result, "data");
|
||||
expect(described?.value).toBe(result.data);
|
||||
expect(described?.value).not.toBe(hook.raw());
|
||||
expect([...(described?.value as Iterable<BaseType>)].map((o) => o["@graph"])).toEqual([his]);
|
||||
expect(() => Object.getOwnPropertyDescriptor(result, "subscription")).toThrow(
|
||||
/RdfOrmSubscription/,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user