@powerhousedao/switchboard 6.2.3-dev.24 → 6.2.3-dev.26

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.
Files changed (44) hide show
  1. package/.tsbuild/test/attachments/byte-grants.test.d.ts +2 -0
  2. package/.tsbuild/test/attachments/byte-grants.test.d.ts.map +1 -0
  3. package/.tsbuild/test/attachments/byte-routes-read-gate.integration.test.d.ts +2 -0
  4. package/.tsbuild/test/attachments/byte-routes-read-gate.integration.test.d.ts.map +1 -0
  5. package/.tsbuild/test/attachments/url-signer.test.d.ts +2 -0
  6. package/.tsbuild/test/attachments/url-signer.test.d.ts.map +1 -0
  7. package/.tsbuild/test/workflow-read-gate.test.d.ts +2 -0
  8. package/.tsbuild/test/workflow-read-gate.test.d.ts.map +1 -0
  9. package/.tsbuild/tsconfig.tsbuildinfo +1 -1
  10. package/.tsbuild/tsdown.config.d.ts.map +1 -1
  11. package/.tsbuild/vitest.config.d.ts.map +1 -1
  12. package/Auth.md +55 -2
  13. package/CHANGELOG.md +41 -0
  14. package/dist/index.d.mts +2 -1
  15. package/dist/index.mjs +4 -5
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/install-packages.d.mts +2 -1
  18. package/dist/install-packages.mjs +2 -2
  19. package/dist/install-packages.mjs.map +1 -1
  20. package/dist/migrate.d.mts +2 -1
  21. package/dist/migrate.mjs +4 -3
  22. package/dist/migrate.mjs.map +1 -1
  23. package/dist/{server-BFbus-at.mjs → server-DQCIIJzK.mjs} +236 -39
  24. package/dist/server-DQCIIJzK.mjs.map +1 -0
  25. package/dist/server.d.mts +38 -24
  26. package/dist/server.d.mts.map +1 -1
  27. package/dist/server.mjs +3 -4
  28. package/dist/{utils-BBHt2kdo.mjs → utils-uaBlzyjS.mjs} +3 -3
  29. package/dist/{utils-BBHt2kdo.mjs.map → utils-uaBlzyjS.mjs.map} +1 -1
  30. package/dist/utils.d.mts +4 -5
  31. package/dist/utils.d.mts.map +1 -1
  32. package/dist/utils.mjs +3 -3
  33. package/package.json +18 -18
  34. package/test/attachments/byte-grants.test.ts +287 -0
  35. package/test/attachments/byte-routes-read-gate.integration.test.ts +314 -0
  36. package/test/attachments/download-target.test.ts +194 -31
  37. package/test/attachments/routes-integration.test.ts +20 -5
  38. package/test/attachments/routes.test.ts +43 -25
  39. package/test/attachments/service-config.test.ts +13 -3
  40. package/test/attachments/url-signer.test.ts +105 -0
  41. package/test/workflow/harness.ts +1 -1
  42. package/test/workflow-read-gate.test.ts +145 -0
  43. package/tsdown.config.ts +3 -1
  44. package/dist/server-BFbus-at.mjs.map +0 -1
@@ -14,6 +14,7 @@ import {
14
14
  import { tmpdir } from "node:os";
15
15
  import { join } from "node:path";
16
16
  import { Readable, Writable } from "node:stream";
17
+ import type { IAttachmentAccessService } from "@powerhousedao/reactor-api";
17
18
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
18
19
  import {
19
20
  buildContentDisposition,
@@ -29,6 +30,21 @@ import {
29
30
  quoteFilename,
30
31
  } from "../../src/attachments/routes.js";
31
32
 
33
+ // Byte-serving mechanics under a granted read; grant decisions are tested in
34
+ // byte-grants.test.ts.
35
+ const ALLOW_ALL: IAttachmentAccessService = {
36
+ canReadAttachment: (request) =>
37
+ Promise.resolve({
38
+ kind: "allowed",
39
+ documentId: request.documentId as never,
40
+ ref: request.attachmentRef as never,
41
+ }),
42
+ };
43
+ const grantedDownload = (attachments: AttachmentBuildResult) =>
44
+ makeDownloadHandler(attachments, ALLOW_ALL, null);
45
+ const grantedStat = (attachments: AttachmentBuildResult) =>
46
+ makeStatHandler(attachments, ALLOW_ALL, null);
47
+
32
48
  type CapturedRes = ServerResponse & {
33
49
  _headers: Record<string, string>;
34
50
  _body: Buffer;
@@ -52,7 +68,7 @@ function makeReq(opts: {
52
68
  params?: Record<string, string>;
53
69
  };
54
70
  req.method = opts.method;
55
- req.url = opts.url ?? "/";
71
+ req.url = opts.url ?? "/?documentId=doc-1";
56
72
  req.headers = {};
57
73
  if (opts.params) req.params = opts.params;
58
74
  return req as unknown as IncomingMessage;
@@ -222,7 +238,7 @@ describe("attachment routes", () => {
222
238
  expect(row!.deleted_at_utc).not.toBeNull();
223
239
 
224
240
  // Download
225
- const downloadHandler = makeDownloadHandler(attachments);
241
+ const downloadHandler = grantedDownload(attachments);
226
242
  const downloadReq = makeReq({
227
243
  method: "GET",
228
244
  params: { hash: upload.hash },
@@ -271,7 +287,7 @@ describe("attachment routes", () => {
271
287
  hash: string;
272
288
  };
273
289
 
274
- const downloadHandler = makeDownloadHandler(attachments);
290
+ const downloadHandler = grantedDownload(attachments);
275
291
  const downloadReq = makeReq({
276
292
  method: "GET",
277
293
  params: { hash: upload.hash },
@@ -314,7 +330,7 @@ describe("attachment routes", () => {
314
330
  hash: string;
315
331
  };
316
332
 
317
- const statHandler = makeStatHandler(attachments);
333
+ const statHandler = grantedStat(attachments);
318
334
  const statReq = makeReq({
319
335
  method: "HEAD",
320
336
  params: { hash: upload.hash },
@@ -373,9 +389,9 @@ describe("attachment routes", () => {
373
389
  };
374
390
 
375
391
  const { createServer } = await import("node:http");
376
- const statHandler = makeStatHandler(attachments);
392
+ const statHandler = grantedStat(attachments);
377
393
  const server = createServer((req, res) => {
378
- const m = /^\/attachments\/([^/]+)$/.exec(req.url ?? "/");
394
+ const m = /^\/attachments\/([^/?]+)(?:\?|$)/.exec(req.url ?? "/");
379
395
  if (!m) {
380
396
  res.statusCode = 404;
381
397
  res.end();
@@ -386,12 +402,14 @@ describe("attachment routes", () => {
386
402
  };
387
403
  void statHandler(req, res);
388
404
  });
389
- await new Promise<void>((resolve) => server.listen(0, resolve));
405
+ await new Promise<void>((resolve) =>
406
+ server.listen(0, "127.0.0.1", resolve),
407
+ );
390
408
  const port = (server.address() as { port: number }).port;
391
409
 
392
410
  try {
393
411
  const response = await fetch(
394
- `http://127.0.0.1:${port}/attachments/${upload.hash}`,
412
+ `http://127.0.0.1:${port}/attachments/${upload.hash}?documentId=doc-1`,
395
413
  {
396
414
  method: "HEAD",
397
415
  },
@@ -412,7 +430,7 @@ describe("attachment routes", () => {
412
430
  });
413
431
 
414
432
  it("HEAD stat returns 404 for unknown hash", async () => {
415
- const handler = makeStatHandler(attachments);
433
+ const handler = grantedStat(attachments);
416
434
  const req = makeReq({ method: "HEAD", params: { hash: "a".repeat(64) } });
417
435
  const res = makeRes();
418
436
  await handler(req, res);
@@ -421,7 +439,7 @@ describe("attachment routes", () => {
421
439
  });
422
440
 
423
441
  it("HEAD stat returns 400 for malformed hash", async () => {
424
- const handler = makeStatHandler(attachments);
442
+ const handler = grantedStat(attachments);
425
443
  const req = makeReq({ method: "HEAD", params: { hash: "not-a-hash" } });
426
444
  const res = makeRes();
427
445
  await handler(req, res);
@@ -523,7 +541,7 @@ describe("attachment routes", () => {
523
541
  });
524
542
 
525
543
  it("GET download returns 404 for unknown hash", async () => {
526
- const handler = makeDownloadHandler(attachments);
544
+ const handler = grantedDownload(attachments);
527
545
  const req = makeReq({
528
546
  method: "GET",
529
547
  params: { hash: "a".repeat(64) },
@@ -535,7 +553,7 @@ describe("attachment routes", () => {
535
553
  });
536
554
 
537
555
  it("GET download returns 400 for malformed hash", async () => {
538
- const handler = makeDownloadHandler(attachments);
556
+ const handler = grantedDownload(attachments);
539
557
  const req = makeReq({
540
558
  method: "GET",
541
559
  params: { hash: "not-a-hash" },
@@ -574,7 +592,7 @@ describe("attachment routes", () => {
574
592
  hash: string;
575
593
  };
576
594
 
577
- const handler = makeDownloadHandler(attachments);
595
+ const handler = grantedDownload(attachments);
578
596
  const req = makeReq({
579
597
  method: "GET",
580
598
  params: { hash: upload.hash.toUpperCase() },
@@ -612,7 +630,7 @@ describe("attachment routes", () => {
612
630
  hash: string;
613
631
  };
614
632
 
615
- const statHandler = makeStatHandler(attachments);
633
+ const statHandler = grantedStat(attachments);
616
634
  const statReq = makeReq({
617
635
  method: "HEAD",
618
636
  params: { hash: upload.hash.toUpperCase() },
@@ -660,7 +678,7 @@ describe("attachment routes", () => {
660
678
  throw new Error(secret);
661
679
  };
662
680
  try {
663
- const handler = makeDownloadHandler(attachments);
681
+ const handler = grantedDownload(attachments);
664
682
  const req = makeReq({
665
683
  method: "GET",
666
684
  params: { hash: "a".repeat(64) },
@@ -933,7 +951,7 @@ describe("attachment routes", () => {
933
951
  it("HEAD stat returns 202 with Retry-After and Attachment-Pending for pending hash", async () => {
934
952
  const { hash } = await createPendingReservation("pending content data");
935
953
 
936
- const handler = makeStatHandler(attachments);
954
+ const handler = grantedStat(attachments);
937
955
  const req = makeReq({ method: "HEAD", params: { hash } });
938
956
  const res = makeRes();
939
957
  await handler(req, res);
@@ -958,7 +976,7 @@ describe("attachment routes", () => {
958
976
  it("HEAD stat 202 for pending hash does not set Content-Disposition or Attachment-Metadata", async () => {
959
977
  const { hash } = await createPendingReservation("no metadata content");
960
978
 
961
- const handler = makeStatHandler(attachments);
979
+ const handler = grantedStat(attachments);
962
980
  const req = makeReq({ method: "HEAD", params: { hash } });
963
981
  const res = makeRes();
964
982
  await handler(req, res);
@@ -972,7 +990,7 @@ describe("attachment routes", () => {
972
990
  it("GET download returns 202 with Retry-After and Attachment-Pending for pending hash", async () => {
973
991
  const { hash } = await createPendingReservation("download pending data");
974
992
 
975
- const handler = makeDownloadHandler(attachments);
993
+ const handler = grantedDownload(attachments);
976
994
  const req = makeReq({ method: "GET", params: { hash } });
977
995
  const res = makeRes();
978
996
  await handler(req, res);
@@ -994,7 +1012,7 @@ describe("attachment routes", () => {
994
1012
  // the response as a successful zero-byte attachment.
995
1013
  const { hash } = await createPendingReservation("check-headers content");
996
1014
 
997
- const handler = makeDownloadHandler(attachments);
1015
+ const handler = grantedDownload(attachments);
998
1016
  const req = makeReq({ method: "GET", params: { hash } });
999
1017
  const res = makeRes();
1000
1018
  await handler(req, res);
@@ -1014,8 +1032,8 @@ describe("attachment routes", () => {
1014
1032
  );
1015
1033
 
1016
1034
  for (const [method, handler] of [
1017
- ["HEAD", makeStatHandler(attachments)],
1018
- ["GET", makeDownloadHandler(attachments)],
1035
+ ["HEAD", grantedStat(attachments)],
1036
+ ["GET", grantedDownload(attachments)],
1019
1037
  ] as const) {
1020
1038
  const req = makeReq({ method, params: { hash } });
1021
1039
  const res = makeRes();
@@ -1034,7 +1052,7 @@ describe("attachment routes", () => {
1034
1052
  it("GET download 202 body is empty (not a zero-byte attachment)", async () => {
1035
1053
  const { hash } = await createPendingReservation("empty body assertion");
1036
1054
 
1037
- const handler = makeDownloadHandler(attachments);
1055
+ const handler = grantedDownload(attachments);
1038
1056
  const req = makeReq({ method: "GET", params: { hash } });
1039
1057
  const res = makeRes();
1040
1058
  await handler(req, res);
@@ -1322,7 +1340,7 @@ describe("attachment routes", () => {
1322
1340
  it("download with non-ASCII fileName produces RFC 6266 Content-Disposition", async () => {
1323
1341
  const reserveHandler = makeReserveHandler(attachments);
1324
1342
  const uploadHandler = makeUploadHandler(attachments);
1325
- const downloadHandler = makeDownloadHandler(attachments);
1343
+ const downloadHandler = grantedDownload(attachments);
1326
1344
 
1327
1345
  const reserveReq = makeReq({
1328
1346
  method: "POST",
@@ -1392,8 +1410,8 @@ describe("attachment routes", () => {
1392
1410
  };
1393
1411
 
1394
1412
  for (const [method, handler] of [
1395
- ["HEAD", makeStatHandler(attachments)],
1396
- ["GET", makeDownloadHandler(attachments)],
1413
+ ["HEAD", grantedStat(attachments)],
1414
+ ["GET", grantedDownload(attachments)],
1397
1415
  ] as const) {
1398
1416
  const res = makeRes();
1399
1417
  await handler(makeReq({ method, params: { hash } }), res);
@@ -1,5 +1,5 @@
1
1
  import { PGlite } from "@electric-sql/pglite";
2
- import type { API } from "@powerhousedao/reactor-api";
2
+ import type { API, IAttachmentAccessService } from "@powerhousedao/reactor-api";
3
3
  import { createHttpAdapter } from "@powerhousedao/reactor-api";
4
4
  import {
5
5
  AttachmentBuilder,
@@ -109,10 +109,20 @@ describe("attachment service built from deriveAttachmentServiceConfig round-trip
109
109
  registerAttachmentRoutes({
110
110
  httpAdapter: adapter,
111
111
  attachments,
112
+ attachmentAccess: {
113
+ canReadAttachment: (
114
+ request: Parameters<IAttachmentAccessService["canReadAttachment"]>[0],
115
+ ) =>
116
+ Promise.resolve({
117
+ kind: "allowed",
118
+ documentId: request.documentId as never,
119
+ ref: request.attachmentRef as never,
120
+ }),
121
+ },
112
122
  authService: undefined,
113
123
  } as unknown as API);
114
124
 
115
- server = await adapter.listen(0);
125
+ server = await adapter.listen(0, undefined, "127.0.0.1");
116
126
  const addr = server.address();
117
127
  if (!addr || typeof addr === "string") throw new Error("no addr");
118
128
  port = addr.port;
@@ -149,7 +159,7 @@ describe("attachment service built from deriveAttachmentServiceConfig round-trip
149
159
  const result = await upload.send(stream);
150
160
  expect(result.header.sizeBytes).toBe(bytes.byteLength);
151
161
 
152
- const got = await service.get(result.ref);
162
+ const got = await service.get(result.ref, { documentId: "doc-1" });
153
163
  expect(got.header.mimeType).toBe("text/plain");
154
164
  const reader = got.body.getReader();
155
165
  const chunks: Uint8Array[] = [];
@@ -0,0 +1,105 @@
1
+ import type { AttachmentHash } from "@powerhousedao/reactor";
2
+ import { describe, expect, it } from "vitest";
3
+ import {
4
+ ATTACHMENT_URL_SIGNING_SECRET_ENV,
5
+ AttachmentUrlSigner,
6
+ resolveAttachmentUrlSigning,
7
+ } from "../../src/attachments/url-signer.js";
8
+
9
+ const HASH = "a".repeat(64) as AttachmentHash;
10
+ const OTHER_HASH = "b".repeat(64) as AttachmentHash;
11
+ const SECRET = "k".repeat(32);
12
+
13
+ function signed(now: number, ttl = 300) {
14
+ const signer = new AttachmentUrlSigner(SECRET, () => now);
15
+ const { query, expiresAtUtc } = signer.sign(HASH, "doc-1", ttl);
16
+ const params = new URLSearchParams(query);
17
+ return {
18
+ signer,
19
+ expiresAtUtc,
20
+ documentId: params.get("documentId")!,
21
+ expires: params.get("expires")!,
22
+ signature: params.get("signature")!,
23
+ };
24
+ }
25
+
26
+ describe("AttachmentUrlSigner", () => {
27
+ const NOW = Date.parse("2026-09-24T00:00:00.000Z");
28
+
29
+ it("verifies what it signed until the expiry", () => {
30
+ const { signer, expiresAtUtc, documentId, expires, signature } =
31
+ signed(NOW);
32
+
33
+ expect(documentId).toBe("doc-1");
34
+ expect(expiresAtUtc).toBe(new Date(NOW + 300_000).toISOString());
35
+ expect(signer.verify(HASH, documentId, expires, signature)).toBe(true);
36
+ });
37
+
38
+ it("refuses at and after the expiry", () => {
39
+ const { expires, signature } = signed(NOW);
40
+ const at = new AttachmentUrlSigner(SECRET, () => NOW + 300_000);
41
+
42
+ expect(at.verify(HASH, "doc-1", expires, signature)).toBe(false);
43
+ });
44
+
45
+ it("refuses a different hash, document, expiry or secret", () => {
46
+ const { signer, expires, signature } = signed(NOW);
47
+
48
+ expect(signer.verify(OTHER_HASH, "doc-1", expires, signature)).toBe(false);
49
+ expect(signer.verify(HASH, "doc-2", expires, signature)).toBe(false);
50
+ expect(
51
+ signer.verify(HASH, "doc-1", String(Number(expires) + 1), signature),
52
+ ).toBe(false);
53
+ expect(
54
+ new AttachmentUrlSigner("x".repeat(32), () => NOW).verify(
55
+ HASH,
56
+ "doc-1",
57
+ expires,
58
+ signature,
59
+ ),
60
+ ).toBe(false);
61
+ });
62
+
63
+ it("refuses malformed expiry and signature values", () => {
64
+ const { signer, expires, signature } = signed(NOW);
65
+
66
+ expect(signer.verify(HASH, "doc-1", `${expires}.0`, signature)).toBe(false);
67
+ expect(signer.verify(HASH, "doc-1", expires, `${signature}=`)).toBe(false);
68
+ expect(signer.verify(HASH, "doc-1", expires, "")).toBe(false);
69
+ });
70
+ });
71
+
72
+ describe("resolveAttachmentUrlSigning", () => {
73
+ it("uses a configured secret", () => {
74
+ const signing = resolveAttachmentUrlSigning({
75
+ [ATTACHMENT_URL_SIGNING_SECRET_ENV]: SECRET,
76
+ NODE_ENV: "production",
77
+ });
78
+ expect(signing.status).toBe("configured");
79
+ });
80
+
81
+ it("fails closed in production when unset", () => {
82
+ expect(resolveAttachmentUrlSigning({ NODE_ENV: "production" })).toEqual({
83
+ status: "unconfigured",
84
+ });
85
+ });
86
+
87
+ it.each([undefined, "development", "test"])(
88
+ "uses a per-process secret when unset and NODE_ENV is %s",
89
+ (nodeEnv) => {
90
+ const signing = resolveAttachmentUrlSigning({ NODE_ENV: nodeEnv });
91
+ expect(signing.status).toBe("ephemeral");
92
+ },
93
+ );
94
+
95
+ it.each(["short", ` ${"k".repeat(32)}`, ""])(
96
+ "refuses to boot on an unusable secret %j",
97
+ (secret) => {
98
+ expect(() =>
99
+ resolveAttachmentUrlSigning({
100
+ [ATTACHMENT_URL_SIGNING_SECRET_ENV]: secret,
101
+ }),
102
+ ).toThrow(ATTACHMENT_URL_SIGNING_SECRET_ENV);
103
+ },
104
+ );
105
+ });
@@ -97,7 +97,7 @@ export async function startWebhookHost(
97
97
  // constructing it would test a path no consumer can take.
98
98
  const { adapter } = await createHttpAdapter("express");
99
99
  adapter.setupMiddleware({});
100
- const server = await adapter.listen(0);
100
+ const server = await adapter.listen(0, undefined, "127.0.0.1");
101
101
  const { port } = server.address() as { port: number };
102
102
  const url = `http://127.0.0.1:${port}`;
103
103
 
@@ -0,0 +1,145 @@
1
+ import {
2
+ ReactorBuilder,
3
+ ReactorClientBuilder,
4
+ type InProcessReactorClientModule,
5
+ } from "@powerhousedao/reactor";
6
+ import { AuthorizationPolicy, type Context } from "@powerhousedao/reactor-api";
7
+ import {
8
+ initializeAuth,
9
+ withSignaturePolicy,
10
+ type DocumentModelModule,
11
+ } from "@powerhousedao/shared/document-model";
12
+ import type { ILogger } from "document-model";
13
+ import { documentModelDocumentModelModule } from "document-model";
14
+ import { afterEach, describe, expect, it, vi } from "vitest";
15
+ import { composeWorkflowRuntime } from "../src/workflow-runtime.mjs";
16
+
17
+ const READER = "0xreader";
18
+ const OUTSIDER = "0xoutsider";
19
+
20
+ // The host's legacy layer under OPEN admits everyone, anonymous included.
21
+ const openAuthorization = {
22
+ config: {
23
+ admins: [],
24
+ defaultProtection: false,
25
+ policy: AuthorizationPolicy.OPEN,
26
+ },
27
+ isSupremeAdmin: () => true,
28
+ canRead: () => Promise.resolve(true),
29
+ canWrite: () => Promise.resolve(true),
30
+ };
31
+
32
+ const logger = {
33
+ verbose: vi.fn(),
34
+ debug: vi.fn(),
35
+ info: vi.fn(),
36
+ warn: vi.fn(),
37
+ error: vi.fn(),
38
+ child: () => logger,
39
+ } as unknown as ILogger;
40
+
41
+ const contextFor = (address?: string) =>
42
+ ({ headers: {}, db: {}, user: address ? { address } : undefined }) as Context;
43
+
44
+ describe("the workflow runtime's read check", () => {
45
+ let module: InProcessReactorClientModule | undefined;
46
+
47
+ afterEach(() => {
48
+ module?.reactor.kill();
49
+ module = undefined;
50
+ });
51
+
52
+ async function hostDeps() {
53
+ module = await new ReactorClientBuilder()
54
+ .withReactorBuilder(
55
+ new ReactorBuilder()
56
+ .withDocumentModelSources([
57
+ documentModelDocumentModelModule as unknown as DocumentModelModule,
58
+ ])
59
+ .withExecutorConfig({
60
+ featureFlags: { documentDecisions: true, authEnforcement: true },
61
+ }),
62
+ )
63
+ .buildModule();
64
+ const client = module.client;
65
+ const id = "wf-policed";
66
+ await client.create(
67
+ withSignaturePolicy(
68
+ documentModelDocumentModelModule.utils.createDocument(),
69
+ "legacy",
70
+ { id },
71
+ ),
72
+ );
73
+ await client.execute(id, "main", [
74
+ initializeAuth({
75
+ version: 1,
76
+ grants: [
77
+ {
78
+ id: "g-read",
79
+ description: "the reader reads the domain",
80
+ effect: "allow",
81
+ principal: { address: READER },
82
+ capability: { can: "read", scope: "global" },
83
+ },
84
+ {
85
+ id: "g-admin",
86
+ description: "administration stays reachable",
87
+ effect: "allow",
88
+ principal: { anyone: true },
89
+ capability: { can: "execute", scope: "auth" },
90
+ },
91
+ ],
92
+ }),
93
+ ]);
94
+
95
+ const createWorkflowRuntime = vi.fn((_deps: Record<string, unknown>) => ({
96
+ shutdown: vi.fn(),
97
+ }));
98
+ await composeWorkflowRuntime({
99
+ reactorClient: client,
100
+ relationalDb: {} as never,
101
+ attachments: {} as never,
102
+ authorizationService: openAuthorization as never,
103
+ logger,
104
+ load: () =>
105
+ Promise.resolve({
106
+ WORKFLOW_PACKAGE_NAME: "@powerhousedao/workflow",
107
+ setPieceRegistryUrl: vi.fn(),
108
+ createWorkflowRuntime,
109
+ } as never),
110
+ });
111
+ const [deps] = createWorkflowRuntime.mock.calls[0]!;
112
+ return {
113
+ id,
114
+ deps: deps as {
115
+ assertCanRead: (id: string, caller: object) => Promise<unknown>;
116
+ subjectOf: (caller: object) => unknown;
117
+ },
118
+ };
119
+ }
120
+
121
+ it("refuses under OPEN a document the caller is not served", async () => {
122
+ const { id, deps } = await hostDeps();
123
+
124
+ await expect(deps.assertCanRead(id, contextFor())).rejects.toThrow();
125
+ await expect(
126
+ deps.assertCanRead(id, contextFor(OUTSIDER)),
127
+ ).rejects.toThrow();
128
+ await expect(
129
+ deps.assertCanRead(id, contextFor(READER)),
130
+ ).resolves.toBeUndefined();
131
+ });
132
+
133
+ it("reads a caller as its own subject, anonymous as the empty one", async () => {
134
+ const { deps } = await hostDeps();
135
+
136
+ expect(deps.subjectOf(contextFor(READER))).toEqual({
137
+ address: READER,
138
+ key: undefined,
139
+ });
140
+ expect(deps.subjectOf(contextFor())).toEqual({
141
+ address: undefined,
142
+ key: undefined,
143
+ });
144
+ });
145
+ });
package/tsdown.config.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { defineConfig } from "tsdown";
2
+ import { dtsExportList } from "../../tsdown.dts.mjs";
2
3
 
3
4
  export default defineConfig({
4
5
  entry: [
@@ -11,6 +12,7 @@ export default defineConfig({
11
12
  platform: "node",
12
13
  outDir: "dist",
13
14
  clean: true,
14
- dts: true,
15
+ dts: { generator: "tsgo" },
16
+ plugins: [dtsExportList()],
15
17
  sourcemap: true,
16
18
  });