@relayfile/relay-helpers 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/receipt.js CHANGED
@@ -1,12 +1,103 @@
1
- /**
2
- * Normalize the writeback receipt into the `{ id, url }` shape the old typed
3
- * clients returned. Falls back to the draft path when the worker hasn't
4
- * written a receipt yet (fire-and-forget / timeout).
5
- */
1
+ import { RelayfileWritebackPendingError, RelayfileWritebackTerminalError, } from '@relayfile/adapter-core/vfs-client';
2
+ function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ function nonEmptyString(value) {
6
+ return typeof value === 'string' && value.trim() ? value : undefined;
7
+ }
8
+ function receiptIdentifier(value) {
9
+ if (typeof value === 'number' && Number.isFinite(value)) {
10
+ return String(value);
11
+ }
12
+ return nonEmptyString(value);
13
+ }
14
+ function hasReceiptEvidence(receipt) {
15
+ return (receiptIdentifier(receipt.created) !== undefined ||
16
+ receiptIdentifier(receipt.id) !== undefined ||
17
+ receiptIdentifier(receipt.externalId) !== undefined ||
18
+ receiptIdentifier(receipt.identifier) !== undefined ||
19
+ nonEmptyString(receipt.path) !== undefined ||
20
+ nonEmptyString(receipt.url) !== undefined ||
21
+ receiptIdentifier(receipt.ts) !== undefined ||
22
+ typeof receipt.merged === 'boolean' ||
23
+ nonEmptyString(receipt.merged) !== undefined ||
24
+ receiptIdentifier(receipt.sha) !== undefined ||
25
+ receipt.ok === true);
26
+ }
27
+ function createdFromError(error) {
28
+ if (error instanceof RelayfileWritebackPendingError) {
29
+ return { status: 'pending', id: error.path, url: '', path: error.path };
30
+ }
31
+ if (error instanceof RelayfileWritebackTerminalError) {
32
+ const path = nonEmptyString(error.path);
33
+ if (!path) {
34
+ throw error;
35
+ }
36
+ return {
37
+ status: 'dropped',
38
+ id: path,
39
+ url: '',
40
+ path,
41
+ reason: error.cause instanceof Error ? error.cause.message : `writeback_${error.status}`,
42
+ };
43
+ }
44
+ throw error;
45
+ }
46
+ function isPromiseLike(value) {
47
+ return isRecord(value) && typeof value.then === 'function';
48
+ }
6
49
  export function created(result) {
50
+ if (isPromiseLike(result)) {
51
+ return Promise.resolve(result).then(created).catch(createdFromError);
52
+ }
53
+ if (!isRecord(result)) {
54
+ throw new TypeError('created() expected a writeback result object');
55
+ }
56
+ const path = nonEmptyString(result.path);
57
+ if (!path) {
58
+ throw new TypeError('created() expected a non-empty writeback result path');
59
+ }
60
+ const receipt = result.receipt;
61
+ if (receipt !== undefined && !isRecord(receipt)) {
62
+ throw new TypeError('created() expected receipt to be an object when present');
63
+ }
64
+ const status = result.deliveryStatus ?? (receipt ? 'confirmed' : 'pending');
65
+ if (status !== 'confirmed' && status !== 'pending' && status !== 'dropped') {
66
+ throw new TypeError(`created() received unknown writeback status "${String(status)}"`);
67
+ }
68
+ if (status === 'confirmed') {
69
+ if (!receipt) {
70
+ throw new TypeError('created() received confirmed status without a receipt');
71
+ }
72
+ if (!hasReceiptEvidence(receipt)) {
73
+ throw new TypeError('created() received a confirmed receipt without delivery evidence');
74
+ }
75
+ return {
76
+ status,
77
+ id: receiptIdentifier(receipt.created) ??
78
+ receiptIdentifier(receipt.id) ??
79
+ receiptIdentifier(receipt.externalId) ??
80
+ receiptIdentifier(receipt.identifier) ??
81
+ receiptIdentifier(receipt.sha) ??
82
+ receiptIdentifier(receipt.ts) ??
83
+ path,
84
+ url: nonEmptyString(receipt.url) ?? '',
85
+ path,
86
+ receipt,
87
+ };
88
+ }
89
+ if (status === 'pending') {
90
+ if (receipt) {
91
+ throw new TypeError('created() received pending status with a receipt');
92
+ }
93
+ return { status, id: path, url: '', path };
94
+ }
7
95
  return {
8
- id: result.receipt?.created ?? result.receipt?.id ?? result.path,
9
- url: result.receipt?.url ?? result.path
96
+ status,
97
+ id: path,
98
+ url: '',
99
+ path,
100
+ ...(receipt ? { receipt } : {}),
10
101
  };
11
102
  }
12
103
  //# sourceMappingURL=receipt.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"receipt.js","sourceRoot":"","sources":["../src/receipt.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,UAAU,OAAO,CAAC,MAAuB;IAC7C,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,IAAI,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,MAAM,CAAC,IAAI;QAChE,GAAG,EAAE,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,MAAM,CAAC,IAAI;KACxC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"receipt.js","sourceRoot":"","sources":["../src/receipt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=receipt.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt.test.d.ts","sourceRoot":"","sources":["../src/receipt.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,301 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtemp } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import nodePath from 'node:path';
5
+ import test from 'node:test';
6
+ import { RelayfileWritebackAdmissionTimeoutError, RelayfileWritebackTerminalError, } from '@relayfile/adapter-core/vfs-client';
7
+ import { created, githubClient, linearClient, PreviewTransport, providerClient, } from './index.js';
8
+ const issueCommentsPath = providerClient('github')['issue-comments'].path({
9
+ owner: 'o',
10
+ repo: 'r',
11
+ issueNumber: 1,
12
+ });
13
+ const path = `${issueCommentsPath}/issue-comments draft.json`;
14
+ test('created preserves confirmed receipts and never substitutes a path for url', () => {
15
+ assert.deepEqual(created({
16
+ path,
17
+ absolutePath: path,
18
+ deliveryStatus: 'confirmed',
19
+ receipt: { created: 'comment-123', url: 'https://github.example/comments/123' },
20
+ }), {
21
+ status: 'confirmed',
22
+ id: 'comment-123',
23
+ url: 'https://github.example/comments/123',
24
+ path,
25
+ receipt: { created: 'comment-123', url: 'https://github.example/comments/123' },
26
+ });
27
+ const withoutUrl = created({
28
+ path,
29
+ absolutePath: path,
30
+ deliveryStatus: 'confirmed',
31
+ receipt: { externalId: 'comment-456' },
32
+ });
33
+ assert.equal(withoutUrl.status, 'confirmed');
34
+ assert.equal(withoutUrl.id, 'comment-456');
35
+ assert.equal(withoutUrl.url, '');
36
+ assert.notEqual(withoutUrl.url, path);
37
+ });
38
+ test('created infers confirmed for legacy transports that return a receipt', () => {
39
+ const result = created({
40
+ path,
41
+ absolutePath: path,
42
+ receipt: { id: 'legacy-123', url: 'https://linear.example/legacy-123' },
43
+ });
44
+ assert.equal(result.status, 'confirmed');
45
+ assert.equal(result.id, 'legacy-123');
46
+ assert.equal(result.url, 'https://linear.example/legacy-123');
47
+ });
48
+ test('created normalizes runtime numeric ids and uses sha or ts as primary receipt identifiers', () => {
49
+ const numeric = created({
50
+ path,
51
+ absolutePath: path,
52
+ deliveryStatus: 'confirmed',
53
+ receipt: { id: 123 },
54
+ });
55
+ assert.equal(numeric.status, 'confirmed');
56
+ assert.equal(numeric.id, '123');
57
+ for (const [receipt, expected] of [
58
+ [{ sha: 'commit-abc123' }, 'commit-abc123'],
59
+ [{ ts: '1781870464.800039' }, '1781870464.800039'],
60
+ ]) {
61
+ const result = created({ path, absolutePath: path, deliveryStatus: 'confirmed', receipt });
62
+ assert.equal(result.status, 'confirmed');
63
+ assert.equal(result.id, expected);
64
+ assert.notEqual(result.id, path);
65
+ }
66
+ });
67
+ test('created returns pending without throwing when a receipt is not yet visible', () => {
68
+ const explicit = created({ path, absolutePath: path, deliveryStatus: 'pending' });
69
+ const legacy = created({ path, absolutePath: path });
70
+ for (const result of [explicit, legacy]) {
71
+ assert.deepEqual(result, { status: 'pending', id: path, url: '', path });
72
+ assert.notEqual(result.url, path);
73
+ }
74
+ });
75
+ test('created preserves an authoritative dropped result without throwing', () => {
76
+ assert.deepEqual(created({ path, absolutePath: path, deliveryStatus: 'dropped' }), { status: 'dropped', id: path, url: '', path });
77
+ const diagnostic = { ok: false, reason: 'path_not_mounted' };
78
+ assert.deepEqual(created({ path, absolutePath: path, deliveryStatus: 'dropped', receipt: diagnostic }), { status: 'dropped', id: path, url: '', path, receipt: diagnostic });
79
+ });
80
+ test('created rejects malformed or unknown runtime transport results clearly', () => {
81
+ const malformed = [
82
+ null,
83
+ undefined,
84
+ 'not-a-result',
85
+ {},
86
+ { path: ' ', absolutePath: path },
87
+ { path, absolutePath: path, receipt: 'not-a-receipt' },
88
+ { path, absolutePath: path, deliveryStatus: 'unknown' },
89
+ { path, absolutePath: path, deliveryStatus: 'confirmed' },
90
+ { path, absolutePath: path, receipt: {} },
91
+ { path, absolutePath: path, receipt: { provider: 'github' } },
92
+ { path, absolutePath: path, receipt: { id: Number.NaN } },
93
+ { path, absolutePath: path, deliveryStatus: 'pending', receipt: { id: 'contradiction' } },
94
+ ];
95
+ for (const value of malformed) {
96
+ assert.throws(() => created(value), TypeError);
97
+ }
98
+ });
99
+ class FixedResultTransport {
100
+ status;
101
+ writes = [];
102
+ constructor(status) {
103
+ this.status = status;
104
+ }
105
+ async read(_request) {
106
+ throw new Error('read not expected');
107
+ }
108
+ async list(_request) {
109
+ throw new Error('list not expected');
110
+ }
111
+ async write(request) {
112
+ this.writes.push(request);
113
+ return {
114
+ path: request.path,
115
+ absolutePath: request.path,
116
+ deliveryStatus: this.status,
117
+ ...(this.status === 'confirmed'
118
+ ? { receipt: { id: `${request.resource}-id`, url: `https://provider.example/${request.resource}-id` } }
119
+ : {}),
120
+ };
121
+ }
122
+ }
123
+ test('GitHub and Linear ergonomic create helpers preserve every transport status', async () => {
124
+ for (const status of ['confirmed', 'pending', 'dropped']) {
125
+ const transport = new FixedResultTransport(status);
126
+ const github = githubClient({ transport });
127
+ const linear = linearClient({ transport });
128
+ const results = await Promise.all([
129
+ github.comment({ owner: 'o', repo: 'r', number: 1 }, 'comment'),
130
+ github.createIssue({ owner: 'o', repo: 'r', title: 'issue', body: 'body' }),
131
+ github.createPullRequest({ owner: 'o', repo: 'r', title: 'pr', head: 'feature', base: 'main' }),
132
+ linear.agentActivity('session-1', { type: 'thought', body: 'working' }),
133
+ linear.respond('session-2', 'done'),
134
+ linear.acknowledge('session-3'),
135
+ linear.comment('issue-1', 'comment'),
136
+ linear.createIssue({ teamId: 'team-1', title: 'issue' }),
137
+ linear.createLabel({ name: 'label' }),
138
+ ]);
139
+ assert.equal(results.length, 9);
140
+ assert.equal(transport.writes.length, 9);
141
+ for (const result of results) {
142
+ assert.equal(result.status, status);
143
+ if (status === 'confirmed') {
144
+ assert.match(result.url, /^https:\/\/provider\.example\//u);
145
+ }
146
+ else {
147
+ assert.equal(result.url, '');
148
+ assert.equal(result.id, result.path);
149
+ }
150
+ }
151
+ }
152
+ });
153
+ test('mount-backed ergonomic helpers report both immediate and timed-out writes as pending', async () => {
154
+ for (const writebackTimeoutMs of [0, 20]) {
155
+ const root = await mkdtemp(nodePath.join(tmpdir(), 'relay-created-pending-'));
156
+ const result = await githubClient({
157
+ relayfileMountRoot: root,
158
+ writebackTimeoutMs,
159
+ writebackPollMs: 5,
160
+ }).comment({ owner: 'o', repo: 'r', number: 1 }, 'pending');
161
+ assert.equal(result.status, 'pending');
162
+ assert.equal(result.id, result.path);
163
+ assert.equal(result.url, '');
164
+ }
165
+ });
166
+ test('preview receipts flow through ergonomic helpers as confirmed', async () => {
167
+ const result = await linearClient({ transport: new PreviewTransport() })
168
+ .createIssue({ teamId: 'team-1', title: 'preview' });
169
+ assert.equal(result.status, 'confirmed');
170
+ assert.match(result.id, /^preview-linear-issues-/u);
171
+ assert.equal(result.url, '');
172
+ });
173
+ test('direct HTTP receipt timeout becomes pending instead of a retry-triggering throw', async () => {
174
+ const fetchImpl = async (input) => {
175
+ const url = String(input);
176
+ if (url.includes('/fs/file')) {
177
+ return Response.json({
178
+ opId: 'op_pending_created',
179
+ status: 'queued',
180
+ targetRevision: 'rev-1',
181
+ writeback: { provider: 'github', state: 'pending' },
182
+ });
183
+ }
184
+ if (url.includes('/ops/op_pending_created')) {
185
+ return Response.json({ opId: 'op_pending_created', status: 'running', attemptCount: 1 });
186
+ }
187
+ return Response.json({ code: 'not_found' }, { status: 404 });
188
+ };
189
+ const result = await githubClient({
190
+ relayfileBaseUrl: 'https://relayfile.example.test',
191
+ relayfileApiToken: 'token',
192
+ workspaceId: 'workspace-1',
193
+ fetchImpl,
194
+ writebackTimeoutMs: 20,
195
+ writebackPollMs: 5,
196
+ }).comment({ owner: 'o', repo: 'r', number: 1 }, 'pending');
197
+ assert.equal(result.status, 'pending');
198
+ assert.equal(result.path.startsWith(`${issueCommentsPath}/`), true);
199
+ assert.equal(result.url, '');
200
+ });
201
+ test('direct HTTP terminal operation evidence becomes dropped without retrying', async () => {
202
+ let opReads = 0;
203
+ const fetchImpl = async (input) => {
204
+ const url = String(input);
205
+ if (url.includes('/fs/file')) {
206
+ return Response.json({
207
+ opId: 'op_dropped_created',
208
+ status: 'queued',
209
+ targetRevision: 'rev-1',
210
+ writeback: { provider: 'linear', state: 'pending' },
211
+ });
212
+ }
213
+ if (url.includes('/ops/op_dropped_created')) {
214
+ opReads += 1;
215
+ return Response.json({
216
+ opId: 'op_dropped_created',
217
+ status: 'dead_lettered',
218
+ attemptCount: 1,
219
+ lastError: 'path is outside the mounted integration scope',
220
+ });
221
+ }
222
+ return Response.json({ code: 'not_found' }, { status: 404 });
223
+ };
224
+ const result = await linearClient({
225
+ relayfileBaseUrl: 'https://relayfile.example.test',
226
+ relayfileApiToken: 'token',
227
+ workspaceId: 'workspace-1',
228
+ fetchImpl,
229
+ writebackTimeoutMs: 100,
230
+ writebackPollMs: 5,
231
+ }).createIssue({ teamId: 'team-1', title: 'dropped' });
232
+ assert.equal(result.status, 'dropped');
233
+ assert.equal(opReads, 1);
234
+ assert.match(result.reason ?? '', /dead_lettered.*outside the mounted integration scope/u);
235
+ assert.equal(result.url, '');
236
+ });
237
+ test('ambiguous admission timeouts remain errors rather than false dropped certainty', async () => {
238
+ const admissionError = new RelayfileWritebackAdmissionTimeoutError({
239
+ provider: 'github',
240
+ operation: 'write.issue-comments',
241
+ path,
242
+ timeoutMs: 20,
243
+ });
244
+ const transport = {
245
+ read: async () => undefined,
246
+ list: async () => [],
247
+ write: async () => { throw admissionError; },
248
+ };
249
+ await assert.rejects(() => githubClient({ transport }).comment({ owner: 'o', repo: 'r', number: 1 }, 'ambiguous'), (error) => error === admissionError);
250
+ });
251
+ test('legacy pathless terminal errors remain constructible and are not misclassified as dropped', async () => {
252
+ const legacyError = new RelayfileWritebackTerminalError({
253
+ provider: 'github',
254
+ operation: 'write.issue-comments',
255
+ opId: 'op_legacy_terminal',
256
+ status: 'failed',
257
+ lastError: 'legacy caller has no path',
258
+ });
259
+ const transport = {
260
+ read: async () => undefined,
261
+ list: async () => [],
262
+ write: async () => { throw legacyError; },
263
+ };
264
+ assert.equal(legacyError.path, undefined);
265
+ await assert.rejects(() => githubClient({ transport }).comment({ owner: 'o', repo: 'r', number: 1 }, 'legacy'), (error) => error === legacyError);
266
+ });
267
+ function assertCreatedOverloads(result, pending) {
268
+ const sync = created(result);
269
+ const asyncResult = created(pending);
270
+ void sync;
271
+ void asyncResult;
272
+ }
273
+ void assertCreatedOverloads;
274
+ function assertCreatedResultNarrowing(result) {
275
+ switch (result.status) {
276
+ case 'confirmed': {
277
+ const narrowed = result;
278
+ const url = narrowed.url;
279
+ return url;
280
+ }
281
+ case 'pending': {
282
+ const narrowed = result;
283
+ const url = narrowed.url;
284
+ return url;
285
+ }
286
+ case 'dropped': {
287
+ const narrowed = result;
288
+ const url = narrowed.url;
289
+ return url;
290
+ }
291
+ default: {
292
+ const exhaustive = result;
293
+ return exhaustive;
294
+ }
295
+ }
296
+ }
297
+ void assertCreatedResultNarrowing;
298
+ // @ts-expect-error unknown statuses cannot enter the typed writeback contract
299
+ const invalidStatus = { path, absolutePath: path, deliveryStatus: 'unknown' };
300
+ void invalidStatus;
301
+ //# sourceMappingURL=receipt.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"receipt.test.js","sourceRoot":"","sources":["../src/receipt.test.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,7 @@ import { RelayfileWritebackPendingError, RelayfileWritebackReceiptError } from '
8
8
  import { WRITEBACK_PATH_CATALOG } from '@relayfile/adapter-core/writeback-paths';
9
9
  import { linearByUuidAliasPath } from '@relayfile/adapter-linear/path-mapper';
10
10
  import * as helpers from './index.js';
11
- import { githubClient, linearClient, notionClient, providerClient, relayClient, slackClient, telegramClient, telegramReceiptTs, } from './index.js';
11
+ import { PreviewTransport, RelayWriteAuthorizationError, bindRelayWriteAuthorizer, githubClient, linearClient, notionClient, providerClient, relayClient, slackClient, telegramClient, telegramReceiptTs, } from './index.js';
12
12
  const clientExportName = (provider) => `${provider.replace(/-([a-z])/g, (_m, c) => c.toUpperCase())}Client`;
13
13
  /** Fire-and-forget client bound to a throwaway mount; no writeback worker runs. */
14
14
  async function mount() {
@@ -28,6 +28,39 @@ test('relayClient.path resolves catalog paths and write drops a collection draft
28
28
  const draft = await onlyJsonIn(path.join(root, 'linear/issues/ISS-1/comments'));
29
29
  assert.deepEqual(draft.body, { body: 'hi' });
30
30
  });
31
+ test('final-write authorization redirects the native VFS fallback before filesystem mutation', async () => {
32
+ const { root, opts } = await mount();
33
+ const canonicalPreview = new PreviewTransport();
34
+ const restoreAuthorization = bindRelayWriteAuthorizer(() => ({
35
+ allowed: true,
36
+ transport: canonicalPreview,
37
+ }));
38
+ try {
39
+ await relayClient('linear', opts).write('comments', { issueId: 'ISS-redirect' }, { body: 'preview' });
40
+ }
41
+ finally {
42
+ restoreAuthorization();
43
+ }
44
+ assert.deepEqual(await readdir(root), []);
45
+ assert.equal(canonicalPreview.actions.length, 1);
46
+ assert.equal(canonicalPreview.actions[0]?.path.startsWith('/linear/issues/ISS-redirect/comments/'), true);
47
+ });
48
+ test('final-write denial rejects the native VFS fallback before filesystem mutation', async () => {
49
+ const { root, opts } = await mount();
50
+ let deniedActions = 0;
51
+ const restoreAuthorization = bindRelayWriteAuthorizer(() => {
52
+ deniedActions += 1;
53
+ return { allowed: false };
54
+ });
55
+ try {
56
+ await assert.rejects(() => relayClient('linear', opts).write('comments', { issueId: 'ISS-denied' }, { body: 'never written' }), RelayWriteAuthorizationError);
57
+ }
58
+ finally {
59
+ restoreAuthorization();
60
+ }
61
+ assert.equal(deniedActions, 1);
62
+ assert.deepEqual(await readdir(root), []);
63
+ });
31
64
  test('relayClient.write writes item (.json) resources to the exact path', async () => {
32
65
  const { root, opts } = await mount();
33
66
  const gh = relayClient('github', opts);
@@ -362,6 +395,71 @@ test('src/generated/clients.ts is in sync with the catalog', async () => {
362
395
  const committed = await readFile(path.join(pkgRoot, 'src/generated/clients.ts'), 'utf8');
363
396
  assert.equal(committed, renderClients(), 'generated clients are stale — run `npm run gen -w @relayfile/relay-helpers`');
364
397
  });
398
+ test('all helper write sites converge on the final-write authorization boundary', async () => {
399
+ const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
400
+ const srcRoot = path.join(pkgRoot, 'src');
401
+ const { findWriteBoundaryViolations, formatWriteBoundaryViolations } = await import(pathToFileURL(path.join(pkgRoot, 'scripts/verify-write-boundary.mjs')).href);
402
+ const violations = await findWriteBoundaryViolations({ sourceRoot: srcRoot });
403
+ assert.deepEqual(violations, [], `helper write paths bypass the final-write authorizer:\n${formatWriteBoundaryViolations(violations)}`);
404
+ });
405
+ test('write-boundary guard rejects nested renamed, bracket, destructured, aliased, and VFS canaries', async () => {
406
+ const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
407
+ const { findWriteBoundaryViolations } = await import(pathToFileURL(path.join(pkgRoot, 'scripts/verify-write-boundary.mjs')).href);
408
+ const canaryRoot = await mkdtemp(path.join(tmpdir(), 'relay-write-boundary-canary-'));
409
+ const nestedRoot = path.join(canaryRoot, 'future/nested');
410
+ await mkdir(nestedRoot, { recursive: true });
411
+ await writeFile(path.join(nestedRoot, 'bypass.ts'), `
412
+ import { writeJsonFile as persist } from '@relayfile/adapter-core/vfs-client';
413
+
414
+ interface Request {
415
+ provider: string;
416
+ resource: string;
417
+ parameters: Record<string, string>;
418
+ path: string;
419
+ body: unknown;
420
+ }
421
+ interface FutureTransport {
422
+ write(request: Request): Promise<unknown>;
423
+ }
424
+
425
+ export async function renamed(selectedTransport: FutureTransport, request: Request) {
426
+ await selectedTransport.write(request);
427
+ await selectedTransport['write'](request);
428
+ const { write: destructured } = selectedTransport;
429
+ await destructured(request);
430
+ const aliased = selectedTransport.write;
431
+ await aliased(request);
432
+ await persist(
433
+ {},
434
+ 'future',
435
+ 'write.future',
436
+ '/future/path.json',
437
+ request.body,
438
+ );
439
+ }
440
+ `);
441
+ const nestedAuthorizerPath = path.join(nestedRoot, 'write-authorizer.ts');
442
+ await writeFile(nestedAuthorizerPath, `
443
+ interface Request {
444
+ provider: string;
445
+ resource: string;
446
+ parameters: Record<string, string>;
447
+ path: string;
448
+ body: unknown;
449
+ }
450
+ interface FutureTransport {
451
+ write(request: Request): Promise<unknown>;
452
+ }
453
+
454
+ export async function nestedAuthorizerBypass(transport: FutureTransport, request: Request) {
455
+ await transport.write(request);
456
+ }
457
+ `);
458
+ const violations = await findWriteBoundaryViolations({ sourceRoot: canaryRoot });
459
+ assert.ok(violations.filter((violation) => violation.kind === 'raw-transport-write').length >= 4, `expected every raw transport canary to fail: ${JSON.stringify(violations)}`);
460
+ assert.ok(violations.some((violation) => violation.kind.startsWith('native-vfs-write')), `expected multiline aliased VFS canary to fail: ${JSON.stringify(violations)}`);
461
+ assert.ok(violations.some((violation) => violation.kind === 'raw-transport-write' && violation.file === nestedAuthorizerPath), `expected nested write-authorizer.ts canary to fail: ${JSON.stringify(violations)}`);
462
+ });
365
463
  test('a named resource-keyed client resolves and writes catalog paths', async () => {
366
464
  const { root, opts } = await mount();
367
465
  const notion = notionClient(opts);