Skip to content

Commit 4e87638

Browse files
committed
test(dogfood): webhook meta read doors withhold declared credentials in both compositions (#24994)
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
1 parent f65b0c9 commit 4e87638

1 file changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// [ruling B+ on #24966, #24994] Every meta read door withholds a webhook's
4+
// declared credentials — `secret` (a credential value) and `headers` (a
5+
// credential map, as a whole) — in BOTH compositions: without
6+
// `@objectstack/plugin-webhooks`, and with it mounted.
7+
//
8+
// The rule under test: `WebhookSchema` marks the two fields with
9+
// `.meta({ credential: … })`, and `getMetadataTypeRedactor('webhook')`
10+
// resolves a redactor DERIVED from those markers (`@objectstack/spec/kernel`),
11+
// present from module load whatever the composition mounts. A webhook's items
12+
// reach the meta read doors identically with and without the plugin (declared
13+
// ones through manifest decomposition, saved ones through the meta save door),
14+
// so a plugin registration would protect only one of the two compositions;
15+
// this file boots both.
16+
//
17+
// Doors pinned, per composition: the meta list (both spellings), the item read,
18+
// the layered view, the published read, the metadata protocol's list and item
19+
// reads, and the stored-row data door with its history twin. Each door read
20+
// carries a positive control (the response names the webhook), so a refusal or
21+
// an empty answer cannot pass for a withheld credential.
22+
//
23+
// Also pinned: the stored record is never mutated (redaction is a serving
24+
// act); a save of the served form keeps the stored values (the metadata
25+
// protocol's carry-forward); and, with the plugin mounted, the dispatcher's
26+
// declared-webhook source still carries both credentials, so delivery signing
27+
// is unaffected.
28+
//
29+
// Disclosure: every credential below is an obviously-fake placeholder, and
30+
// every assertion is absence by rule, never a served value.
31+
32+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
33+
import { bootStack, type VerifyStack } from '@objectstack/verify';
34+
import { MessagingServicePlugin } from '@objectstack/service-messaging';
35+
import { WebhookOutboxPlugin } from '@objectstack/plugin-webhooks';
36+
import { defineStack } from '@objectstack/spec';
37+
import { ObjectSchema, Field } from '@objectstack/spec/data';
38+
import { defineWebhook } from '@objectstack/spec/automation';
39+
40+
const SYSTEM = { isSystem: true, positions: [], permissions: [] };
41+
42+
/** Obviously-fake placeholders: absence of these strings is the whole assertion. */
43+
const DECLARED_SECRET = 'TEST_SECRET_DECLARED_24994';
44+
const DECLARED_HEADER = 'TEST_HEADER_DECLARED_24994';
45+
const SAVED_SECRET = 'TEST_SECRET_SAVED_24994';
46+
const SAVED_HEADER = 'TEST_HEADER_SAVED_24994';
47+
const PLACEHOLDERS = [DECLARED_SECRET, DECLARED_HEADER, SAVED_SECRET, SAVED_HEADER] as const;
48+
49+
const DECLARED = 'wcr_declared';
50+
const SAVED = 'wcr_saved';
51+
52+
const WcrTask = ObjectSchema.create({
53+
name: 'wcr_task',
54+
sharingModel: 'public_read_write',
55+
label: 'WCR Task',
56+
pluralLabel: 'WCR Tasks',
57+
fields: { name: Field.text({ label: 'Name', required: true }) },
58+
});
59+
60+
const fixture = defineStack({
61+
manifest: {
62+
id: 'com.dogfood.webhook-credential-redaction',
63+
namespace: 'wcr',
64+
version: '0.0.0',
65+
type: 'app',
66+
name: 'Webhook Credential Redaction Fixture',
67+
description: 'One object and one declared webhook carrying both declared credential fields, for the meta read-door pins of ruling B+ on #24966.',
68+
},
69+
objects: [WcrTask],
70+
webhooks: [
71+
defineWebhook({
72+
name: DECLARED,
73+
label: 'WCR Declared',
74+
object: 'wcr_task',
75+
triggers: ['create'],
76+
url: 'https://hooks.example/wcr-declared',
77+
headers: { 'X-Placeholder': DECLARED_HEADER },
78+
secret: DECLARED_SECRET,
79+
isActive: false,
80+
}),
81+
],
82+
});
83+
84+
const savedBody = () => ({
85+
name: SAVED,
86+
label: 'WCR Saved',
87+
object: 'wcr_task',
88+
triggers: ['update'],
89+
url: 'https://hooks.example/wcr-saved',
90+
headers: { 'X-Placeholder': SAVED_HEADER },
91+
secret: SAVED_SECRET,
92+
isActive: false,
93+
});
94+
95+
const rowsOf = (r: any): any[] => (Array.isArray(r) ? r : Array.isArray(r?.records) ? r.records : Array.isArray(r?.value) ? r.value : []);
96+
const holdsAPlaceholder = (v: unknown): boolean => {
97+
const text = typeof v === 'string' ? v : JSON.stringify(v ?? null);
98+
return PLACEHOLDERS.some((p) => text.includes(p));
99+
};
100+
const bodyOf = (row: any): Record<string, unknown> =>
101+
typeof row?.metadata === 'string' ? JSON.parse(row.metadata) : (row?.metadata ?? {});
102+
103+
const COMPOSITIONS = [
104+
{ label: 'without plugin-webhooks', plugins: () => [] as unknown[] },
105+
{ label: 'with plugin-webhooks mounted', plugins: () => [new MessagingServicePlugin(), new WebhookOutboxPlugin()] },
106+
] as const;
107+
108+
for (const composition of COMPOSITIONS) {
109+
describe(`[ruling B+] every meta read door withholds a webhook's declared credentials — ${composition.label}`, () => {
110+
let stack: VerifyStack;
111+
let admin: string;
112+
let member: string;
113+
let ql: any;
114+
let protocol: any;
115+
116+
const stored = async (object: 'sys_metadata' | 'sys_metadata_history', name: string) =>
117+
rowsOf(await ql.find(object, { where: { type: 'webhook', name }, context: SYSTEM }));
118+
119+
/** One door read: 200, names the webhook (positive control), and holds no credential. */
120+
const expectWithheld = async (token: string, path: string, names: readonly string[]) => {
121+
const res = await stack.apiAs(token, 'GET', path);
122+
const text = await res.text();
123+
expect(res.status, `${path}: ${text.slice(0, 200)}`).toBe(200);
124+
for (const name of names) expect(text.includes(name), `${path} names ${name}`).toBe(true);
125+
expect(holdsAPlaceholder(text), `${path} withholds the declared credential fields`).toBe(false);
126+
};
127+
128+
beforeAll(async () => {
129+
stack = await bootStack(fixture as never, { extraPlugins: composition.plugins() as never });
130+
ql = await stack.kernel.getServiceAsync('objectql');
131+
protocol = await stack.kernel.getServiceAsync('protocol');
132+
admin = await stack.signIn();
133+
member = await stack.signUp(`wcr-member-${composition.plugins().length}@redaction.test`);
134+
const put = await stack.apiAs(admin, 'PUT', `/meta/webhook/${SAVED}`, savedBody());
135+
expect(put.status, await put.clone().text()).toBe(200);
136+
}, 180_000);
137+
138+
afterAll(async () => {
139+
await stack?.stop?.();
140+
});
141+
142+
it('guard the guard: the saved webhook is stored with both credential fields, and the plugin is mounted exactly when this composition says so', async () => {
143+
const [row] = await stored('sys_metadata', SAVED);
144+
expect(bodyOf(row).secret).toBe(SAVED_SECRET);
145+
expect(bodyOf(row).headers).toEqual({ 'X-Placeholder': SAVED_HEADER });
146+
let declaredService: unknown;
147+
try {
148+
declaredService = stack.kernel.getService('declaredWebhooks');
149+
} catch {
150+
declaredService = undefined;
151+
}
152+
expect(Boolean(declaredService)).toBe(composition.plugins().length > 0);
153+
});
154+
155+
it('the meta list, both spellings, withholds both fields of the declared and the saved webhook', async () => {
156+
for (const token of [admin, member]) {
157+
await expectWithheld(token, '/meta/webhook', [DECLARED, SAVED]);
158+
await expectWithheld(token, '/meta/webhooks', [DECLARED, SAVED]);
159+
}
160+
});
161+
162+
it('the item read and the layered view withhold both fields', async () => {
163+
for (const token of [admin, member]) {
164+
for (const name of [DECLARED, SAVED]) {
165+
await expectWithheld(token, `/meta/webhook/${name}`, [name]);
166+
await expectWithheld(token, `/meta/webhook/${name}/layers`, [name]);
167+
}
168+
}
169+
});
170+
171+
it('the published read withholds both fields', async () => {
172+
for (const token of [admin, member]) await expectWithheld(token, `/meta/webhook/${SAVED}/published`, [SAVED]);
173+
});
174+
175+
it('the metadata protocol\'s list and item reads withhold both fields', async () => {
176+
const listed = await protocol.getMetaItems({ type: 'webhook' });
177+
expect(JSON.stringify(listed).includes(SAVED)).toBe(true);
178+
expect(holdsAPlaceholder(listed)).toBe(false);
179+
for (const name of [DECLARED, SAVED]) {
180+
const item = await protocol.getMetaItem({ type: 'webhook', name });
181+
expect(JSON.stringify(item).includes(name), name).toBe(true);
182+
expect(holdsAPlaceholder(item), name).toBe(false);
183+
}
184+
});
185+
186+
it('the stored-row data door and its history twin withhold both fields', async () => {
187+
const filter = encodeURIComponent(JSON.stringify({ type: 'webhook', name: SAVED }));
188+
await expectWithheld(admin, `/data/sys_metadata?filter=${filter}`, [SAVED]);
189+
await expectWithheld(admin, `/data/sys_metadata_history?filter=${filter}`, [SAVED]);
190+
});
191+
192+
it('a save of the served form keeps the stored values (the carry-forward), and the store is never redacted', async () => {
193+
const read = await stack.apiAs(admin, 'GET', `/meta/webhook/${SAVED}`);
194+
expect(read.status).toBe(200);
195+
const json = (await read.json()) as any;
196+
const served = (json?.item ?? json?.data?.item ?? json?.data ?? json) as Record<string, unknown>;
197+
expect(served?.name).toBe(SAVED);
198+
const body = Object.fromEntries(Object.entries(served).filter(([k]) => !k.startsWith('_')));
199+
expect(Object.keys(body)).not.toContain('secret');
200+
expect(Object.keys(body)).not.toContain('headers');
201+
202+
const put = await stack.apiAs(admin, 'PUT', `/meta/webhook/${SAVED}`, { ...body, label: 'WCR Saved (edited)' });
203+
expect(put.status, await put.clone().text()).toBe(200);
204+
205+
const [row] = await stored('sys_metadata', SAVED);
206+
expect(bodyOf(row).label).toBe('WCR Saved (edited)');
207+
expect(bodyOf(row).secret).toBe(SAVED_SECRET);
208+
expect(bodyOf(row).headers).toEqual({ 'X-Placeholder': SAVED_HEADER });
209+
});
210+
211+
if (composition.plugins().length > 0) {
212+
it('the dispatcher\'s declared-webhook source still carries both credentials: delivery signing is unaffected', async () => {
213+
const declaredWebhooks = (await stack.kernel.getServiceAsync('declaredWebhooks')) as any;
214+
const declared = await declaredWebhooks.declaredWebhookNamed(DECLARED);
215+
expect(declared?.webhook?.secret).toBe(DECLARED_SECRET);
216+
expect(declared?.webhook?.headers).toEqual({ 'X-Placeholder': DECLARED_HEADER });
217+
});
218+
}
219+
});
220+
}

0 commit comments

Comments
 (0)