@vercel/board 0.0.0-stage → 0.15.0

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 (126) hide show
  1. package/README.md +179 -2
  2. package/dist/a2-5EG8FkZ8.d.mts +3359 -0
  3. package/dist/a2-DYGYankn.mjs +3045 -0
  4. package/dist/access-CJU2-vaa.mjs +226 -0
  5. package/dist/access-CV96SYgr.d.mts +32 -0
  6. package/dist/access.d.mts +29 -0
  7. package/dist/access.mjs +4 -0
  8. package/dist/actors-rdN4dDTU.mjs +223 -0
  9. package/dist/attempts-BQUp9MKO.mjs +919 -0
  10. package/dist/auth-COzc09sn.mjs +276 -0
  11. package/dist/auth-wHFcAte3.d.mts +203 -0
  12. package/dist/backend-a2/blob-vercel.d.mts +8 -0
  13. package/dist/backend-a2/blob-vercel.mjs +81 -0
  14. package/dist/backend-a2/contract.d.mts +18 -0
  15. package/dist/backend-a2/contract.mjs +5 -0
  16. package/dist/backend-a2/scheduler-local.d.mts +11 -0
  17. package/dist/backend-a2/scheduler-local.mjs +2 -0
  18. package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
  19. package/dist/backend-a2/scheduler-qstash.mjs +2 -0
  20. package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
  21. package/dist/backend-a2/scheduler-vercel.mjs +2 -0
  22. package/dist/backend-a2/store-memory.d.mts +11 -0
  23. package/dist/backend-a2/store-memory.mjs +19 -0
  24. package/dist/backend-a2/store-postgres.d.mts +38 -0
  25. package/dist/backend-a2/store-postgres.mjs +110 -0
  26. package/dist/backend-a2/store-redis-http.d.mts +17 -0
  27. package/dist/backend-a2/store-redis-http.mjs +32 -0
  28. package/dist/backend-a2/store-redis.d.mts +23 -0
  29. package/dist/backend-a2/store-redis.mjs +39 -0
  30. package/dist/backend-a2/store-sqlite.d.mts +12 -0
  31. package/dist/backend-a2/store-sqlite.mjs +31 -0
  32. package/dist/backend-a2.d.mts +4 -0
  33. package/dist/backend-a2.mjs +2 -0
  34. package/dist/backend-http-8ex5g7Wv.d.mts +630 -0
  35. package/dist/backend-http-BsDSgmeo.mjs +613 -0
  36. package/dist/backend-http-Cv6xbEy9.mjs +469 -0
  37. package/dist/backend-http.d.mts +10 -0
  38. package/dist/backend-http.mjs +2 -0
  39. package/dist/backend-http.node.d.mts +10 -0
  40. package/dist/backend-http.node.mjs +2 -0
  41. package/dist/blob-C0J3wbGv.d.mts +39 -0
  42. package/dist/bots-CGkO76qH.mjs +67 -0
  43. package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
  44. package/dist/chat-provider-jFSeyNmB.mjs +389 -0
  45. package/dist/cli.d.mts +1 -0
  46. package/dist/cli.mjs +2252 -0
  47. package/dist/client-DGDPaN7U.mjs +380 -0
  48. package/dist/conformance-DRR23YNy.mjs +512 -0
  49. package/dist/conformance.d.mts +30 -0
  50. package/dist/conformance.mjs +2 -0
  51. package/dist/contract-CuJmlrhw.mjs +566 -0
  52. package/dist/emoji-CTLpvH-A.mjs +445 -0
  53. package/dist/engine-3EvQ8idk.mjs +1449 -0
  54. package/dist/errors-DX3hpwWt.mjs +64 -0
  55. package/dist/exports-BpgyEdh6.d.mts +4 -0
  56. package/dist/exports-CyY3_FjZ.mjs +1285 -0
  57. package/dist/files-SniSmeEr.mjs +47 -0
  58. package/dist/index.d.mts +13 -0
  59. package/dist/index.default-BZ3lItjH.mjs +12 -0
  60. package/dist/index.mjs +19 -0
  61. package/dist/index.node.d.mts +14 -0
  62. package/dist/index.node.mjs +5 -0
  63. package/dist/inspect/app.js +416 -0
  64. package/dist/inspect/engine.js +5 -0
  65. package/dist/local-Cr_dSjvs.mjs +44 -0
  66. package/dist/own-OwgPWdmh.mjs +15 -0
  67. package/dist/project-CFiArraX.mjs +124 -0
  68. package/dist/provider-BGjz-mDa.d.mts +2473 -0
  69. package/dist/provider-CuewcDNZ.mjs +205 -0
  70. package/dist/redis-tables-B35vv4CZ.mjs +842 -0
  71. package/dist/reducers-B_vUGQRp.d.mts +1172 -0
  72. package/dist/reducers-mUPFuFJV.mjs +293 -0
  73. package/dist/schemas-Be08nyfW.mjs +74 -0
  74. package/dist/schemas-CWxs3ECJ.d.mts +50 -0
  75. package/dist/serve-Ccbb2LSb.mjs +170 -0
  76. package/dist/server-Cxey38B-.mjs +17 -0
  77. package/dist/serving-BNbzc96L.mjs +2 -0
  78. package/dist/sql-DbiEnqM0.mjs +571 -0
  79. package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
  80. package/dist/sync-github.d.mts +473 -0
  81. package/dist/sync-github.mjs +603 -0
  82. package/dist/sync-kapso.d.mts +175 -0
  83. package/dist/sync-kapso.mjs +86 -0
  84. package/dist/sync-slack.d.mts +364 -0
  85. package/dist/sync-slack.mjs +728 -0
  86. package/dist/sync.chat.d.mts +2 -0
  87. package/dist/sync.chat.mjs +2 -0
  88. package/dist/sync.d.mts +3 -0
  89. package/dist/sync.mjs +4 -0
  90. package/dist/targets-ojFEXEyN.mjs +94 -0
  91. package/dist/tokens-DKLn5P9B.mjs +256 -0
  92. package/dist/types-BHCjTZdB.d.mts +104 -0
  93. package/dist/users-Dls7_ANV.mjs +42 -0
  94. package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
  95. package/dist/watchers-hash-D9hbHLKB.mjs +225 -0
  96. package/docs/01-board/01-mounting.md +506 -0
  97. package/docs/01-board/02-database.md +258 -0
  98. package/docs/01-board/03-identity.md +262 -0
  99. package/docs/01-board/04-messages.md +192 -0
  100. package/docs/01-board/05-reading.md +177 -0
  101. package/docs/01-board/06-watchers.md +249 -0
  102. package/docs/01-board/07-schedules.md +133 -0
  103. package/docs/01-board/08-middleware.md +139 -0
  104. package/docs/01-board/09-access.md +499 -0
  105. package/docs/01-quickstart.md +196 -0
  106. package/docs/02-sync/01-overview.md +290 -0
  107. package/docs/02-sync/02-callbacks.md +265 -0
  108. package/docs/02-sync/03-slack.md +248 -0
  109. package/docs/02-sync/04-github.md +201 -0
  110. package/docs/02-sync/05-whatsapp.md +148 -0
  111. package/docs/02-sync/06-providers.md +228 -0
  112. package/docs/03-guides/01-coding-agents.md +226 -0
  113. package/docs/03-guides/02-inspector.md +104 -0
  114. package/docs/03-guides/03-ci.md +251 -0
  115. package/docs/03-guides/04-triage.md +233 -0
  116. package/docs/03-guides/05-agent-factory.md +142 -0
  117. package/docs/04-reference/01-cli.md +606 -0
  118. package/docs/04-reference/02-board.md +579 -0
  119. package/docs/04-reference/03-http.md +489 -0
  120. package/docs/04-reference/04-log.md +250 -0
  121. package/docs/04-reference/05-conformance.md +97 -0
  122. package/docs/index.md +186 -0
  123. package/package.json +165 -3
  124. package/sql/postgres/board.sql +145 -0
  125. package/sql/postgres/notifications.sql +24 -0
  126. package/sql/postgres/schema.sql +451 -0
@@ -0,0 +1,1285 @@
1
+ import { t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
+ import "./schemas-Be08nyfW.mjs";
3
+ import { A as SyncBody, B as UploadStartBody, C as Ref, D as SignInQuery, E as SchedulesQuery, F as SyncReceiptBody, I as SyncRootsQuery, L as SyncUnlinkBody, M as SyncCopiesQuery, N as SyncFindQuery, O as SourceReadQuery, P as SyncImportBody, R as ThreadsQuery, S as ReactBody, T as ScheduleBody, _ as InboxQuery, b as MessagesQuery, d as EditBody, f as EventsQuery, g as InboxPushBody, h as InboxNextQuery, j as SyncCodeBody, k as SyncAccountBody, m as InboxAckBody, o as derivedId, p as IdentityUnlinkBody, u as AckBody, v as LabelBody, w as RegisterBody, x as PostBody, y as MatchesQuery, z as TypingBody } from "./watch-spec-W0c7ypHJ.mjs";
4
+ import { n as createBoardClient, t as BoardError } from "./client-DGDPaN7U.mjs";
5
+ import { a as queryObject, c as VERSION, i as openapi, l as SYNC, n as METHODS, o as readCapped, r as createRouter, s as readJson } from "./watchers-hash-D9hbHLKB.mjs";
6
+ import { a as forbidden, c as toErrorResponse, l as unauthorized, n as RejectedError, o as json, r as badRequest, s as notFound, t as HttpError } from "./errors-DX3hpwWt.mjs";
7
+ import { i as resolveActor, r as claimsOf, t as KNOWN_AGENTS } from "./actors-rdN4dDTU.mjs";
8
+ import { d as issuersOf, g as verifiesTokens, l as hostPrincipal, o as createAuthority, s as fromTokens } from "./auth-COzc09sn.mjs";
9
+ import { i as DroppedError, n as syncTarget, o as runMiddleware, r as withEmailMentions, t as mentionsEmails } from "./targets-ojFEXEyN.mjs";
10
+ //#region src/server/policy.ts
11
+ /**
12
+ * The policy a board's options say: verifying requests, and `access` over them and over the
13
+ * principals its backend stores. `keeps`: the backend keeps the board, so the defaults let its
14
+ * people and own deployments in when `access` is left out.
15
+ */
16
+ function createPolicy(options, keeps, serving) {
17
+ /** Whether requests are served at all: a board on `http()` serves none without an `access` of its own. */
18
+ const open = keeps || options.access !== void 0;
19
+ const access = options.access ?? [];
20
+ /** The board's own Vercel project and team, from `project` or the environment, asked once. */
21
+ let own = null;
22
+ const project = () => (own ??= (async () => {
23
+ if (!serving) return {
24
+ id: options.project?.id ?? null,
25
+ teamId: options.project?.teamId ?? null
26
+ };
27
+ const found = await serving();
28
+ const [id, teamId] = await Promise.all([found.ownProjectId(options.project), found.ownTeamId(options.project)]);
29
+ return {
30
+ id,
31
+ teamId
32
+ };
33
+ })()).catch((error) => {
34
+ own = null;
35
+ throw error;
36
+ });
37
+ /** What discovery says the board's project is, as a request shows it: clients mint tokens with it. */
38
+ async function discoveryIdentity(headers) {
39
+ if (!serving) return {
40
+ projectId: options.project?.id ?? null,
41
+ teamId: options.project?.teamId ?? null
42
+ };
43
+ return (await serving()).discoveryIdentity(headers, options.project);
44
+ }
45
+ let tokens = null;
46
+ /** The verifier: `authenticate`, or `oidcTokens()` where this build has it. */
47
+ const verifier = () => options.authenticate ? Promise.resolve(options.authenticate) : tokens ??= serving ? serving().then((found) => found.oidcTokens()) : Promise.reject(new HttpError(501, "no_verifier", "this build of board verifies no tokens: pass board({ authenticate })"));
48
+ const authority = createAuthority({
49
+ access,
50
+ project,
51
+ hosts: options.authenticate !== void 0 && !verifiesTokens(options.authenticate)
52
+ });
53
+ const context = {
54
+ project,
55
+ issuers: typeof access === "function" ? [] : issuersOf(access)
56
+ };
57
+ /** Verified principals by credential, until they expire, so a stream's polls don't verify from scratch. */
58
+ const verified = /* @__PURE__ */ new Map();
59
+ /** And by request, for one with no credential (a session cookie), for as long as it's served. */
60
+ const perRequest = /* @__PURE__ */ new WeakMap();
61
+ /** Who a request's credential is: a 401 or the host's own rejection when it's no one. */
62
+ async function principalOf(request) {
63
+ const live = (principal) => principal.expiresAt === null || principal.expiresAt.getTime() > Date.now();
64
+ const known = perRequest.get(request);
65
+ if (known) {
66
+ const principal = await known;
67
+ if (live(principal)) return principal;
68
+ }
69
+ const key = request.credential === null ? null : [
70
+ request.audience,
71
+ request.credential,
72
+ request.headers.get("x-vercel-oidc-token") ?? ""
73
+ ].join("");
74
+ const cached = key === null ? void 0 : verified.get(key);
75
+ if (cached && cached.until > Date.now()) {
76
+ perRequest.set(request, cached.principal);
77
+ return cached.principal;
78
+ }
79
+ const principal = verify(request);
80
+ perRequest.set(request, principal);
81
+ if (key !== null) principal.then((found) => {
82
+ if (found.facts?.issuer === "host" || found.expiresAt === null) return;
83
+ if (verified.size >= 1e3) verified.delete(verified.keys().next().value);
84
+ verified.set(key, {
85
+ principal,
86
+ until: found.expiresAt.getTime()
87
+ });
88
+ }, () => perRequest.delete(request));
89
+ return principal;
90
+ }
91
+ async function verify(request) {
92
+ const authenticate = await verifier();
93
+ let result;
94
+ try {
95
+ result = await authenticate({
96
+ ...request,
97
+ board: context
98
+ });
99
+ } catch (error) {
100
+ if (error instanceof Response) throw new RejectedError(error);
101
+ throw error;
102
+ }
103
+ if (result instanceof Response) throw new RejectedError(result);
104
+ if (result === null || result === void 0) throw unauthorized("missing bearer token");
105
+ return fromTokens(result) ? result : hostPrincipal(result);
106
+ }
107
+ return {
108
+ open,
109
+ project,
110
+ discoveryIdentity,
111
+ /**
112
+ * Who a request is, and what it may do now: verified, under the rules, with the profile it
113
+ * claims. Each call works its access out again; its principal is verified once per credential
114
+ * until it expires.
115
+ */
116
+ async identify(request) {
117
+ const found = await principalOf(request);
118
+ const principal = request.claims.scope === null ? found : {
119
+ ...found,
120
+ scope: request.claims.scope
121
+ };
122
+ const { author, actor } = resolveActor(principal, request.claims, request.audience);
123
+ return {
124
+ principal: {
125
+ ...principal,
126
+ access: await authority.grant(principal)
127
+ },
128
+ author,
129
+ actor
130
+ };
131
+ },
132
+ /** What a stored principal (a schedule's creator, a watcher's registrant) may do now: null once nothing. */
133
+ accessNow: (principal) => authority.accessNow(principal)
134
+ };
135
+ }
136
+ //#endregion
137
+ //#region src/contract/logos.ts
138
+ const LOGOS = {
139
+ claude: "m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z",
140
+ openai: "M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z",
141
+ x: "M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z",
142
+ slack: "M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z",
143
+ github: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12",
144
+ whatsapp: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z"
145
+ };
146
+ //#endregion
147
+ //#region src/server/avatars.ts
148
+ /** Each known agent's mark on its color. */
149
+ const STYLES = {
150
+ claude: {
151
+ background: "#D97757",
152
+ mark: { path: LOGOS.claude }
153
+ },
154
+ codex: {
155
+ background: "#000000",
156
+ mark: { path: LOGOS.openai }
157
+ },
158
+ grok: {
159
+ background: "#000000",
160
+ mark: { path: LOGOS.x }
161
+ },
162
+ pi: {
163
+ background: "#1f1f1f",
164
+ mark: { glyph: "π" }
165
+ }
166
+ };
167
+ /** A built-in avatar for a detected agent; other names get their initial. */
168
+ function avatarSvg(name) {
169
+ const style = STYLES[name] ?? {
170
+ background: "#666666",
171
+ mark: { glyph: name.slice(0, 1).toUpperCase() }
172
+ };
173
+ const mark = "path" in style.mark ? `<path transform="translate(30 30) scale(2.8333)" fill="#ffffff" d="${style.mark.path}"/>` : `<text x="64" y="86" font-family="ui-sans-serif,system-ui,sans-serif" font-size="66" font-weight="600" text-anchor="middle" fill="#ffffff">${style.mark.glyph}</text>`;
174
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><rect width="128" height="128" rx="28" fill="${style.background}"/>${mark}</svg>`;
175
+ }
176
+ //#endregion
177
+ //#region src/server/serve.ts
178
+ /** The methods whose answer is something new. */
179
+ const CREATED = /* @__PURE__ */ new Set([
180
+ "post",
181
+ "schedule",
182
+ "upload",
183
+ "uploadFinish"
184
+ ]);
185
+ /** Shown inline only when a browser can't run it: everything else downloads. */
186
+ const INLINE = /^(image\/(png|jpeg|gif|webp|avif)|video\/(mp4|webm)|text\/plain)$/;
187
+ /**
188
+ * The file, sandboxed: it can't run as a page on the board's origin, and only
189
+ * types a browser can't execute display inline.
190
+ */
191
+ function attachmentResponse({ attachment, data }) {
192
+ const disposition = INLINE.test(attachment.type) ? "inline" : "attachment";
193
+ const body = data instanceof Uint8Array ? new Blob([data]) : data;
194
+ return new Response(body, { headers: {
195
+ "content-type": attachment.type,
196
+ "content-length": String(attachment.size),
197
+ "content-disposition": `${disposition}; filename*=UTF-8''${encodeURIComponent(attachment.name)}`,
198
+ "content-security-policy": "sandbox; default-src 'none'",
199
+ "x-content-type-options": "nosniff",
200
+ "cache-control": "private, max-age=31536000, immutable"
201
+ } });
202
+ }
203
+ /** A method's input, from the request: its path's `:id` or `:key`, its query, or its body, validated. */
204
+ async function inputOf(name, request, url, params) {
205
+ const id = params.id ?? "";
206
+ const key = params.key ?? "";
207
+ const body = () => readJson(request);
208
+ switch (name) {
209
+ case "discovery":
210
+ case "me":
211
+ case "channels":
212
+ case "actors":
213
+ case "identity":
214
+ case "watchers": return {};
215
+ case "message":
216
+ case "posted":
217
+ case "delete":
218
+ case "cancelSchedule":
219
+ case "attachment":
220
+ case "uploadFinish": return { id };
221
+ case "messages": return MessagesQuery.parse(queryObject(url));
222
+ case "threads": return ThreadsQuery.parse(queryObject(url));
223
+ case "schedules": return SchedulesQuery.parse(queryObject(url));
224
+ case "matches": return MatchesQuery.parse(queryObject(url));
225
+ case "events": {
226
+ const query = EventsQuery.parse(queryObject(url));
227
+ const last = request.headers.get("last-event-id");
228
+ if (query.after === void 0 && last && Ref.safeParse(last).success) query.after = last;
229
+ return query;
230
+ }
231
+ case "post": return PostBody.parse(await body());
232
+ case "schedule": return ScheduleBody.parse(await body());
233
+ case "register": return RegisterBody.parse(await body());
234
+ case "edit": return {
235
+ id,
236
+ ...EditBody.parse(await body())
237
+ };
238
+ case "react": return {
239
+ id,
240
+ ...ReactBody.parse(await body())
241
+ };
242
+ case "label": return {
243
+ id,
244
+ ...LabelBody.parse(await body())
245
+ };
246
+ case "ack": return {
247
+ id,
248
+ ...AckBody.parse(await body())
249
+ };
250
+ case "upload": {
251
+ const header = request.headers.get("x-board-filename");
252
+ let name = null;
253
+ try {
254
+ name = header === null ? null : decodeURIComponent(header);
255
+ } catch {
256
+ throw badRequest("`x-board-filename` is not URI-encoded");
257
+ }
258
+ return {
259
+ data: await readCapped(request, ATTACHMENT_BYTES_MAX),
260
+ name,
261
+ type: request.headers.get("content-type")
262
+ };
263
+ }
264
+ case "uploadStart": return UploadStartBody.parse(await body());
265
+ case "syncImport": return SyncImportBody.parse(await body());
266
+ case "syncReceipt": return SyncReceiptBody.parse(await body());
267
+ case "syncThread": return { thread: params.thread ?? "" };
268
+ case "syncFind": return SyncFindQuery.parse(queryObject(url));
269
+ case "syncCopies": return SyncCopiesQuery.parse(queryObject(url));
270
+ case "syncRoots": return SyncRootsQuery.parse(queryObject(url));
271
+ case "inboxPush": return {
272
+ key,
273
+ ...InboxPushBody.parse(await body())
274
+ };
275
+ case "inboxNext": return {
276
+ key,
277
+ ...InboxNextQuery.parse(queryObject(url))
278
+ };
279
+ case "inboxAck": return {
280
+ key,
281
+ id,
282
+ ...InboxAckBody.parse(await body())
283
+ };
284
+ case "inbox": return {
285
+ key,
286
+ ...InboxQuery.parse(queryObject(url))
287
+ };
288
+ case "syncCode": return SyncCodeBody.parse(await body());
289
+ case "syncAccount": return SyncAccountBody.parse(await body());
290
+ case "syncUnlink": return SyncUnlinkBody.parse(await body());
291
+ case "typing": return {
292
+ id,
293
+ ...TypingBody.parse(await body()) ?? {}
294
+ };
295
+ case "readSource": return {
296
+ key,
297
+ ...SourceReadQuery.parse(queryObject(url))
298
+ };
299
+ }
300
+ }
301
+ /** A platform user, as the middleware is told who's writing. */
302
+ const externalActor = (author) => ({
303
+ id: author.id,
304
+ kind: "external",
305
+ handle: author.handle,
306
+ name: author.name,
307
+ avatar: author.avatar
308
+ });
309
+ /** A write's caller, as the middleware is told: where it came from. The middleware's own handle and sync's lookups skip it. */
310
+ function sourceOf(caller) {
311
+ const via = caller.origin.via;
312
+ if (via?.kind === "middleware" || via?.kind === "engine") return null;
313
+ return via ?? { kind: "api" };
314
+ }
315
+ /** The middleware's own handle: its writes skip the middleware. */
316
+ const MIDDLEWARE = {
317
+ self: "middleware",
318
+ origin: { via: { kind: "middleware" } }
319
+ };
320
+ /** board()'s own code, for what it looks up itself. */
321
+ const OWN = {
322
+ self: null,
323
+ origin: { via: {
324
+ kind: "client",
325
+ name: null
326
+ } }
327
+ };
328
+ /**
329
+ * The board, serving the protocol over HTTP on its backend: it routes and validates each request,
330
+ * verifies its caller, applies `access`, runs the middleware, and calls the backend's method of the
331
+ * same name with the caller it verified. Sync runs here too, for requests and for its own code.
332
+ */
333
+ async function createServe(context) {
334
+ const { options, backend, runtime, policy } = context;
335
+ const entries = options.sync ?? {};
336
+ const engine = Object.keys(entries).length > 0 ? (await runtime.sync()).createSyncEngine(entries) : null;
337
+ const streamMaxMs = options.streamMaxMs ?? 28e4;
338
+ const route = createRouter(backend.routes, options.basePath);
339
+ const { state, ownUrl, clientAs, handleOf } = context;
340
+ const watch = options.watch ?? {};
341
+ const middleware = options.middleware ?? [];
342
+ /** What a request says about who it is, none of it verified yet: its bearer token, its claims, and the board's URL. */
343
+ function requestOf(request, audience) {
344
+ return {
345
+ credential: (request.headers.get("authorization") ?? "").match(/^Bearer\s+(.+)$/i)?.[1] ?? null,
346
+ claims: claimsOf(request.headers),
347
+ audience,
348
+ headers: request.headers
349
+ };
350
+ }
351
+ /** A request's caller, verified: who it is, and what `access` lets it do now. */
352
+ async function callerOf(request) {
353
+ return {
354
+ identity: await policy.identify(request),
355
+ origin: { via: { kind: "api" } }
356
+ };
357
+ }
358
+ /** What a board that serves no requests answers them: a board on `http()` with no `access` of its own. */
359
+ const servesOnly = () => new HttpError(404, "not_found", `${ownUrl() ?? "this board"} answers only wake-ups and webhooks`);
360
+ /**
361
+ * `@[ada@vercel.com]`: an email a synced platform knows becomes that user's mention, and the
362
+ * board learns who they are; the rest stay plain text.
363
+ */
364
+ async function withEmails(text) {
365
+ return (await withEmailMentions(text, engine ? async (email) => {
366
+ const actor = await engine.findByEmail(email);
367
+ if (actor) await backend.syncImport(engine.caller, {
368
+ op: "seen",
369
+ author: {
370
+ ...actor,
371
+ isBot: false
372
+ }
373
+ }, new AbortController().signal);
374
+ return actor;
375
+ } : void 0)).text;
376
+ }
377
+ /**
378
+ * A sync target as the board that syncs it resolves it. One for an entry this board doesn't
379
+ * have goes on as it is, for the sync that does, unless this board syncs: then it's a mistake.
380
+ */
381
+ async function resolveTarget(target) {
382
+ if (typeof target !== "string") return target;
383
+ const at = target.indexOf(":");
384
+ if (at <= 0) throw badRequest(`sync targets look like <key>:<channel or thread>, got: ${target}`);
385
+ const key = target.slice(0, at);
386
+ if (engine && entries[key]) return engine.normalizeTarget(target);
387
+ if (engine) throw badRequest(`no sync entry named ${key}`);
388
+ return syncTarget(target);
389
+ }
390
+ /**
391
+ * What a verified caller may do, as board()'s `access` said when it verified them: board() checks
392
+ * it before it does anything for them past calling the backend (a platform call, an email lookup,
393
+ * sync's own write, reading an upload, the middleware), so a caller it won't let in starts none of
394
+ * it. Null for this deployment's own code, which may do everything.
395
+ */
396
+ const accessOf = (caller) => "identity" in caller ? caller.identity.principal.access : null;
397
+ /**
398
+ * Whether board() may work for a write of `caller`'s that lands in `channel`, or in the thread of
399
+ * message `re`, which it must read: `post` there. One it may not goes to the backend as it came,
400
+ * for the backend to say why.
401
+ */
402
+ async function mayPost(caller, where, signal) {
403
+ const access = accessOf(caller);
404
+ if (access === null) return {
405
+ access: null,
406
+ channel: where.channel ?? ""
407
+ };
408
+ const channel = where.channel ?? (where.re === void 0 ? void 0 : (await lookup(caller, where.re, signal))?.channel);
409
+ if (channel === void 0) return null;
410
+ return access.can("post", channel) ? {
411
+ access,
412
+ channel
413
+ } : null;
414
+ }
415
+ /** Who this deployment's own code is on its backend, asked once each: the middleware is told who's writing. */
416
+ const selves = /* @__PURE__ */ new Map();
417
+ async function whoIs(caller, signal) {
418
+ if ("identity" in caller) return {
419
+ principal: caller.identity.principal.id,
420
+ actor: caller.identity.actor
421
+ };
422
+ const key = JSON.stringify([
423
+ caller.origin.via?.kind ?? "",
424
+ caller.self,
425
+ caller.profile ?? null
426
+ ]);
427
+ let found = selves.get(key);
428
+ if (!found) {
429
+ selves.set(key, found = backend.me(caller, {}, signal).then((me) => ({
430
+ principal: me.principal,
431
+ actor: {
432
+ id: me.actor,
433
+ kind: me.kind,
434
+ handle: me.handle,
435
+ name: me.name,
436
+ avatar: me.avatar
437
+ }
438
+ })));
439
+ found.catch(() => selves.delete(key));
440
+ }
441
+ return found;
442
+ }
443
+ let middlewareBoard = null;
444
+ /** Runs the middleware on a write, then `commit`s what it passed on: a `DroppedError` when it drops it. */
445
+ const screen = (write, author, source, commit) => runMiddleware(middleware, {
446
+ write,
447
+ author,
448
+ source,
449
+ board: () => middlewareBoard ??= handleOf(MIDDLEWARE, null)
450
+ }, commit);
451
+ /** Whether a lookup found what it looked for: a 404 says it didn't. */
452
+ async function found(lookup) {
453
+ try {
454
+ await lookup();
455
+ return true;
456
+ } catch (error) {
457
+ if (error instanceof HttpError && error.status === 404) return false;
458
+ throw error;
459
+ }
460
+ }
461
+ /** Files a write carries, as the board stored them: each one `caller` may have, or the backend's 404. */
462
+ async function attachmentsOf(caller, ids, signal) {
463
+ const attachments = [];
464
+ for (const id of new Set(ids ?? [])) {
465
+ const { attachment, data } = await backend.attachment(caller, { id }, signal);
466
+ if (data instanceof ReadableStream) await data.cancel().catch(() => {});
467
+ attachments.push(attachment);
468
+ }
469
+ return attachments;
470
+ }
471
+ /**
472
+ * A post through the middleware, then `send`: once board() knows it would land (a reply's root is
473
+ * there, and the caller may post, label, and sync where it does), with the files it may have. A
474
+ * retry of one that landed stands, whatever the middleware would say now; one that wouldn't land
475
+ * goes as it came, for the backend to say why.
476
+ */
477
+ async function screenPost(caller, input, signal, send) {
478
+ const source = sourceOf(caller);
479
+ if (middleware.length === 0 || source === null) return send(input);
480
+ if (input.id !== void 0 && await found(() => backend.posted(caller, { id: input.id }, signal))) return send(input);
481
+ let channel = input.channel;
482
+ let re = null;
483
+ if (input.re !== void 0) {
484
+ const root = await lookup(caller, input.re, signal);
485
+ if (!root) return send(input);
486
+ channel = root.channel;
487
+ re = root.re ?? root.id;
488
+ }
489
+ const access = accessOf(caller);
490
+ const labels = Object.keys(input.labels ?? {}).length > 0;
491
+ const syncs = input.re === void 0 && (input.sync?.length ?? 0) > 0;
492
+ if (channel === void 0 || access && (!access.can("post", channel) || labels && !access.can("label", channel) || syncs && !access.can("sync", channel))) return send(input);
493
+ const attachments = await attachmentsOf(caller, input.attachments, signal);
494
+ const { actor } = await whoIs(caller, signal);
495
+ return screen({
496
+ kind: "post",
497
+ channel,
498
+ re,
499
+ text: input.text,
500
+ labels: input.labels ?? {},
501
+ attachments
502
+ }, actor, source, (write) => send({
503
+ ...input,
504
+ text: write.text,
505
+ labels: write.labels
506
+ }));
507
+ }
508
+ /**
509
+ * The caller's own message, still there, where it may post: what an edit of it changes. One it
510
+ * can't read may still be its own, as board()'s own code finds it, only if it was written there
511
+ * without reading too: one its principal wrote where it reads (a repo's workflows share one) is
512
+ * as missing to it as one that isn't there.
513
+ */
514
+ async function ownMessage(caller, id, signal) {
515
+ const access = accessOf(caller);
516
+ const readable = await lookup(caller, id, signal);
517
+ const message = readable ?? (access !== null ? await lookup(OWN, id, signal) : null);
518
+ if (!message || message.deleted || access && !access.can("post", message.channel)) return null;
519
+ if (!readable && !message.blind) return null;
520
+ return message.by.principal === (await whoIs(caller, signal)).principal ? message : null;
521
+ }
522
+ /** An edit through the middleware, then `send`: only the author's own, of a message that's still there, reaches it. */
523
+ async function screenEdit(caller, input, signal, send) {
524
+ const source = sourceOf(caller);
525
+ if (middleware.length === 0 || source === null) return send(input);
526
+ const message = await ownMessage(caller, input.id, signal);
527
+ if (!message) return send(input);
528
+ const { actor } = await whoIs(caller, signal);
529
+ return screen({
530
+ kind: "edit",
531
+ id: message.id,
532
+ channel: message.channel,
533
+ re: message.re,
534
+ text: input.text
535
+ }, actor, source, (write) => send({
536
+ ...input,
537
+ text: write.text
538
+ }));
539
+ }
540
+ /**
541
+ * Where an import came from, as the middleware is told: the platform event sync says it's from,
542
+ * or what its entry and origin say. Null when it names an entry its caller doesn't sync, which
543
+ * the backend refuses.
544
+ */
545
+ async function importSource(caller, body, found, signal) {
546
+ const key = body.source?.key ?? body.from;
547
+ if (body.source) {
548
+ if (!("identity" in caller ? (await backend.watchers(caller, {}, signal)).watchers.some((registration) => registration.owner === caller.identity.principal.id && registration.sync.some((entry) => entry.key === key)) : entries[body.source.key] !== void 0)) return null;
549
+ }
550
+ const origin = (body.op === "post" ? body.origin : null) ?? found?.origin ?? null;
551
+ return {
552
+ kind: "sync",
553
+ key: key ?? "",
554
+ platform: body.author.id.slice(0, Math.max(0, body.author.id.indexOf(":"))),
555
+ channel: body.source?.channel ?? found?.origin?.channel ?? origin?.channel ?? "",
556
+ thread: body.source?.thread ?? (origin ? origin.subthread ?? origin.thread : "")
557
+ };
558
+ }
559
+ /** The board message an import changes: the one it names, or the one its platform IDs are on the board as. */
560
+ async function importTarget(caller, body, signal) {
561
+ let id = body.message ?? null;
562
+ if (body.origin !== void 0) {
563
+ const { thread, message } = await backend.syncFind(caller, {
564
+ thread: body.origin.thread,
565
+ ...body.origin.message !== void 0 ? { message: body.origin.message } : {}
566
+ }, signal);
567
+ id = body.origin.message !== void 0 ? message : thread === null ? null : (await backend.syncThread(caller, { thread }, signal)).root;
568
+ }
569
+ return id === null ? null : lookup(caller, id, signal);
570
+ }
571
+ /**
572
+ * An import, through the middleware when it's a post or an edit that would land: what the
573
+ * middleware passed on, or what it responded when it dropped it, for sync to tell the author on
574
+ * the platform. A retry of one that landed stands.
575
+ */
576
+ async function syncImport(caller, body, signal) {
577
+ const send = (patched) => backend.syncImport(caller, patched, signal);
578
+ if (middleware.length === 0 || caller.origin.via?.kind === "middleware" || body.op !== "post" && body.op !== "edit") return send(body);
579
+ const access = accessOf(caller);
580
+ if (access && (!access.anywhere("sync") || "identity" in caller && caller.identity.principal.scope !== void 0)) return send(body);
581
+ const dropped = async (commit) => {
582
+ try {
583
+ return await commit();
584
+ } catch (error) {
585
+ if (!(error instanceof DroppedError)) throw error;
586
+ return {
587
+ message: null,
588
+ dropped: [...error.responses]
589
+ };
590
+ }
591
+ };
592
+ if (body.op === "post") {
593
+ if (body.id !== void 0 && (await backend.syncFind(caller, { post: body.id }, signal)).message !== null) return send(body);
594
+ let channel = body.channel;
595
+ let re = null;
596
+ if (body.re !== void 0) {
597
+ const root = await lookup(caller, body.re, signal);
598
+ if (!root) return send(body);
599
+ channel = root.channel;
600
+ re = root.re ?? root.id;
601
+ }
602
+ if (channel === void 0 || access && !access.can("sync", channel)) return send(body);
603
+ const source = await importSource(caller, body, null, signal);
604
+ if (!source) return send(body);
605
+ const attachments = await attachmentsOf(caller, body.attachments, signal);
606
+ return dropped(async () => screen({
607
+ kind: "post",
608
+ channel,
609
+ re,
610
+ text: body.text,
611
+ labels: body.labels ?? {},
612
+ attachments
613
+ }, externalActor(body.author), source, async (write) => send({
614
+ ...body,
615
+ text: write.text,
616
+ labels: write.labels
617
+ })));
618
+ }
619
+ if (body.origin === void 0 === (body.message === void 0)) return send(body);
620
+ const found = await importTarget(caller, body, signal);
621
+ if (!found || found.deleted || access && !access.can("sync", found.channel) || body.message !== void 0 && found.by.principal !== body.author.id) return send(body);
622
+ const source = await importSource(caller, body, found, signal);
623
+ if (!source) return send(body);
624
+ return dropped(async () => screen({
625
+ kind: "edit",
626
+ id: found.id,
627
+ channel: found.channel,
628
+ re: found.re,
629
+ text: body.text
630
+ }, externalActor(body.author), source, async (write) => send({
631
+ ...body,
632
+ text: write.text
633
+ })));
634
+ }
635
+ /** A message as `caller` may read it, or null. */
636
+ async function lookup(caller, id, signal) {
637
+ try {
638
+ const { message, replies } = await backend.message(caller, { id }, signal);
639
+ return [message, ...replies].find((candidate) => candidate.id === id) ?? null;
640
+ } catch (error) {
641
+ if (error instanceof HttpError && error.status === 404) return null;
642
+ throw error;
643
+ }
644
+ }
645
+ /**
646
+ * A post, with the emails it mentions resolved and the targets its channel syncs with landing
647
+ * beside it, once the caller may post there (and sync, for targets it names), through the
648
+ * middleware.
649
+ */
650
+ async function post(caller, input, signal) {
651
+ const send = (poster, body) => screenPost(poster, body, signal, (patched) => backend.post(poster, patched, signal));
652
+ if (!engine) return send(caller, input.sync?.length ? {
653
+ ...input,
654
+ sync: await Promise.all(input.sync.map(resolveTarget))
655
+ } : input);
656
+ const allowed = (input.sync?.length ?? 0) > 0 || mentionsEmails(input.text) || input.channel !== void 0 && !input.noSync ? await mayPost(caller, input, signal) : null;
657
+ if (!allowed) return send(caller, input);
658
+ const { access, channel } = allowed;
659
+ const body = input.re === void 0 && (input.sync?.length ?? 0) > 0 && (access === null || access.can("sync", channel)) ? {
660
+ ...input,
661
+ sync: await Promise.all(input.sync.map(resolveTarget))
662
+ } : input;
663
+ let poster = caller;
664
+ if (body.channel !== void 0 && !body.noSync) {
665
+ const config = await engine.configTargets(body.channel);
666
+ if (config.length > 0) poster = {
667
+ ...caller,
668
+ origin: {
669
+ ...caller.origin,
670
+ config
671
+ }
672
+ };
673
+ }
674
+ const text = await withEmails(body.text);
675
+ const answer = await send(poster, {
676
+ ...body,
677
+ text
678
+ });
679
+ return text !== input.text && access !== null && !access.readable(answer.message.channel) ? {
680
+ ...answer,
681
+ message: {
682
+ ...answer.message,
683
+ text: input.text
684
+ }
685
+ } : answer;
686
+ }
687
+ /**
688
+ * An import's answer, as a sync callback's client gets it: the message it wrote or changed, or the
689
+ * middleware's responses in the event's platform thread and a `DroppedError` when it dropped it.
690
+ */
691
+ async function landed(caller, answer) {
692
+ if (!answer.dropped) return answer.message;
693
+ for (const text of answer.dropped) await caller.platform.respond(text);
694
+ throw new DroppedError(answer.dropped);
695
+ }
696
+ /**
697
+ * What a sync callback's client does, on any backend. Its own writes are imports, as the platform
698
+ * user the event came from, which the middleware sees as any import; everything else, its reads
699
+ * included, it does as sync. A change it names by ID is an import that names it so.
700
+ */
701
+ async function asPlatform(name, caller, input, signal) {
702
+ const me = engine.caller;
703
+ const { author, key } = caller.platform;
704
+ const { via } = caller.origin;
705
+ const source = {
706
+ key,
707
+ channel: via.channel,
708
+ thread: via.thread
709
+ };
710
+ const change = async (body) => {
711
+ if (await landed(caller, await syncImport(me, body, signal)) === null) throw notFound(`message not found: #${body.message}`, "message_not_found");
712
+ return { ok: true };
713
+ };
714
+ switch (name) {
715
+ case "post": return importPost(caller, input, signal);
716
+ case "edit": {
717
+ const { id, text } = input;
718
+ return change({
719
+ op: "edit",
720
+ author,
721
+ from: null,
722
+ message: id,
723
+ text: await withEmails(text),
724
+ source
725
+ });
726
+ }
727
+ case "delete": return change({
728
+ op: "delete",
729
+ author,
730
+ from: null,
731
+ message: input.id
732
+ });
733
+ case "react": {
734
+ const { id, emoji, remove } = input;
735
+ return change({
736
+ op: "react",
737
+ author,
738
+ from: null,
739
+ message: id,
740
+ emoji,
741
+ ...remove !== void 0 ? { remove } : {}
742
+ });
743
+ }
744
+ case "label": {
745
+ const { id, set, unset } = input;
746
+ return change({
747
+ op: "labels",
748
+ author,
749
+ from: null,
750
+ message: id,
751
+ ...set ? { set } : {},
752
+ ...unset ? { unset } : {}
753
+ });
754
+ }
755
+ default: return invoke(name, me, input, signal);
756
+ }
757
+ }
758
+ /** A sync callback's post, as an import by its platform user: once per client ID, which the backend sees to. */
759
+ async function importPost(caller, input, signal) {
760
+ const sync = engine;
761
+ const me = sync.caller;
762
+ const { author, key } = caller.platform;
763
+ const { via } = caller.origin;
764
+ const read = async (id) => {
765
+ const message = await lookup(me, id, signal);
766
+ if (!message) throw notFound(`message not found: #${id}`, "message_not_found");
767
+ return message;
768
+ };
769
+ if (input.id !== void 0) {
770
+ const found = (await backend.syncFind(me, { post: input.id }, signal)).message;
771
+ if (found !== null) return { message: await read(found) };
772
+ }
773
+ const targets = await Promise.all((input.sync ?? []).map(resolveTarget));
774
+ const config = input.channel !== void 0 && !input.noSync ? await sync.configTargets(input.channel) : [];
775
+ const message = await landed(caller, await syncImport(config.length > 0 ? {
776
+ ...me,
777
+ origin: {
778
+ ...me.origin,
779
+ config
780
+ }
781
+ } : me, {
782
+ op: "post",
783
+ author,
784
+ from: null,
785
+ origin: null,
786
+ source: {
787
+ key,
788
+ channel: via.channel,
789
+ thread: via.thread
790
+ },
791
+ ...input.channel !== void 0 ? { channel: input.channel } : { re: input.re },
792
+ text: await withEmails(input.text),
793
+ ...input.labels ? { labels: input.labels } : {},
794
+ ...input.noSync ? { noSync: true } : {},
795
+ ...input.attachments?.length ? { attachments: input.attachments } : {},
796
+ ...input.id !== void 0 ? { id: input.id } : {}
797
+ }, signal));
798
+ for (const target of targets) await backend.syncReceipt(me, {
799
+ kind: "thread",
800
+ message,
801
+ target: target.target,
802
+ targetKind: target.kind,
803
+ cause: "request"
804
+ }, signal);
805
+ return { message: await read(message) };
806
+ }
807
+ /** The protocol method `name`, as board() serves it: what it posts and edits with the emails they mention resolved. */
808
+ async function invoke(name, caller, input, signal) {
809
+ switch (name) {
810
+ case "post": return post(caller, input, signal);
811
+ case "edit": {
812
+ const { id, text } = input;
813
+ const send = (body) => screenEdit(caller, body, signal, (patched) => backend.edit(caller, patched, signal));
814
+ if (!engine || !mentionsEmails(text) || !await mayPost(caller, { re: id }, signal)) return send({
815
+ id,
816
+ text
817
+ });
818
+ return send({
819
+ id,
820
+ text: await withEmails(text)
821
+ });
822
+ }
823
+ case "syncImport": return syncImport(caller, input, signal);
824
+ case "schedule": {
825
+ const body = input;
826
+ const allowed = engine && mentionsEmails(body.text) ? await mayPost(caller, body, signal) : null;
827
+ if (!allowed) return backend.schedule(caller, body, signal);
828
+ const text = await withEmails(body.text);
829
+ const answer = await backend.schedule(caller, {
830
+ ...body,
831
+ text
832
+ }, signal);
833
+ const { access } = allowed;
834
+ return text !== body.text && access !== null && !access.readable(answer.schedule.channel) ? {
835
+ ...answer,
836
+ schedule: {
837
+ ...answer.schedule,
838
+ text: body.text
839
+ }
840
+ } : answer;
841
+ }
842
+ case "inbox": {
843
+ const query = input;
844
+ if (engine && entries[query.key] && query.channel !== void 0) {
845
+ const access = accessOf(caller);
846
+ if (access === null || access.anywhere("sync")) return backend.inbox(caller, {
847
+ ...query,
848
+ channel: await engine.inboxChannel(query.key, query.channel)
849
+ }, signal);
850
+ }
851
+ return backend.inbox(caller, query, signal);
852
+ }
853
+ default: {
854
+ const method = backend[name];
855
+ if (typeof method !== "function") throw notFound(`no route: ${name}`);
856
+ return method.call(backend, caller, input, signal);
857
+ }
858
+ }
859
+ }
860
+ /**
861
+ * A sign-in that carries a code, where the entry's sync runs: a browser opens it with no token,
862
+ * so the code is all it has. `signIn` sends the person to the platform as whoever the code was
863
+ * issued to; `signedIn`, where the platform sends them back, links the account they approved.
864
+ */
865
+ async function signingIn(name, url, signal) {
866
+ const { sync: key, code } = SignInQuery.parse(queryObject(url));
867
+ if (!engine || !entries[key]) throw notFound(`no sync entry named ${key}`);
868
+ const page = (status, text) => new Response(`${text}\n`, {
869
+ status,
870
+ headers: { "content-type": "text/plain; charset=utf-8" }
871
+ });
872
+ const { holder } = await backend.syncFind(engine.caller, { code }, signal);
873
+ if (holder === null) return page(404, "That link has expired or been used. Run `vb identity link` again for a new one.");
874
+ if (name === "signIn") {
875
+ const back = ownUrl();
876
+ const at = await engine.signIn(key, holder, back ? `${back}/identity/signed-in?${new URLSearchParams({
877
+ sync: key,
878
+ code
879
+ })}` : void 0);
880
+ return new Response(null, {
881
+ status: 302,
882
+ headers: { location: at }
883
+ });
884
+ }
885
+ const linked = await engine.redeemSignIn(key, holder, code);
886
+ if (!linked) return page(409, `${entries[key].platform} hasn't said you approved yet: finish signing in, then open this page again.`);
887
+ return page(200, `Linked ${linked.handle ? `@${linked.handle}` : linked.actor}. You can close this tab.`);
888
+ }
889
+ /** board()'s own routes for a caller: sync's, which run where board() does. */
890
+ async function serve(name, caller, request, url, params) {
891
+ const signal = request.signal;
892
+ switch (name) {
893
+ case "sync": {
894
+ const body = SyncBody.parse(await readJson(request));
895
+ const id = params.id ?? "";
896
+ const access = accessOf(caller);
897
+ if (access && engine) {
898
+ const message = await lookup(caller, id, signal);
899
+ if (!message) throw notFound(`message not found: #${id}`, "message_not_found");
900
+ if (!access.can("sync", message.channel)) throw forbidden(`this token can't sync in ${message.channel}`);
901
+ }
902
+ const target = await resolveTarget(body.target);
903
+ const { attached } = await backend.syncReceipt(caller, {
904
+ kind: "thread",
905
+ message: id,
906
+ target: target.target,
907
+ targetKind: target.kind,
908
+ cause: "request"
909
+ }, signal);
910
+ return json({
911
+ target: target.target,
912
+ attached: attached ?? true
913
+ });
914
+ }
915
+ case "identityUnlink": {
916
+ const body = IdentityUnlinkBody.parse(await readJson(request));
917
+ const { links } = await backend.identity(caller, {}, signal);
918
+ const wanted = body.actor.replace(/^@/, "").toLowerCase();
919
+ const found = links.filter((link) => link.actor === body.actor || link.handle?.toLowerCase() === wanted).map((link) => link.actor);
920
+ if (found.length === 0) throw notFound(`${body.actor} isn't linked to you`, "not_linked");
921
+ if (found.length > 1) throw badRequest(`${body.actor} names several of your accounts: use ${found.join(" or ")}`, "ambiguous_actor");
922
+ await backend.syncUnlink(caller, { account: found[0] }, signal);
923
+ return json({
924
+ ok: true,
925
+ actor: found[0]
926
+ });
927
+ }
928
+ default: throw notFound(`no route: ${request.method} ${url.pathname}`);
929
+ }
930
+ }
931
+ const synced = Object.fromEntries(METHODS.map((name) => [name, name === "syncImport" ? syncImport : (caller, input, signal) => backend[name].call(backend, caller, input, signal)]));
932
+ engine?.bind({
933
+ backend: synced,
934
+ client: (platform, trigger) => clientAs({
935
+ platform: {
936
+ author: platform.author,
937
+ key: platform.key,
938
+ respond: platform.respond
939
+ },
940
+ origin: { via: platform.via }
941
+ }, trigger)
942
+ });
943
+ /**
944
+ * Server-sent events, from the backend's long-poll: each event's `id` is its cursor, so
945
+ * `Last-Event-ID` resumes exactly. A request's caller is worked out again for each page, so what
946
+ * `access` says now holds, and a token that runs out ends it.
947
+ */
948
+ async function sse(caller, query, request, identify) {
949
+ const first = await backend.events(caller, {
950
+ ...query,
951
+ wait: 0
952
+ }, request.signal);
953
+ const encoder = new TextEncoder();
954
+ const controller = new AbortController();
955
+ const deadline = Date.now() + streamMaxMs;
956
+ const signal = AbortSignal.any([
957
+ request.signal,
958
+ controller.signal,
959
+ AbortSignal.timeout(streamMaxMs)
960
+ ]);
961
+ const body = new ReadableStream({
962
+ async start(stream) {
963
+ const send = (text) => {
964
+ try {
965
+ stream.enqueue(encoder.encode(text));
966
+ } catch {}
967
+ };
968
+ const ping = setInterval(() => send(": ping\n\n"), 15e3);
969
+ try {
970
+ send(": connected\n\n");
971
+ let page = first;
972
+ let after;
973
+ for (;;) {
974
+ for (const event of page.events) send(`id: ${event.cursor}\nevent: board\ndata: ${JSON.stringify(event)}\n\n`);
975
+ if (page.cursor !== (page.events.at(-1)?.cursor ?? after)) send(`id: ${page.cursor}\n\n`);
976
+ after = page.cursor;
977
+ const left = deadline - Date.now();
978
+ if (signal.aborted || left <= 0) break;
979
+ page = await backend.events(identify ? await identify() : caller, {
980
+ ...query,
981
+ after,
982
+ wait: Math.min(300, left / 1e3)
983
+ }, signal);
984
+ }
985
+ } catch (error) {
986
+ if (!signal.aborted) console.error("[board] feed failed", error);
987
+ } finally {
988
+ clearInterval(ping);
989
+ try {
990
+ stream.close();
991
+ } catch {}
992
+ }
993
+ },
994
+ cancel() {
995
+ controller.abort();
996
+ }
997
+ });
998
+ return new Response(body, { headers: {
999
+ "content-type": "text/event-stream",
1000
+ "cache-control": "no-cache, no-transform",
1001
+ connection: "keep-alive"
1002
+ } });
1003
+ }
1004
+ /**
1005
+ * Discovery, as a request reaches the board: who the board is to it (the URL a token must be minted
1006
+ * for, and the project people mint them with), which is board()'s to say, and the sync entries
1007
+ * accounts link on. Those are the backend's, where it keeps the board; a board on `http()` lists its own.
1008
+ */
1009
+ async function discovery(request, url, signal) {
1010
+ const { projectId, teamId } = await policy.discoveryIdentity(request.headers);
1011
+ const sync = backend.keeps ? (await backend.discovery(OWN, {}, signal)).sync : Object.entries(entries).map(([key, entry]) => ({
1012
+ key,
1013
+ platform: entry.platform
1014
+ }));
1015
+ return {
1016
+ version: VERSION,
1017
+ url,
1018
+ audience: url,
1019
+ team_id: teamId,
1020
+ project_id: projectId,
1021
+ sync
1022
+ };
1023
+ }
1024
+ async function dispatch(request, injected) {
1025
+ try {
1026
+ const url = new URL(request.url);
1027
+ const match = route(request.method, url.pathname);
1028
+ if (!match) throw notFound(`no route: ${request.method} ${url.pathname}`);
1029
+ const { target } = match;
1030
+ if (!injected && !match.root && (state.learned === null || state.learned.startsWith(`${match.base}/`))) state.learned = match.base;
1031
+ const boardUrl = (options.url ?? `${url.origin}${match.base}`).replace(/\/+$/, "");
1032
+ if (target.kind === "board" && target.name === "webhook") {
1033
+ if (!engine || !entries[match.params.key ?? ""]) throw notFound(`no sync entry named ${match.params.key}`);
1034
+ return await engine.handleWebhook(match.params.key, request);
1035
+ }
1036
+ if (target.kind === "board" && (target.name === "signIn" || target.name === "signedIn")) return await signingIn(target.name, url, request.signal);
1037
+ if (target.kind === "board" && target.name === "avatar") {
1038
+ const name = match.param ?? "";
1039
+ if (!KNOWN_AGENTS.includes(name)) throw notFound(`no avatar: ${name}`);
1040
+ return new Response(avatarSvg(name), { headers: {
1041
+ "content-type": "image/svg+xml",
1042
+ "cache-control": "public, max-age=86400"
1043
+ } });
1044
+ }
1045
+ if (target.kind === "board" && target.name === "openapi") return json(openapi());
1046
+ if (target.kind === "backend") return await target.route.handle(request, {
1047
+ caller: injected && "platform" in injected ? engine.caller : injected,
1048
+ param: match.param
1049
+ });
1050
+ if (!injected && !policy.open) throw servesOnly();
1051
+ if (!injected && target.kind === "protocol" && target.name === "discovery") return json(await discovery(request, boardUrl, request.signal));
1052
+ if (target.kind === "protocol" && typeof backend[target.name] !== "function") throw notFound(`no route: ${request.method} ${url.pathname}`);
1053
+ const own = injected && "platform" in injected ? engine.caller : injected;
1054
+ const asked = own ? null : requestOf(request, boardUrl);
1055
+ const caller = own ?? await callerOf(asked);
1056
+ if (target.kind === "board") return await serve(target.name, caller, request, url, match.params);
1057
+ const input = await inputOf(target.name, request, url, match.params);
1058
+ if (target.name === "events" && (request.headers.get("accept") ?? "").includes("text/event-stream")) return await sse(caller, input, request, asked ? () => callerOf(asked) : null);
1059
+ const output = injected && "platform" in injected ? await asPlatform(target.name, injected, input, request.signal) : await invoke(target.name, caller, input, request.signal);
1060
+ if (target.name === "attachment") return attachmentResponse(output);
1061
+ return json(output, CREATED.has(target.name) ? 201 : 200);
1062
+ } catch (error) {
1063
+ return toErrorResponse(error);
1064
+ }
1065
+ }
1066
+ return {
1067
+ dispatch,
1068
+ async inject(request, caller, trigger, writes) {
1069
+ if (trigger !== null && request.method === "POST" && new URL(request.url).pathname.endsWith("/messages")) {
1070
+ const body = await request.json() ?? {};
1071
+ body.id ??= await derivedId("board-inbound", trigger, "client", String(writes.made++));
1072
+ request = new Request(request.url, {
1073
+ method: "POST",
1074
+ headers: request.headers,
1075
+ body: JSON.stringify(body)
1076
+ });
1077
+ }
1078
+ return dispatch(request, caller);
1079
+ },
1080
+ async run(id, event, { trigger, attempt }) {
1081
+ if (id === "@sync" && engine) return engine.deliver(event, { attempt });
1082
+ const watcher = watch[id];
1083
+ if (!watcher) throw new Error(`this board has no watcher ${id}`);
1084
+ await watcher.run({
1085
+ event,
1086
+ board: handleOf({
1087
+ self: id,
1088
+ origin: { via: {
1089
+ kind: "watcher",
1090
+ id
1091
+ } }
1092
+ }, trigger)
1093
+ });
1094
+ },
1095
+ async receive(key, item, { id, attempt }) {
1096
+ if (!engine) throw new Error(`this board has no sync, so nothing reads ${key}'s inbox`);
1097
+ await engine.receive(key, item, {
1098
+ id,
1099
+ attempt
1100
+ });
1101
+ },
1102
+ pull: () => engine ? engine.pull() : Promise.resolve(0),
1103
+ async readSource(key, target, limit) {
1104
+ if (!engine || !entries[key]) throw notFound(`this board doesn't run sync entry ${key}: read it where its sync runs`, "not_synced_here");
1105
+ return engine.readSource(key, target, limit);
1106
+ },
1107
+ typing: async (messages, status) => engine ? engine.typing(messages, status) : []
1108
+ };
1109
+ }
1110
+ //#endregion
1111
+ //#region src/server/board.ts
1112
+ /** How long a handle's live events wait for the next, in seconds, before asking again. */
1113
+ const EVENTS_WAIT_S = 25;
1114
+ /**
1115
+ * A board for this project, on `backend`: `a2({ store, … })` keeps it in this deployment, and
1116
+ * `http(url)` uses another board that speaks the protocol. What it returns is the handle: its
1117
+ * methods act as the board's caller, and `fetch` serves the protocol over HTTP. Create it once,
1118
+ * at module scope, or one per tenant for a host.
1119
+ */
1120
+ function createBoard(options, runtime) {
1121
+ const backend = options.backend ?? runtime.http();
1122
+ const policy = createPolicy(options, backend.keeps, runtime.serving);
1123
+ const watch = options.watch ?? {};
1124
+ const entries = options.sync ?? {};
1125
+ const state = { learned: null };
1126
+ /** Under the configured mount, so the router accepts the paths an in-process client builds. */
1127
+ const inProcess = `http://in-process.board${(options.basePath ?? "").replace(/\/+$/, "")}`;
1128
+ /**
1129
+ * This board's own URL, where a board elsewhere wakes it: `url`, or on Vercel its production
1130
+ * domain (a preview's own URL there), and never a request's Host, which its sender chooses.
1131
+ */
1132
+ function ownUrl() {
1133
+ if (options.url) return options.url.replace(/\/+$/, "");
1134
+ const env = typeof process === "undefined" ? void 0 : process.env;
1135
+ const domain = env?.VERCEL_ENV === "production" ? env.VERCEL_PROJECT_PRODUCTION_URL : env?.VERCEL_URL;
1136
+ const base = options.basePath ?? state.learned;
1137
+ return domain && base !== null ? `https://${domain}${base}`.replace(/\/+$/, "") : null;
1138
+ }
1139
+ let loading = null;
1140
+ /** The board's pipeline, made once, when the board first serves, watches, syncs, or a handle calls it. */
1141
+ function server() {
1142
+ loading ??= createServe(context);
1143
+ loading.catch(() => loading = null);
1144
+ return loading;
1145
+ }
1146
+ function clientAs(caller, trigger) {
1147
+ const writes = { made: 0 };
1148
+ return createBoardClient({
1149
+ url: inProcess,
1150
+ token: "in-process",
1151
+ fetch: (async (input, init) => (await server()).inject(new Request(input, init), caller, trigger, writes))
1152
+ });
1153
+ }
1154
+ const serve = async (request) => (await server()).dispatch(request, null);
1155
+ /** A handle on `client`, which `as` gives other profiles of. */
1156
+ function handle(client, url) {
1157
+ const made = {
1158
+ ...client,
1159
+ fetch: serve,
1160
+ as
1161
+ };
1162
+ if (url) Object.defineProperty(made, "url", {
1163
+ get: url,
1164
+ enumerable: true
1165
+ });
1166
+ return made;
1167
+ }
1168
+ /**
1169
+ * A handle through board(), as `caller`: a post's `id` is its own, derived under who it writes as,
1170
+ * so a retry posts once and no request can take it first. A verified caller's are a backend's,
1171
+ * derived already (a schedule's send's), so they go as they are.
1172
+ */
1173
+ function handleOf(caller, trigger) {
1174
+ const client = clientAs(caller, trigger);
1175
+ if ("identity" in caller) return handle({
1176
+ ...client,
1177
+ events: (query, opts) => events(client, query, opts)
1178
+ }, () => ownUrl() ?? inProcess);
1179
+ const via = caller.origin.via?.kind ?? "client";
1180
+ const id = (given) => derivedId(`board-${via === "watcher" ? "watch" : via}`, caller.self ?? "", given);
1181
+ const owned = async (input) => typeof input === "string" || input.id === void 0 ? input : {
1182
+ ...input,
1183
+ id: await id(input.id)
1184
+ };
1185
+ return handle({
1186
+ ...client,
1187
+ post: async (channel, input) => client.post(channel, await owned(input)),
1188
+ reply: async (re, input) => client.reply(re, await owned(input)),
1189
+ posted: async (given) => client.posted(await id(given)),
1190
+ request: (path, init = {}) => {
1191
+ const { as: profile, ...rest } = init;
1192
+ return profile !== void 0 ? as(profile).request(path, rest) : client.request(path, rest);
1193
+ },
1194
+ events: (query, opts) => events(client, query, opts)
1195
+ }, () => ownUrl() ?? inProcess);
1196
+ }
1197
+ const profileCaller = (profile) => {
1198
+ const name = profile === null ? null : typeof profile === "string" ? profile : profile.name;
1199
+ return {
1200
+ self: name,
1201
+ profile,
1202
+ origin: { via: {
1203
+ kind: "client",
1204
+ name
1205
+ } }
1206
+ };
1207
+ };
1208
+ function as(profile) {
1209
+ return handleOf(profileCaller(profile), null);
1210
+ }
1211
+ const context = {
1212
+ options,
1213
+ backend,
1214
+ runtime,
1215
+ policy,
1216
+ state,
1217
+ ownUrl,
1218
+ clientAs,
1219
+ handleOf
1220
+ };
1221
+ const subscriptions = Object.fromEntries(Object.entries(watch).map(([id, watcher]) => [id, {
1222
+ kind: "tail",
1223
+ tail: watcher.tail,
1224
+ when: watcher.when ?? "matches"
1225
+ }]));
1226
+ if (Object.keys(entries).length > 0) subscriptions[SYNC] = { kind: "sync" };
1227
+ /** This board's sync entries, as its sync and discovery see them. */
1228
+ const ownEntries = Object.entries(entries).map(([key, entry]) => ({
1229
+ key,
1230
+ platform: entry.platform,
1231
+ signIn: entry.users !== null
1232
+ }));
1233
+ const binding = {
1234
+ subscriptions,
1235
+ run: async (id, event, meta) => (await server()).run(id, event, meta),
1236
+ receive: async (key, item, meta) => (await server()).receive(key, item, meta),
1237
+ pull: async () => ownEntries.length > 0 ? (await server()).pull() : 0,
1238
+ sync: ownEntries,
1239
+ readSource: async (key, target, limit) => (await server()).readSource(key, target, limit),
1240
+ typing: async (messages, status) => (await server()).typing(messages, status),
1241
+ accessNow: (principal) => policy.accessNow(principal),
1242
+ project: () => policy.project(),
1243
+ handle: (caller) => handleOf(caller, null),
1244
+ url: ownUrl
1245
+ };
1246
+ backend.bind(binding);
1247
+ return handleOf({
1248
+ self: null,
1249
+ origin: { via: {
1250
+ kind: "client",
1251
+ name: null
1252
+ } }
1253
+ }, null);
1254
+ }
1255
+ /**
1256
+ * Live events for a handle, from the backend's long-poll: each page from the last cursor it saw,
1257
+ * waiting for the next. It backs off after a failure, up to 30 seconds; a 4xx throws.
1258
+ */
1259
+ async function* events(client, query = {}, opts = {}) {
1260
+ let after = query.after;
1261
+ let failures = 0;
1262
+ while (!opts.signal?.aborted) {
1263
+ let page;
1264
+ try {
1265
+ page = await client.poll({
1266
+ ...query,
1267
+ ...after !== void 0 ? { after } : {},
1268
+ wait: EVENTS_WAIT_S
1269
+ }, opts.signal);
1270
+ } catch (error) {
1271
+ if (opts.signal?.aborted) return;
1272
+ if (error instanceof BoardError && error.status >= 400 && error.status < 500) throw error;
1273
+ await new Promise((resolve) => setTimeout(resolve, Math.min(3e4, 500 * 2 ** failures++)));
1274
+ continue;
1275
+ }
1276
+ failures = 0;
1277
+ for (const event of page.events) {
1278
+ yield event;
1279
+ if (opts.signal?.aborted) return;
1280
+ }
1281
+ after = page.cursor;
1282
+ }
1283
+ }
1284
+ //#endregion
1285
+ export { createBoard as t };