Skip to content

Commit 0d8af06

Browse files
committed
test(runtime): composed pins for the stored-metadata body boundary
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz Co-authored-by: Claude <noreply@anthropic.com>
1 parent 99c9a41 commit 0d8af06

1 file changed

Lines changed: 347 additions & 0 deletions

File tree

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#21520] The stored-metadata family's boundary for app-authored bodies, end
5+
* to end in a composed kernel: a body may not touch the family's tables
6+
* (`sys_metadata` / `sys_metadata_history`) except by reading them through the
7+
* read seam; changes to metadata go through the metadata API.
8+
*
9+
* Every observation here is a NEUTRAL marker: a hook body appends a fixed
10+
* token to a free-text column (`tags` on `sys_metadata`, `change_note` on its
11+
* history, `status` on the ordinary table), and an action body writes the same
12+
* kind of token. Whether a body ran, or a write landed, is read off that
13+
* column — nothing else of a stored row is read.
14+
*
15+
* ① binding — a body hook targeting a family table (string or list form, in
16+
* the app bundle, or authored at runtime through the metadata door) is not
17+
* bound, so the metadata door's own save does not run it; a wildcard body
18+
* hook binds and is not run for a family table. Controls: the same hooks on
19+
* an ordinary table bind and fire; a platform CODE hook on `sys_metadata`
20+
* still fires on the metadata door's save.
21+
* ② writing — an action body's write of a family table (an insert, and a
22+
* predicate update) answers `403 PERMISSION_DENIED` and lands nothing, for
23+
* the administrator and for a member (the body runs elevated for both).
24+
* Control: the same body's write of an ordinary table lands.
25+
*
26+
* Composition: the in-process kernel `@objectstack/verify`'s `bootStack` mirrors
27+
* (engine, sqlite-wasm default datasource, HTTP server, the app, platform
28+
* objects, auth, security, sharing, REST, dispatcher), requests injected through
29+
* the HTTP app as signed-in users. The boot is paid in `beforeAll`, never inside
30+
* a case.
31+
*/
32+
33+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
34+
import { ObjectKernel } from '@objectstack/core';
35+
import type { Plugin, PluginContext } from '@objectstack/core';
36+
import { ObjectQLPlugin } from '@objectstack/objectql';
37+
import type { ObjectQL } from '@objectstack/objectql';
38+
import { HonoServerPlugin } from '@objectstack/plugin-hono-server';
39+
import { createRestApiPlugin } from '@objectstack/rest';
40+
import { AuthPlugin } from '@objectstack/plugin-auth';
41+
import { SecurityPlugin, appSecurityPluginOptions } from '@objectstack/plugin-security';
42+
import { SharingServicePlugin } from '@objectstack/plugin-sharing';
43+
import { PlatformObjectsPlugin } from '@objectstack/platform-objects/plugin';
44+
import { AppPlugin } from './app-plugin.js';
45+
import { DefaultDatasourcePlugin } from './default-datasource-plugin.js';
46+
import { createDispatcherPlugin } from './dispatcher-plugin.js';
47+
48+
const BOOT_TIMEOUT = 180_000;
49+
const ORIGIN = 'http://localhost:3000';
50+
const API = '/api/v1';
51+
const ADMIN = { email: 'admin@objectos.ai', password: 'admin123' };
52+
const MEMBER = { email: 'boundary-member@example.invalid', password: 'Member-Pass-123' };
53+
const ORDINARY = 'boundary_note';
54+
55+
const js = (source: string, capabilities: string[] = []) => ({ language: 'js', source, capabilities, timeoutMs: 5000 });
56+
/** Append `token` to a free-text column of the row being written. */
57+
const append = (column: string, token: string) =>
58+
`ctx.input.${column} = (typeof ctx.input.${column} === 'string' ? ctx.input.${column} : '') + '|${token}';`;
59+
60+
const PIN_APP: any = {
61+
manifest: { id: 'com.pin.boundary21520', name: 'Body boundary pins', version: '1.0.0' },
62+
objects: [
63+
{
64+
name: ORDINARY,
65+
label: 'Boundary note',
66+
fields: {
67+
title: { type: 'text', label: 'Title' },
68+
status: { type: 'text', label: 'Status' },
69+
},
70+
actions: [
71+
{
72+
name: 'body_inserts_metadata',
73+
label: 'Body inserts a stored-metadata row',
74+
type: 'script',
75+
body: js(
76+
"await ctx.api.object('sys_metadata').insert({ name: 'boundary_body_row', type: 'note', metadata: '{}' });\nreturn { wrote: true };",
77+
['api.write'],
78+
),
79+
},
80+
{
81+
name: 'body_updates_metadata',
82+
label: 'Body updates stored-metadata rows by predicate',
83+
type: 'script',
84+
body: js(
85+
"await ctx.api.object('sys_metadata').update({ tags: '|body-wrote' }, { where: { type: 'action' }, multi: true });\nreturn { wrote: true };",
86+
['api.write'],
87+
),
88+
},
89+
{
90+
name: 'body_inserts_history',
91+
label: 'Body inserts a history row',
92+
type: 'script',
93+
body: js(
94+
"await ctx.api.object('sys_metadata_history').insert({ name: 'boundary_body_row', type: 'note', version: 1, operation_type: 'create', metadata: '{}' });\nreturn { wrote: true };",
95+
['api.write'],
96+
),
97+
},
98+
{
99+
name: 'body_inserts_ordinary',
100+
label: 'Body inserts an ordinary row (control)',
101+
type: 'script',
102+
body: js(`await ctx.api.object('${ORDINARY}').insert({ title: 'body-wrote' });\nreturn { wrote: true };`, ['api.write']),
103+
},
104+
],
105+
},
106+
],
107+
hooks: [
108+
{ name: 'boundary_hook_on_metadata', object: 'sys_metadata', events: ['beforeInsert', 'beforeUpdate'], body: js(append('tags', 'explicit-ran')) },
109+
{ name: 'boundary_hook_on_history', object: ['sys_metadata_history'], events: ['beforeInsert'], body: js(append('change_note', 'explicit-ran')) },
110+
{
111+
name: 'boundary_hook_wildcard',
112+
object: '*',
113+
events: ['beforeInsert', 'beforeUpdate'],
114+
body: js(
115+
`if (ctx.object === 'sys_metadata') { ${append('tags', 'wildcard-ran')} }\n`
116+
+ `if (ctx.object === '${ORDINARY}') { ${append('status', 'wildcard-ran')} }`,
117+
),
118+
},
119+
{ name: 'boundary_hook_ordinary', object: ORDINARY, events: ['beforeInsert'], body: js(append('status', 'ordinary-ran')) },
120+
],
121+
permissions: [
122+
{
123+
name: 'boundary_member_default',
124+
label: 'Boundary member default',
125+
isDefault: true,
126+
objects: { [ORDINARY]: { allowRead: true, allowCreate: true } },
127+
},
128+
],
129+
};
130+
131+
/** A platform-shaped CODE hook on `sys_metadata` (registered as code, never a body): counts its runs. */
132+
let platformHookRuns = 0;
133+
const PLATFORM_HOOK_PLUGIN: Plugin = {
134+
name: 'pin.boundary21520.platform-hook',
135+
version: '0.0.0',
136+
init: async () => {},
137+
start: async (ctx: PluginContext) => {
138+
const ql = ctx.getService<ObjectQL>('objectql');
139+
for (const event of ['afterInsert', 'afterUpdate']) {
140+
ql.registerHook(event, async () => { platformHookRuns += 1; }, { object: 'sys_metadata', packageId: 'pin.platform' });
141+
}
142+
},
143+
};
144+
145+
let kernel: any;
146+
let httpServer: any;
147+
let app: any;
148+
let adminToken: string;
149+
let memberToken: string;
150+
let prevNodeEnv: string | undefined;
151+
/** The metadata door's answer to saving a runtime-authored hook on `sys_metadata` (printed, not asserted). */
152+
let recordedFamilyHookSaveStatus: number | undefined;
153+
154+
const req = (path: string, init?: RequestInit) => app.request(`${ORIGIN}${API}${path}`, init);
155+
const as = (token: string | undefined, method: string, path: string, body?: unknown) =>
156+
req(path, {
157+
method,
158+
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
159+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
160+
});
161+
162+
async function readJson(res: Response): Promise<any> {
163+
const text = await res.text();
164+
try { return JSON.parse(text); } catch { return text; }
165+
}
166+
167+
async function engine(): Promise<any> {
168+
return kernel.getServiceAsync('objectql');
169+
}
170+
171+
/** One free-text column of the rows matching `where`, read in-process. */
172+
async function columnOf(object: string, where: Record<string, unknown>, column: string): Promise<string[]> {
173+
const rows: any[] = await (await engine()).find(object, { where, fields: ['id', column], context: { isSystem: true } });
174+
return rows.map((r) => String(r?.[column] ?? ''));
175+
}
176+
177+
async function signIn(who: { email: string; password: string }): Promise<string> {
178+
const res = await req('/auth/sign-in/email', {
179+
method: 'POST',
180+
headers: { 'Content-Type': 'application/json' },
181+
body: JSON.stringify(who),
182+
});
183+
if (!res.ok) throw new Error(`pin signIn failed: ${res.status}`);
184+
return (await res.json()).token;
185+
}
186+
187+
async function signUpMember(): Promise<string> {
188+
// Default audience posture is invite_only: enter through a pending invitation.
189+
await (await engine()).insert(
190+
'sys_invitation',
191+
{
192+
id: 'inv_pin_21520',
193+
email: MEMBER.email,
194+
status: 'pending',
195+
organization_id: 'org_pin_audience_gate',
196+
role: 'member',
197+
inviter_id: 'usr_pin_audience_gate',
198+
expires_at: new Date(Date.now() + 3_600_000),
199+
},
200+
{ context: { isSystem: true } },
201+
);
202+
const res = await req('/auth/sign-up/email', {
203+
method: 'POST',
204+
headers: { 'Content-Type': 'application/json' },
205+
body: JSON.stringify({ email: MEMBER.email, password: MEMBER.password, name: 'boundary member' }),
206+
});
207+
if (!res.ok) throw new Error(`pin signUp failed: ${res.status}`);
208+
return (await res.json()).token;
209+
}
210+
211+
async function waitFor(predicate: () => Promise<boolean>, ms = 15_000): Promise<boolean> {
212+
const until = Date.now() + ms;
213+
while (Date.now() < until) {
214+
if (await predicate()) return true;
215+
await new Promise((r) => setTimeout(r, 100));
216+
}
217+
return false;
218+
}
219+
220+
/** Save an `action` item through the metadata door, as the administrator: the platform's own family write. */
221+
async function saveThroughMetadataDoor(name: string, label: string): Promise<Response> {
222+
return as(adminToken, 'PUT', `/meta/action/${name}`, {
223+
name,
224+
label,
225+
objectName: ORDINARY,
226+
type: 'script',
227+
body: js('return { ok: true };'),
228+
});
229+
}
230+
231+
beforeAll(async () => {
232+
prevNodeEnv = process.env.NODE_ENV;
233+
process.env.NODE_ENV = 'development'; // the dev-admin seed, as `objectstack dev` / bootStack arm it
234+
235+
kernel = new ObjectKernel();
236+
await kernel.use(new ObjectQLPlugin());
237+
await kernel.use(new DefaultDatasourcePlugin({ driver: 'sqlite-wasm', config: { filename: ':memory:' } }));
238+
await kernel.use(new HonoServerPlugin({ port: 0 }));
239+
await kernel.use(new AppPlugin(PIN_APP));
240+
await kernel.use(new PlatformObjectsPlugin());
241+
await kernel.use(new AuthPlugin({ secret: 'body-boundary-21520-secret', autoDefaultOrganization: false }));
242+
await kernel.use(PLATFORM_HOOK_PLUGIN);
243+
await kernel.use(new SecurityPlugin(appSecurityPluginOptions(PIN_APP)));
244+
await kernel.use(new SharingServicePlugin());
245+
await kernel.use(createRestApiPlugin({}));
246+
await kernel.use(createDispatcherPlugin({}));
247+
await kernel.bootstrap();
248+
249+
httpServer = await kernel.getServiceAsync('http-server');
250+
app = httpServer.getRawApp();
251+
adminToken = await signIn(ADMIN);
252+
memberToken = await signUpMember();
253+
}, BOOT_TIMEOUT);
254+
255+
afterAll(async () => {
256+
console.info(`[#21520 pin] metadata door save of a runtime-authored family-table hook answered: ${recordedFamilyHookSaveStatus}`);
257+
try { await httpServer?.close?.(); } catch { /* best-effort */ }
258+
try { await kernel?.shutdown?.(); } catch { /* best-effort */ }
259+
if (prevNodeEnv === undefined) delete process.env.NODE_ENV;
260+
else process.env.NODE_ENV = prevNodeEnv;
261+
}, 60_000);
262+
263+
describe('[#21520] ① binding — a body hook targeting a family table is not bound', () => {
264+
it('the metadata door\'s save runs no body bound to a family table, and still fires the platform code hook', async () => {
265+
const before = platformHookRuns;
266+
const res = await saveThroughMetadataDoor('boundary_saved_one', 'Saved once');
267+
expect(res.status, JSON.stringify(await readJson(res))).toBeLessThan(300);
268+
269+
const tags = await columnOf('sys_metadata', { type: 'action', name: 'boundary_saved_one' }, 'tags');
270+
expect(tags.length, 'the metadata door stored no row').toBeGreaterThan(0);
271+
for (const value of tags) {
272+
expect(value, 'an explicitly-bound body ran on the save').not.toContain('explicit-ran');
273+
expect(value, 'a wildcard body ran on the save').not.toContain('wildcard-ran');
274+
}
275+
const notes = await columnOf('sys_metadata_history', { type: 'action', name: 'boundary_saved_one' }, 'change_note');
276+
for (const value of notes) expect(value, 'a list-form body ran on the history row').not.toContain('explicit-ran');
277+
278+
expect(platformHookRuns, 'the platform code hook did not fire on the save').toBeGreaterThan(before);
279+
});
280+
281+
it('control: the same app\'s hooks on an ordinary table bind and fire, the wildcard among them', async () => {
282+
const res = await as(adminToken, 'POST', `/data/${ORDINARY}`, { title: 'boundary-control' });
283+
expect(res.status).toBeLessThan(300);
284+
const [status] = await columnOf(ORDINARY, { title: 'boundary-control' }, 'status');
285+
expect(status).toContain('ordinary-ran');
286+
expect(status).toContain('wildcard-ran');
287+
});
288+
289+
it('a hook authored at runtime through the metadata door is not bound to a family table; an ordinary one is', async () => {
290+
const familyHook = await as(adminToken, 'PUT', '/meta/hook/boundary_authored_on_metadata', {
291+
name: 'boundary_authored_on_metadata',
292+
object: 'sys_metadata',
293+
events: ['beforeInsert', 'beforeUpdate'],
294+
body: js(append('tags', 'authored-ran')),
295+
});
296+
const ordinaryHook = await as(adminToken, 'PUT', '/meta/hook/boundary_authored_ordinary', {
297+
name: 'boundary_authored_ordinary',
298+
object: ORDINARY,
299+
events: ['beforeInsert'],
300+
body: js(append('status', 'authored-ran')),
301+
});
302+
expect(ordinaryHook.status, JSON.stringify(await readJson(ordinaryHook))).toBeLessThan(300);
303+
// Recorded, not asserted: whether the metadata door accepts the family hook
304+
// at save is the save door's question; this boundary refuses it at bind.
305+
recordedFamilyHookSaveStatus = familyHook.status;
306+
307+
// The resync has bound the ordinary authored hook once it fires…
308+
const bound = await waitFor(async () => {
309+
await as(adminToken, 'POST', `/data/${ORDINARY}`, { title: 'boundary-authored-probe' });
310+
const statuses = await columnOf(ORDINARY, { title: 'boundary-authored-probe' }, 'status');
311+
return statuses.some((s) => s.includes('authored-ran'));
312+
});
313+
expect(bound, 'the runtime-authored ordinary hook never bound').toBe(true);
314+
315+
// …and by then the family one, had it bound, would run on this save.
316+
const res = await saveThroughMetadataDoor('boundary_saved_two', 'Saved after the authored hooks');
317+
expect(res.status).toBeLessThan(300);
318+
for (const value of await columnOf('sys_metadata', { type: 'action', name: 'boundary_saved_two' }, 'tags')) {
319+
expect(value, 'a runtime-authored body ran on the save').not.toContain('authored-ran');
320+
}
321+
}, 30_000);
322+
});
323+
324+
describe('[#21520] ② writing — an action body may not write a family table', () => {
325+
for (const [role, token] of [['administrator', () => adminToken], ['member', () => memberToken]] as const) {
326+
it(`invoked by the ${role}: each family write answers 403 PERMISSION_DENIED and lands nothing`, async () => {
327+
for (const action of ['body_inserts_metadata', 'body_updates_metadata', 'body_inserts_history']) {
328+
const res = await as(token(), 'POST', `/actions/${ORDINARY}/${action}`, { params: {} });
329+
const payload = await readJson(res);
330+
expect(res.status, `${action}: ${JSON.stringify(payload)}`).toBe(403);
331+
expect(payload?.error?.code ?? payload?.code, action).toBe('PERMISSION_DENIED');
332+
}
333+
expect(await columnOf('sys_metadata', { name: 'boundary_body_row' }, 'name')).toEqual([]);
334+
expect(await columnOf('sys_metadata_history', { name: 'boundary_body_row' }, 'name')).toEqual([]);
335+
for (const value of await columnOf('sys_metadata', { type: 'action' }, 'tags')) {
336+
expect(value, 'the predicate update landed').not.toContain('body-wrote');
337+
}
338+
});
339+
340+
it(`invoked by the ${role}: the same body's write of an ordinary table lands (control)`, async () => {
341+
const before = (await columnOf(ORDINARY, { title: 'body-wrote' }, 'title')).length;
342+
const res = await as(token(), 'POST', `/actions/${ORDINARY}/body_inserts_ordinary`, { params: {} });
343+
expect(res.status, JSON.stringify(await readJson(res))).toBe(200);
344+
expect((await columnOf(ORDINARY, { title: 'body-wrote' }, 'title')).length).toBe(before + 1);
345+
});
346+
}
347+
});

0 commit comments

Comments
 (0)