@vercel/board 0.16.0 → 0.17.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 (127) hide show
  1. package/README.md +27 -27
  2. package/dist/{a2-C41hD8SF.mjs → a2-Bck5x1Kh.mjs} +969 -25
  3. package/dist/access.d.mts +1 -1
  4. package/dist/access.mjs +3 -3
  5. package/dist/{actors-DjMeZpnU.mjs → actors-BOamkEuz.mjs} +3 -73
  6. package/dist/attempts-CLG840Oj.mjs +20 -0
  7. package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
  8. package/dist/backend-B-SJ7p6p.d.mts +680 -0
  9. package/dist/backend-a2/contract.d.mts +873 -2
  10. package/dist/backend-a2/contract.mjs +2 -2
  11. package/dist/backend-a2/scheduler-local.d.mts +1 -1
  12. package/dist/backend-a2/scheduler-local.mjs +1 -1
  13. package/dist/backend-a2/scheduler-qstash.d.mts +78 -1
  14. package/dist/backend-a2/scheduler-qstash.mjs +521 -1
  15. package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
  16. package/dist/backend-a2/scheduler-vercel.mjs +203 -1
  17. package/dist/backend-a2/store-memory.d.mts +2 -2
  18. package/dist/backend-a2/store-memory.mjs +3 -3
  19. package/dist/backend-a2/store-postgres.d.mts +1 -1
  20. package/dist/backend-a2/store-postgres.mjs +2850 -2
  21. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  22. package/dist/backend-a2/store-redis-http.mjs +490 -2
  23. package/dist/backend-a2/store-redis.d.mts +10 -2
  24. package/dist/backend-a2/store-redis.mjs +51 -2
  25. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  26. package/dist/backend-a2/store-sqlite.mjs +3 -3
  27. package/dist/backend-a2.d.mts +2706 -3
  28. package/dist/backend-a2.mjs +1 -1
  29. package/dist/{backend-http-Bid7RIpC.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
  30. package/dist/{backend-http-0roEmjl0.mjs → backend-http-C5I7Wfdw.mjs} +2 -2
  31. package/dist/{backend-http-BVRno-ca.mjs → backend-http-CuUIACCs.mjs} +32 -3
  32. package/dist/backend-http.d.mts +1 -1
  33. package/dist/backend-http.mjs +1 -1
  34. package/dist/backend-http.node.d.mts +1 -1
  35. package/dist/backend-http.node.mjs +1 -1
  36. package/dist/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
  37. package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
  38. package/dist/cli.mjs +30 -10
  39. package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
  40. package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
  41. package/dist/conformance.d.mts +1 -1
  42. package/dist/conformance.mjs +1 -1
  43. package/dist/{engine-Bir5s8ts.mjs → engine-D0oBUBEK.mjs} +249 -163
  44. package/dist/events-CewD4kw7.mjs +75 -0
  45. package/dist/{exports-Dvc1o52I.mjs → exports-DOdA7KhF.mjs} +35 -32
  46. package/dist/exports-ZsgjzEnM.d.mts +4 -0
  47. package/dist/external-BV_cUncs.mjs +151 -0
  48. package/dist/folds-CRYY14kc.d.mts +534 -0
  49. package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
  50. package/dist/ids-C6AYo1bu.mjs +31 -0
  51. package/dist/index.d.mts +4 -4
  52. package/dist/{index.default-Bkg77Pxn.mjs → index.default-n4qKN7EL.mjs} +1 -1
  53. package/dist/index.mjs +5 -5
  54. package/dist/index.node.d.mts +4 -4
  55. package/dist/index.node.mjs +3 -3
  56. package/dist/inspect/app.js +31 -31
  57. package/dist/inspect/engine.js +5 -5
  58. package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
  59. package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
  60. package/dist/mentions-CB6ShY4R.mjs +72 -0
  61. package/dist/{targets-yM5JBUgr.mjs → middleware-CqaCwucP.mjs} +1 -33
  62. package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
  63. package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
  64. package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
  65. package/dist/provider-B3DpYSDq.mjs +220 -0
  66. package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
  67. package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
  68. package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
  69. package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
  70. package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
  71. package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
  72. package/dist/{serve-BsOOc-6_.mjs → serve-CNYvA4pT.mjs} +5 -5
  73. package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
  74. package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
  75. package/dist/{server-jwPY7_wi.mjs → server-snYFkITb.mjs} +4 -4
  76. package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
  77. package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
  78. package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
  79. package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
  80. package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
  81. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
  82. package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
  83. package/dist/store-memory-Bas9Uq5C.mjs +872 -0
  84. package/dist/store-memory-ZLiZbH3R.mjs +2 -0
  85. package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
  86. package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
  87. package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
  88. package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
  89. package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
  90. package/dist/sync-github.d.mts +24 -17
  91. package/dist/sync-github.mjs +89 -22
  92. package/dist/sync-kapso.d.mts +15 -13
  93. package/dist/sync-kapso.mjs +17 -14
  94. package/dist/sync-slack.d.mts +12 -17
  95. package/dist/sync-slack.mjs +54 -42
  96. package/dist/sync.chat.d.mts +1 -1
  97. package/dist/sync.chat.mjs +1 -1
  98. package/dist/sync.d.mts +3 -3
  99. package/dist/sync.mjs +5 -4
  100. package/dist/targets-C-wdR5wP.mjs +33 -0
  101. package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
  102. package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
  103. package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
  104. package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-DojIuoFg.mjs} +233 -28
  105. package/docs/01-board/01-mounting.md +1 -1
  106. package/docs/02-sync/01-overview.md +69 -47
  107. package/docs/02-sync/02-functions.md +395 -0
  108. package/docs/02-sync/03-slack.md +30 -25
  109. package/docs/02-sync/04-github.md +62 -26
  110. package/docs/02-sync/05-whatsapp.md +23 -19
  111. package/docs/02-sync/06-providers.md +58 -26
  112. package/docs/03-guides/04-triage.md +1 -1
  113. package/docs/04-reference/01-cli.md +26 -6
  114. package/docs/04-reference/02-board.md +18 -5
  115. package/docs/04-reference/03-http.md +5 -2
  116. package/docs/04-reference/05-conformance.md +1 -1
  117. package/docs/index.md +5 -10
  118. package/package.json +22 -6
  119. package/dist/a2-D24cvwpN.d.mts +0 -3359
  120. package/dist/access-CJU2-vaa.mjs +0 -226
  121. package/dist/attempts-Bxp9KgPf.mjs +0 -919
  122. package/dist/exports-DLiE7fLY.d.mts +0 -4
  123. package/dist/provider-CuewcDNZ.mjs +0 -205
  124. package/dist/redis-tables-B35vv4CZ.mjs +0 -842
  125. package/dist/reducers-B_vUGQRp.d.mts +0 -1172
  126. package/dist/watchers-hash-CLjsa6od.mjs +0 -225
  127. package/docs/02-sync/02-callbacks.md +0 -265
@@ -1,305 +1,210 @@
1
- import { S as storable, _ as RemoteWatcherId, c as ChannelPath, d as LabelKey, g as MessageText, h as MessageId, i as Attachment, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, o as Author, p as Labels, u as Handle, v as SCOPE_MAX, y as SeenActorKind } from "./schemas-Be08nyfW.mjs";
1
+ import { S as storable, _ as RemoteWatcherId, c as ChannelPath, d as LabelKey, g as MessageText, h as MessageId, i as Attachment, l as Emoji, n as ActorId, o as Author, p as Labels, u as Handle, v as SCOPE_MAX, y as SeenActorKind } from "./schemas-Be08nyfW.mjs";
2
+ import { a as Link, i as ExternalTarget, n as ExternalEvent, o as Origin } from "./external-BV_cUncs.mjs";
3
+ import { n as setOwn, t as own } from "./own-OwgPWdmh.mjs";
4
+ import { a as reducer, i as makeReducerBuilder, t as assertSyncSchema } from "./reducer-CjmvZSAb-D_OSISmj.mjs";
5
+ import { n as foldMessages, t as foldActors } from "./folds-Dyjput8M.mjs";
2
6
  import { z } from "zod";
3
- import { contract } from "experimental-a2";
4
- //#region src/contract/contract.ts
5
- /**
6
- * An external thread or channel, prefixed with its sync key:
7
- * `slack:C0123ABC` (a channel), `slack:C0123ABC:1727.1234` (a thread),
8
- * `github:basehub-ai/www:12` (a PR thread).
9
- */
10
- const ExternalTarget = z.string().min(3).max(300);
11
- /** Where a message came from on a platform. `thread` is grouped (GitHub review threads fold into their PR). */
12
- const Origin = z.object({
13
- channel: ExternalTarget.optional(),
14
- thread: ExternalTarget,
15
- /** The exact platform thread, when it differs from `thread`. */
16
- subthread: ExternalTarget.nullable(),
17
- message: z.string().min(1).max(300),
18
- context: z.string().max(300).nullable()
19
- });
20
- /** What a linked message said, as the board saw it when the link arrived. */
21
- const LinkSnapshot = z.object({
22
- author: z.string().min(1).max(256),
23
- text: z.string().max(MESSAGE_TEXT_MAX),
24
- /** When it was written, if the platform said. */
25
- createdAt: z.string().max(64).nullable(),
26
- /** Its files, linked rather than copied. */
27
- files: z.array(z.object({
28
- name: z.string().min(1).max(1e3),
29
- url: z.string().max(4e3).nullable()
30
- })).max(20)
31
- });
32
- /**
33
- * A platform message linked from a message (a Slack permalink): the board message
34
- * it was synced to, if any, and a snapshot of it, or why the board couldn't read it.
35
- */
36
- const Link = z.object({
37
- url: z.string().min(1).max(4e3),
38
- message: MessageId.nullable(),
39
- snapshot: LinkSnapshot.nullable(),
40
- /** Why there's no snapshot: `not_in_channel`, `channel_not_found`. */
41
- unavailable: z.string().max(300).nullable()
42
- });
7
+ //#region ../../node_modules/.pnpm/experimental-a2@0.22.0_@upstash+qstash@2.12.0_@vercel+queue@0.7.0_ai@7.0.130_zod@4.6.5__52060f00b21aecfab049c27fa3abd2f9/node_modules/experimental-a2/dist/contract-CWukt01d.js
8
+ function contract(options) {
9
+ for (const type of Object.keys(options.events ?? {})) if (type.startsWith("a2.")) throw new TypeError(`contract '${String(options.name)}': event types starting with 'a2.' are reserved`);
10
+ return defineContract(options);
11
+ }
12
+ function defineContract(options) {
13
+ const { name } = options;
14
+ if (typeof name !== "string" || name.length === 0) throw new TypeError("contract name must be a non-empty string");
15
+ if (name.includes("")) throw new TypeError("contract name contains a reserved control character");
16
+ const defs = options.events;
17
+ for (const [type, schema] of Object.entries(defs)) {
18
+ if (typeof schema?.["~standard"]?.validate !== "function") throw new TypeError(`contract '${name}': the value for '${type}' is not a Standard Schema (expected an object with '~standard')`);
19
+ assertSyncSchema(schema, `event '${type}'`);
20
+ }
21
+ const events = Object.freeze({ ...defs });
22
+ const presenceDefs = options.presence ?? {};
23
+ for (const [field, schema] of Object.entries(presenceDefs)) {
24
+ if (typeof schema?.["~standard"]?.validate !== "function") throw new TypeError(`contract '${name}': the value for presence field '${field}' is not a Standard Schema (expected an object with '~standard')`);
25
+ assertSyncSchema(schema, `presence field '${field}'`);
26
+ }
27
+ const presence = Object.freeze({ ...presenceDefs });
28
+ return {
29
+ name,
30
+ events,
31
+ presence,
32
+ batch: (...items) => items,
33
+ reducer: options.reducer === void 0 ? void 0 : {
34
+ ...options.reducer,
35
+ ...makeReducerBuilder(events, presence, options.reducer).fold(options.reducer.fold)
36
+ }
37
+ };
38
+ }
39
+ //#endregion
40
+ //#region src/contract/events.ts
43
41
  /** The sync key a write came from, or null when it was written on the board. */
44
42
  const From = z.string().nullable();
45
43
  /** The thread key: the event ID of the thread's root. Lanes and sync state are keyed by it. */
46
44
  const ThreadKey = z.string().min(1).max(200).refine(storable, "thread key has a NUL character or a lone surrogate");
47
45
  /** The board's one log. Every board fact is an event in session `board`. */
48
- const boardContract = contract({
49
- name: "board",
50
- events: {
51
- "message.posted": z.object({
52
- channel: ChannelPath,
53
- /** Empty only when the message has attachments. */
54
- text: z.union([MessageText, z.literal("")]),
55
- /** The thread root, or null for a root. */
56
- re: MessageId.nullable(),
57
- /** The exact message replied to, when it isn't the root. */
58
- replyTo: MessageId.nullable(),
59
- thread: ThreadKey,
60
- labels: Labels,
61
- noSync: z.boolean(),
62
- mentions: z.array(ActorId),
63
- by: Author,
64
- origin: Origin.nullable(),
65
- from: From,
66
- /** Files on the message. Events written before attachments have none. */
67
- attachments: z.array(Attachment).max(10).optional(),
68
- /** Platform messages it links to. Events written before links have none. */
69
- links: z.array(Link).max(10).optional(),
70
- /** (server) Posted by a schedule: its ID and which send this is, from 0. Other messages have none. */
71
- schedule: z.object({
72
- id: z.number().int().positive(),
73
- n: z.number().int().nonnegative()
74
- }).optional(),
75
- /**
76
- * (server) Its author couldn't read the channel it posted in. Where a
77
- * caller can't read, it edits and deletes only these of its own. Events
78
- * from before channel access have none: everyone read everything then.
79
- */
80
- blind: z.literal(true).optional(),
81
- /**
82
- * (server) The principal of the sync elsewhere that imported it, whose copy `origin` names:
83
- * that sync's to act on. None for the board's own sync, or a write on the board.
84
- */
85
- importedBy: ActorId.optional()
86
- }),
87
- /** A message's links changed: an edit linked something new, or the platform sent what a link says. */
88
- "message.linked": z.object({
89
- id: MessageId,
90
- thread: ThreadKey,
91
- links: z.array(Link).max(10),
92
- by: Author,
93
- from: From
94
- }),
95
- "message.edited": z.object({
96
- id: MessageId,
97
- thread: ThreadKey,
98
- text: MessageText,
99
- mentions: z.array(ActorId),
100
- by: Author,
101
- from: From
102
- }),
103
- "message.deleted": z.object({
104
- id: MessageId,
105
- thread: ThreadKey,
106
- by: Author,
107
- from: From
108
- }),
109
- "reaction.added": z.object({
110
- id: MessageId,
111
- thread: ThreadKey,
112
- emoji: Emoji,
113
- by: Author,
114
- from: From
115
- }),
116
- "reaction.removed": z.object({
117
- id: MessageId,
118
- thread: ThreadKey,
119
- emoji: Emoji,
120
- by: Author,
121
- from: From
122
- }),
123
- "labels.changed": z.object({
124
- id: MessageId,
125
- thread: ThreadKey,
126
- set: Labels,
127
- unset: z.array(LabelKey),
128
- by: Author
129
- }),
130
- /** A thread is synced with an external channel (start a thread there) or thread (join it). */
131
- "thread.synced": z.object({
132
- thread: ThreadKey,
133
- target: ExternalTarget,
134
- cause: z.enum(["config", "request"]),
135
- kind: z.enum(["channel", "thread"]).optional(),
136
- by: Author
137
- }),
138
- /** An outbound delivery receipt: board message `id` exists on the platform as `external`. */
139
- "message.synced": z.object({
140
- id: MessageId,
141
- thread: ThreadKey,
142
- target: ExternalTarget,
143
- external: z.object({
144
- thread: ExternalTarget,
145
- message: z.string().min(1).max(300),
146
- /** Posted with its author's own platform token, so it shows as them. Older receipts have none. */
147
- asUser: z.literal(true).optional()
148
- }),
149
- /**
150
- * (server) The principal of the sync elsewhere that recorded it: its copy is that sync's to
151
- * change. None for the board's own sync.
152
- */
153
- syncedBy: ActorId.optional()
154
- }),
46
+ const boardEvents = {
47
+ "message.posted": z.object({
48
+ channel: ChannelPath,
49
+ /** Empty only when the message has attachments. */
50
+ text: z.union([MessageText, z.literal("")]),
51
+ /** The thread root, or null for a root. */
52
+ re: MessageId.nullable(),
53
+ /** The exact message replied to, when it isn't the root. */
54
+ replyTo: MessageId.nullable(),
55
+ thread: ThreadKey,
56
+ labels: Labels,
57
+ noSync: z.boolean(),
58
+ mentions: z.array(ActorId),
59
+ by: Author,
60
+ origin: Origin.nullable(),
61
+ from: From,
62
+ /** Files on the message. Events written before attachments have none. */
63
+ attachments: z.array(Attachment).max(10).optional(),
64
+ /** Platform messages it links to. Events written before links have none. */
65
+ links: z.array(Link).max(10).optional(),
66
+ /** (server) Posted by a schedule: its ID and which send this is, from 0. Other messages have none. */
67
+ schedule: z.object({
68
+ id: z.number().int().positive(),
69
+ n: z.number().int().nonnegative()
70
+ }).optional(),
155
71
  /**
156
- * Board message `id`'s files reached `target` after its text: `files` are the
157
- * platform's IDs for them, or null when the platform refused files and the
158
- * text got a line for each instead.
72
+ * (server) Its author couldn't read the channel it posted in. Where a
73
+ * caller can't read, it edits and deletes only these of its own. Events
74
+ * from before channel access have none: everyone read everything then.
159
75
  */
160
- "message.files_synced": z.object({
161
- id: MessageId,
162
- thread: ThreadKey,
163
- target: ExternalTarget,
164
- files: z.array(z.string().min(1).max(300)).max(10).nullable(),
165
- /** (server) As on `message.synced`: only the sync that recorded the copy sets its files. */
166
- syncedBy: ActorId.optional()
167
- }),
168
- /** A delivery that gave up: board message `id` didn't reach `target`, and `error` says why. */
169
- "message.sync_failed": z.object({
170
- id: MessageId,
171
- thread: ThreadKey,
172
- target: ExternalTarget,
173
- error: z.string().min(1).max(500)
174
- }),
175
- "actor.seen": z.object({
176
- actor: ActorId,
177
- kind: SeenActorKind,
178
- handle: Handle,
179
- name: z.string().min(1).max(128).refine(storable, "name has a NUL character or a lone surrogate"),
180
- avatar: z.string().url().nullable(),
181
- /**
182
- * (server) A principal a host's `authenticate` made, or one of its profiles: someone the board
183
- * signs in, whatever its ID or kind, so sync never writes as it. Others have none.
184
- */
185
- host: z.literal(true).optional()
186
- }),
76
+ blind: z.literal(true).optional(),
187
77
  /**
188
- * A platform actor (`slack:user:U0456`) is the same person as a principal
189
- * (`vercel:user:…`): for display, `--by`, and mentions, never for permissions.
190
- * `method` says how it was proved: a one-time code posted from that account,
191
- * or the account's own sign-in (`consent`).
78
+ * (server) The principal of the sync elsewhere that imported it, whose copy `origin` names:
79
+ * that sync's to act on. None for the board's own sync, or a write on the board.
192
80
  */
193
- "actor.linked": z.object({
194
- actor: ActorId,
195
- to: ActorId,
196
- method: z.enum(["code", "consent"]),
197
- by: Author
81
+ importedBy: ActorId.optional()
82
+ }),
83
+ /** A message's links changed: an edit linked something new, or the platform sent what a link says. */
84
+ "message.linked": z.object({
85
+ id: MessageId,
86
+ thread: ThreadKey,
87
+ links: z.array(Link).max(10),
88
+ by: Author,
89
+ from: From
90
+ }),
91
+ "message.edited": z.object({
92
+ id: MessageId,
93
+ thread: ThreadKey,
94
+ text: MessageText,
95
+ mentions: z.array(ActorId),
96
+ by: Author,
97
+ from: From
98
+ }),
99
+ "message.deleted": z.object({
100
+ id: MessageId,
101
+ thread: ThreadKey,
102
+ by: Author,
103
+ from: From
104
+ }),
105
+ "reaction.added": z.object({
106
+ id: MessageId,
107
+ thread: ThreadKey,
108
+ emoji: Emoji,
109
+ by: Author,
110
+ from: From
111
+ }),
112
+ "reaction.removed": z.object({
113
+ id: MessageId,
114
+ thread: ThreadKey,
115
+ emoji: Emoji,
116
+ by: Author,
117
+ from: From
118
+ }),
119
+ "labels.changed": z.object({
120
+ id: MessageId,
121
+ thread: ThreadKey,
122
+ set: Labels,
123
+ unset: z.array(LabelKey),
124
+ by: Author
125
+ }),
126
+ /** A thread is synced with an external channel (start a thread there) or thread (join it). */
127
+ "thread.synced": z.object({
128
+ thread: ThreadKey,
129
+ target: ExternalTarget,
130
+ cause: z.enum(["config", "request"]),
131
+ kind: z.enum(["channel", "thread"]).optional(),
132
+ by: Author
133
+ }),
134
+ /** An outbound delivery receipt: board message `id` exists on the platform as `external`. */
135
+ "message.synced": z.object({
136
+ id: MessageId,
137
+ thread: ThreadKey,
138
+ target: ExternalTarget,
139
+ external: z.object({
140
+ thread: ExternalTarget,
141
+ message: z.string().min(1).max(300),
142
+ /** Posted with its author's own platform token, so it shows as them. Older receipts have none. */
143
+ asUser: z.literal(true).optional()
198
144
  }),
199
- "actor.unlinked": z.object({
200
- actor: ActorId,
201
- by: Author
202
- })
203
- }
204
- });
205
- const BOARD_SESSION = "board";
206
- const ExternalAuthor = z.object({
207
- /** The platform principal, e.g. `slack:user:U0456`. */
208
- id: z.string().min(1),
209
- handle: z.string().min(1),
210
- name: z.string().min(1),
211
- avatar: z.string().nullable(),
212
- isBot: z.boolean()
213
- });
214
- /**
215
- * A file on a platform message, as Chat SDK describes it: enough for the
216
- * platform's adapter to fetch it later (`rehydrateAttachment`), never the bytes.
217
- */
218
- const ExternalFile = z.object({
219
- type: z.enum([
220
- "image",
221
- "file",
222
- "video",
223
- "audio"
224
- ]),
225
- name: z.string().max(1e3).optional(),
226
- mimeType: z.string().max(255).optional(),
227
- size: z.number().int().nonnegative().optional(),
228
- url: z.string().max(4e3).optional(),
229
- fetchMetadata: z.record(z.string(), z.string().max(4e3)).optional()
230
- });
231
- /**
232
- * A platform message a platform message links to, at intake: where it lives
233
- * (`thread`, with the sync key prefix, and `message`), and what it said when the
234
- * platform sent that along (Slack's unfurl).
235
- */
236
- const ExternalLink = z.object({
237
- url: z.string().min(1).max(4e3),
238
- thread: ExternalTarget,
239
- message: z.string().min(1).max(300),
240
- snapshot: LinkSnapshot.nullable()
241
- });
242
- const ExternalLocation = z.object({
243
- channel: ExternalTarget,
244
- thread: ExternalTarget,
245
- subthread: ExternalTarget.nullable()
246
- });
247
- /** A platform event, normalized at intake. Thread IDs are external IDs with the sync key prefix. */
248
- const ExternalEvent = z.discriminatedUnion("kind", [
249
- z.object({
250
- kind: z.literal("message"),
251
- ...ExternalLocation.shape,
252
- message: z.string(),
253
- isRoot: z.boolean(),
254
- text: z.string(),
255
- context: z.string().nullable(),
256
- author: ExternalAuthor,
257
- /** Files on the message, fetched when it lands on the board. Older events have none. */
258
- files: z.array(ExternalFile).max(20).optional(),
259
- /** Platform messages it links to. Older events have none. */
260
- links: z.array(ExternalLink).max(10).optional()
145
+ /**
146
+ * (server) The principal of the sync elsewhere that recorded it: its copy is that sync's to
147
+ * change. None for the board's own sync.
148
+ */
149
+ syncedBy: ActorId.optional()
150
+ }),
151
+ /**
152
+ * Board message `id`'s files reached `target` after its text: `files` are the
153
+ * platform's IDs for them, or null when the platform refused files and the
154
+ * text got a line for each instead.
155
+ */
156
+ "message.files_synced": z.object({
157
+ id: MessageId,
158
+ thread: ThreadKey,
159
+ target: ExternalTarget,
160
+ files: z.array(z.string().min(1).max(300)).max(10).nullable(),
161
+ /** (server) As on `message.synced`: only the sync that recorded the copy sets its files. */
162
+ syncedBy: ActorId.optional()
261
163
  }),
262
- z.object({
263
- kind: z.literal("edited"),
264
- ...ExternalLocation.shape,
265
- message: z.string(),
266
- text: z.string(),
267
- author: ExternalAuthor,
268
- links: z.array(ExternalLink).max(10).optional(),
269
- /** Only the links changed (the platform sent what they say), so `text` isn't news. */
270
- linksOnly: z.boolean().optional()
164
+ /** A delivery that gave up: board message `id` didn't reach `target`, and `error` says why. */
165
+ "message.sync_failed": z.object({
166
+ id: MessageId,
167
+ thread: ThreadKey,
168
+ target: ExternalTarget,
169
+ error: z.string().min(1).max(500)
271
170
  }),
272
- z.object({
273
- kind: z.literal("deleted"),
274
- ...ExternalLocation.shape,
275
- message: z.string()
171
+ "actor.seen": z.object({
172
+ actor: ActorId,
173
+ kind: SeenActorKind,
174
+ handle: Handle,
175
+ name: z.string().min(1).max(128).refine(storable, "name has a NUL character or a lone surrogate"),
176
+ avatar: z.string().url().nullable(),
177
+ /**
178
+ * (server) A principal a host's `authenticate` made, or one of its profiles: someone the board
179
+ * signs in, whatever its ID or kind, so sync never writes as it. Others have none.
180
+ */
181
+ host: z.literal(true).optional()
276
182
  }),
277
- z.object({
278
- kind: z.literal("reaction"),
279
- ...ExternalLocation.shape,
280
- message: z.string(),
281
- emoji: z.string(),
282
- added: z.boolean(),
283
- author: ExternalAuthor
183
+ /**
184
+ * A platform actor (`slack:user:U0456`) is the same person as a principal
185
+ * (`vercel:user:…`): for display, `--by`, and mentions, never for permissions.
186
+ * `method` says how it was proved: a one-time code posted from that account,
187
+ * or the account's own sign-in (`consent`).
188
+ */
189
+ "actor.linked": z.object({
190
+ actor: ActorId,
191
+ to: ActorId,
192
+ method: z.enum(["code", "consent"]),
193
+ by: Author
284
194
  }),
285
- z.object({
286
- /** A platform lifecycle event: a PR opening, closing, being reviewed. */
287
- kind: z.literal("lifecycle"),
288
- ...ExternalLocation.omit({ subthread: true }).shape,
289
- type: z.string(),
290
- title: z.string(),
291
- url: z.string().nullable(),
292
- labels: z.record(z.string(), z.string()),
293
- unset: z.array(z.string()),
294
- /** Key prefixes the platform owns on the root, e.g. `tag.`: keys under them that aren't in `labels` are unset. */
295
- owns: z.array(z.string()).optional(),
296
- /** A reply to add to the thread, e.g. "merged by julian". */
297
- reply: z.string().nullable(),
298
- /** Create the root if the board hasn't seen this thread. */
299
- createRoot: z.boolean(),
300
- author: ExternalAuthor
195
+ "actor.unlinked": z.object({
196
+ actor: ActorId,
197
+ by: Author
301
198
  })
302
- ]);
199
+ };
200
+ const BOARD_SESSION = "board";
201
+ //#endregion
202
+ //#region src/contract/contract.ts
203
+ /** The board's one log. Every board fact is an event in session `board`. */
204
+ const boardContract = contract({
205
+ name: "board",
206
+ events: boardEvents
207
+ });
303
208
  /** Raw platform events live outside the board log, one session per external channel. */
304
209
  const inboxContract = contract({
305
210
  name: "board-inbox",
@@ -563,4 +468,72 @@ const identityContract = contract({
563
468
  });
564
469
  const IDENTITY_SESSION = "identity";
565
470
  //#endregion
566
- export { schedulesContract as _, ExternalTarget as a, Link as c, SCHEDULES_SESSION as d, boardContract as f, registryContract as g, inboxSession as h, ExternalLocation as i, Origin as l, inboxContract as m, ExternalAuthor as n, IDENTITY_SESSION as o, identityContract as p, ExternalEvent as r, INBOXES_SESSION as s, BOARD_SESSION as t, REGISTRY_SESSION as u, watchContract as v, watchSession as y };
471
+ //#region src/contract/reducers.ts
472
+ const messagesReducer = reducer({
473
+ contract: boardContract,
474
+ name: "board.messages.v5",
475
+ initialState: {
476
+ byId: {},
477
+ replies: {}
478
+ }
479
+ }).fold(foldMessages);
480
+ reducer({
481
+ contract: boardContract,
482
+ name: "board.posted.v1",
483
+ initialState: {}
484
+ }).fold((state, event) => {
485
+ if (event.type === "message.posted") setOwn(state, event.id, event.index);
486
+ return state;
487
+ });
488
+ const channelsReducer = reducer({
489
+ contract: boardContract,
490
+ name: "board.channels.v1",
491
+ initialState: {}
492
+ }).fold((state, event) => {
493
+ if (event.type !== "message.posted") return state;
494
+ const { channel, re } = event.payload;
495
+ const entry = own(state, channel) ?? setOwn(state, channel, {
496
+ path: channel,
497
+ lastIndex: 0,
498
+ messages: 0,
499
+ threads: 0
500
+ });
501
+ entry.lastIndex = event.index;
502
+ entry.messages += 1;
503
+ if (re === null) entry.threads += 1;
504
+ return state;
505
+ });
506
+ const actorsReducer = reducer({
507
+ contract: boardContract,
508
+ name: "board.actors.v2",
509
+ initialState: {
510
+ byId: {},
511
+ byHandle: {},
512
+ links: {}
513
+ }
514
+ }).fold(foldActors);
515
+ const identityCodesReducer = reducer({
516
+ contract: identityContract,
517
+ name: "board.identity-codes.v1",
518
+ initialState: {}
519
+ }).fold((state, event) => {
520
+ if (event.type === "identity.code_issued") {
521
+ const { hash, principal, sync, expiresAt } = event.payload;
522
+ state[hash] ??= {
523
+ principal,
524
+ sync,
525
+ expiresAt,
526
+ usedBy: null,
527
+ usedIn: null
528
+ };
529
+ } else if (event.type === "identity.code_used") {
530
+ const code = state[event.payload.hash];
531
+ if (code && code.usedBy === null) {
532
+ code.usedBy = event.payload.actor;
533
+ code.usedIn = event.payload.message ?? null;
534
+ }
535
+ }
536
+ return state;
537
+ });
538
+ //#endregion
539
+ export { BOARD_SESSION as _, IDENTITY_SESSION as a, SCHEDULES_SESSION as c, inboxContract as d, inboxSession as f, watchSession as g, watchContract as h, messagesReducer as i, boardContract as l, schedulesContract as m, channelsReducer as n, INBOXES_SESSION as o, registryContract as p, identityCodesReducer as r, REGISTRY_SESSION as s, actorsReducer as t, identityContract as u };
@@ -0,0 +1,19 @@
1
+ //#region ../../node_modules/.pnpm/experimental-a2@0.22.0_@upstash+qstash@2.12.0_@vercel+queue@0.7.0_ai@7.0.130_zod@4.6.5__52060f00b21aecfab049c27fa3abd2f9/node_modules/experimental-a2/dist/retryable-lazy-DZWmHpii.js
2
+ const retryableLazy = (initialize) => {
3
+ let current = null;
4
+ const get = () => {
5
+ if (current) return current;
6
+ const attempt = Promise.resolve().then(initialize);
7
+ current = attempt;
8
+ attempt.catch(() => {
9
+ if (current === attempt) current = null;
10
+ });
11
+ return attempt;
12
+ };
13
+ return {
14
+ get,
15
+ peek: () => current
16
+ };
17
+ };
18
+ //#endregion
19
+ export { retryableLazy as t };
@@ -0,0 +1,69 @@
1
+ //#region ../../node_modules/.pnpm/experimental-a2@0.22.0_@upstash+qstash@2.12.0_@vercel+queue@0.7.0_ai@7.0.130_zod@4.6.5__52060f00b21aecfab049c27fa3abd2f9/node_modules/experimental-a2/dist/scheduler-task-X1SToDjP.js
2
+ const recordOf = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
3
+ const validName = (value) => typeof value === "string" && value.length > 0;
4
+ const validDueAt = (value) => typeof value === "number" && Number.isInteger(value) && value % 1e3 === 0;
5
+ const validIndex = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
6
+ const baseTask = (record) => Object.hasOwn(record, "contract") && Object.hasOwn(record, "sessionId") && validName(record["contract"]) && validName(record["sessionId"]) ? {
7
+ contract: record["contract"],
8
+ sessionId: record["sessionId"]
9
+ } : null;
10
+ /**
11
+ * Validate one delivered task.
12
+ * Adapters call this only after their transport authentication has succeeded.
13
+ */
14
+ function normalizeSchedulerTask(value) {
15
+ const record = recordOf(value);
16
+ if (!record) return null;
17
+ const base = baseTask(record);
18
+ if (!base) return null;
19
+ if (record["version"] !== 1) return null;
20
+ if (record["kind"] === "drain") {
21
+ const dueAt = record["dueAt"];
22
+ if (!validDueAt(dueAt)) return null;
23
+ return {
24
+ version: 1,
25
+ kind: "drain",
26
+ ...base,
27
+ dueAt
28
+ };
29
+ }
30
+ if (record["kind"] === "clear") {
31
+ const { dueAt, id, ifLastIndex } = record;
32
+ if (!validName(id) || !validDueAt(dueAt) || !validIndex(ifLastIndex)) return null;
33
+ return {
34
+ version: 1,
35
+ kind: "clear",
36
+ id,
37
+ ...base,
38
+ dueAt,
39
+ ifLastIndex
40
+ };
41
+ }
42
+ if (record["kind"] !== "append") return null;
43
+ const { dueAt, events, id, originIndex } = record;
44
+ if (!validName(id) || !validDueAt(dueAt)) return null;
45
+ if (originIndex !== void 0 && (!validIndex(originIndex) || originIndex === 0)) return null;
46
+ if (!Array.isArray(events) || events.length === 0) return null;
47
+ const parsedEvents = [];
48
+ for (const event of events) {
49
+ const item = recordOf(event);
50
+ if (!item || typeof item["id"] !== "string" || typeof item["type"] !== "string" || !Object.hasOwn(item, "payload")) return null;
51
+ parsedEvents.push({
52
+ id: item["id"],
53
+ type: item["type"],
54
+ payload: item["payload"]
55
+ });
56
+ }
57
+ if (new Set(parsedEvents.map((event) => event.id)).size !== parsedEvents.length) return null;
58
+ return {
59
+ version: 1,
60
+ kind: "append",
61
+ id,
62
+ ...base,
63
+ dueAt,
64
+ events: parsedEvents,
65
+ ...originIndex === void 0 ? {} : { originIndex }
66
+ };
67
+ }
68
+ //#endregion
69
+ export { normalizeSchedulerTask as t };