@vercel/board 0.29.0 → 0.31.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.
- package/README.md +8 -17
- package/dist/{a2-HUbQJ0RD.mjs → a2-DngWY2Eb.mjs} +741 -415
- package/dist/access-CY5xVMIK.mjs +242 -0
- package/dist/{access-CV96SYgr.d.mts → access-DPcrKIH_.d.mts} +6 -1
- package/dist/access.d.mts +4 -4
- package/dist/access.mjs +5 -4
- package/dist/{actors-DcJhFbwJ.mjs → actors-Bm-ojWWT.mjs} +2 -2
- package/dist/{auth-JO22iIwr.mjs → auth-CyO2aw5n.mjs} +126 -236
- package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
- package/dist/{backend-CPC_vyOo.d.mts → backend-Bob-5CM7.d.mts} +93 -42
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +38 -98
- package/dist/backend-a2/contract.mjs +1 -1
- package/dist/backend-a2/scheduler-local.d.mts +1 -1
- package/dist/backend-a2/scheduler-local.mjs +1 -1
- package/dist/backend-a2/scheduler-qstash.d.mts +3 -3
- package/dist/backend-a2/scheduler-qstash.mjs +8 -8
- package/dist/backend-a2/scheduler-vercel.d.mts +3 -3
- package/dist/backend-a2/scheduler-vercel.mjs +6 -6
- package/dist/backend-a2/store-memory.d.mts +2 -2
- package/dist/backend-a2/store-memory.mjs +3 -3
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +34 -15
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +6 -6
- package/dist/backend-a2/store-redis.d.mts +2 -2
- package/dist/backend-a2/store-redis.mjs +5 -5
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +3 -3
- package/dist/backend-a2.d.mts +143 -242
- package/dist/backend-a2.mjs +2 -2
- package/dist/{backend-http-CqfKhrnn.d.mts → backend-http-BRrRbjRx.d.mts} +59 -45
- package/dist/{backend-http-CzIJMm58.mjs → backend-http-CvUKOn-m.mjs} +11 -10
- package/dist/{backend-http-Bmp0IgD3.mjs → backend-http-D8mUzuKQ.mjs} +2 -2
- package/dist/backend-http.d.mts +1 -1
- package/dist/backend-http.mjs +1 -1
- package/dist/backend-http.node.d.mts +1 -1
- package/dist/backend-http.node.mjs +1 -1
- package/dist/{backend-vercel-DQc_TaHp.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
- package/dist/{backend-vercel-4tvcPKqX.d.mts → backend-vercel-BaSEUdwg.d.mts} +62 -47
- package/dist/{backend-vercel-BNauAs7w.mjs → backend-vercel-CmY9yZ9m.mjs} +279 -42
- package/dist/backend-vercel.d.mts +1 -1
- package/dist/backend-vercel.mjs +1 -1
- package/dist/backend-vercel.node.d.mts +1 -1
- package/dist/backend-vercel.node.mjs +1 -1
- package/dist/{blob-DmiQjrYy.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
- package/dist/{blob-DdbpJqN1.mjs → blob-WbB2DJKn.mjs} +30 -11
- package/dist/{chat-provider-BWHbChcQ.d.mts → chat-provider-BChYrrA7.d.mts} +32 -28
- package/dist/{chat-provider-COtsU5jB.mjs → chat-provider-DkFp2hrh.mjs} +105 -60
- package/dist/cli-commands.d.mts +1 -1
- package/dist/cli-commands.mjs +1 -1
- package/dist/cli-embedded.d.mts +2 -2
- package/dist/cli-embedded.mjs +1 -1
- package/dist/cli.mjs +4 -4
- package/dist/{client-BqAjk0Q4.d.mts → client-COfYrgfV.d.mts} +240 -227
- package/dist/{commands-D6gvrOh3.d.mts → commands-DK4W3HCJ.d.mts} +47 -10
- package/dist/{commands-C3R1AWgm.mjs → commands-L1JSGz2Z.mjs} +212 -54
- package/dist/conformance-DK43HiHf.mjs +1000 -0
- package/dist/conformance.d.mts +8 -2
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-DQxXk7dX.mjs → engine-DurYIRt8.mjs} +29 -210
- package/dist/{errors-DDktVeUa.d.mts → errors-2vGs49Db.d.mts} +1 -1
- package/dist/{client-D0Px7KzF.mjs → errors-CJXhhzvT.mjs} +132 -27
- package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
- package/dist/{exports-BG-0Wyn-.mjs → exports-CWIdN0Ub.mjs} +195 -103
- package/dist/exports-FBOQWT7O.d.mts +5 -0
- package/dist/{folds-BDMSxBUf.mjs → folds-Cu9Q9zyQ.mjs} +3 -22
- package/dist/{folds-ZkHM4M-v.d.mts → folds-DLUzfibJ.d.mts} +15 -40
- package/dist/{index-D3VvNfh_.d.mts → index-Ho-ayyQL.d.mts} +2 -2
- package/dist/index.d.mts +7 -7
- package/dist/{index.default-rB8sJ9RX.mjs → index.default-BUcwDaAe.mjs} +1 -1
- package/dist/index.mjs +5 -6
- package/dist/index.node.d.mts +7 -7
- package/dist/index.node.mjs +3 -4
- package/dist/inspect/app.js +62 -61
- package/dist/inspect/engine.js +3 -3
- package/dist/{internal-CvsrprnU-CRY89I-b.mjs → internal-CvsrprnU-zZMpDiRY.mjs} +1 -1
- package/dist/{local-DhkrAqPP.mjs → local-C76Fy5ad.mjs} +1 -1
- package/dist/{main-CVBqe3VO.mjs → main-Dz-8komG.mjs} +251 -127
- package/dist/{middleware-CXjEeef4.mjs → middleware-BavYSDUa.mjs} +1 -1
- package/dist/{node-Zd_C-gxP.mjs → node-BgeGcwX0.mjs} +7 -3
- package/dist/{platform-B4TnJtWu-CwcTyKg2.mjs → platform-B4TnJtWu-Do3agKr5.mjs} +1 -1
- package/dist/{project-CBXlLftU.mjs → project-CQ4VUibA.mjs} +1 -1
- package/dist/{provider-DmSTp9lI.d.mts → provider-gynDUoyW.d.mts} +13 -43
- package/dist/{redis-tables-BJQVUspu.mjs → redis-tables-DHf85ixI.mjs} +40 -42
- package/dist/{reducer-CjmvZSAb-D_OSISmj.mjs → reducer-Bswr11ar-P324o9vn.mjs} +2 -2
- package/dist/{reducer-CYPHsSmd-BQIfozGi.d.mts → reducer-CYPHsSmd-BXjGiptm.d.mts} +1 -1
- package/dist/{reducers-DJzVpXwM.mjs → reducers-DcENwtwZ.mjs} +74 -77
- package/dist/{retryable-lazy-DZWmHpii-Bye-W8q7.mjs → retryable-lazy-DZWmHpii-DpTc5PIP.mjs} +1 -1
- package/dist/{scheduler-task-X1SToDjP-BVoWuInI.mjs → scheduler-task-X1SToDjP-D2Il2rln.mjs} +1 -1
- package/dist/{serve-G9RuaDSr.mjs → serve-CTmow-YH.mjs} +7 -7
- package/dist/{server-qgez0Xny-D5mLfqFQ.d.mts → server-B9gu0qi1-DI1WvcVK.d.mts} +10 -4
- package/dist/{server-BixV_byP.mjs → server-DG8D6VV0.mjs} +4 -4
- package/dist/{server-DtFrWxi3-Boa6gdWO.mjs → server-DX7mM6nm-D7N_7bug.mjs} +32 -23
- package/dist/{serving-B75Y4BF-.mjs → serving-DBFRo6U_.mjs} +1 -1
- package/dist/{session-clear-wHd5GCpZ-BnKSxReK.mjs → session-clear-XJYpmUpg-2q8l_FJR.mjs} +14 -2
- package/dist/{sql-D4Z_efJX.mjs → sql-CT5xVuGr.mjs} +2 -8
- package/dist/{sqlite-db-Bz2JkfCU.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
- package/dist/{store-D0MhBvg6-4Z2L0Kqd.d.mts → store-DEzcHZOA-B-5vhxef.d.mts} +12 -2
- package/dist/store-N8PXxDAS-DWWgv-Vz.mjs +5 -0
- package/dist/{store-codec-DwrWlgQx-CYxGW0aD.mjs → store-codec-pAG2oPgQ-SqDXlPBR.mjs} +4 -4
- package/dist/store-memory-4IBl0Ztu.mjs +2 -0
- package/dist/{store-memory-Bas9Uq5C.mjs → store-memory-B0PkVSuQ.mjs} +14 -7
- package/dist/{store-polling-BZt2Rf3D-BiEM6_HB.mjs → store-polling-BZt2Rf3D-Div-bJ_D.mjs} +2 -2
- package/dist/{store-presence-polling-JuAy2Ck2-CYpuK75X.mjs → store-presence-polling-JuAy2Ck2-Dh-g_KYM.mjs} +3 -3
- package/dist/{store-sqlite-CxxPCcCT.mjs → store-sqlite-D85t3tE7.mjs} +14 -7
- package/dist/store-sqlite-iBTYYKBj.mjs +2 -0
- package/dist/{stream-buffer-DrTyiIuo-BKy-vI14.mjs → stream-buffer-DrTyiIuo-Be10yOBR.mjs} +2 -2
- package/dist/{sync-kapso.d.mts → sync-chat-sdk.d.mts} +33 -37
- package/dist/sync-chat-sdk.mjs +44 -0
- package/dist/sync-github.d.mts +5 -18
- package/dist/sync-github.mjs +4 -70
- package/dist/sync-linear.d.mts +3 -3
- package/dist/sync-linear.mjs +6 -5
- package/dist/sync-slack.d.mts +7 -7
- package/dist/sync-slack.mjs +33 -54
- package/dist/sync.d.mts +3 -3
- package/dist/sync.mjs +2 -4
- package/dist/{tokens-CC3izSNr.mjs → tokens-Dnyd_y1n.mjs} +15 -6
- package/dist/{types-No0WdhE5.d.mts → types-CmrFjpml.d.mts} +2 -4
- package/dist/{watchers-hash-BDhLVkV5.mjs → watchers-hash-BjwlnGD9.mjs} +124 -115
- package/docs/01-board/01-mounting.md +2 -2
- package/docs/01-board/03-identity.md +5 -6
- package/docs/01-board/04-messages.md +96 -2
- package/docs/01-board/05-reading.md +2 -1
- package/docs/01-board/06-watchers.md +8 -6
- package/docs/01-board/08-middleware.md +6 -3
- package/docs/01-board/09-access.md +145 -14
- package/docs/02-sync/01-overview.md +19 -26
- package/docs/02-sync/02-functions.md +8 -9
- package/docs/02-sync/03-slack.md +5 -5
- package/docs/02-sync/04-github.md +2 -2
- package/docs/02-sync/06-providers.md +91 -62
- package/docs/03-guides/01-coding-agents.md +1 -1
- package/docs/03-guides/03-ci.md +7 -4
- package/docs/04-reference/01-cli.md +109 -51
- package/docs/04-reference/02-board.md +76 -28
- package/docs/04-reference/03-http.md +76 -38
- package/docs/04-reference/04-log.md +8 -11
- package/docs/04-reference/05-conformance.md +47 -7
- package/docs/index.md +2 -2
- package/package.json +7 -10
- package/sql/postgres/board.sql +1 -1
- package/sql/postgres/schema.sql +1 -1
- package/dist/conformance-DlnfuzEB.mjs +0 -556
- package/dist/errors-CusN4x2l.mjs +0 -88
- package/dist/events-qf5XG5VQ.mjs +0 -75
- package/dist/exports-mhAYiU4i.d.mts +0 -5
- package/dist/store-N8PXxDAS-BWBgzBHp.mjs +0 -5
- package/dist/store-memory-ZLiZbH3R.mjs +0 -2
- package/dist/store-sqlite-BmwL3pUR.mjs +0 -2
- package/dist/sync-kapso.mjs +0 -89
- package/dist/sync.chat.d.mts +0 -2
- package/dist/sync.chat.mjs +0 -2
- package/dist/users-yryHKFMo.mjs +0 -42
- package/docs/02-sync/05-whatsapp.md +0 -152
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
|
|
2
|
-
import { S as storable, a as AttachmentId, c as ChannelPath, d as LabelKey, g as MessageText, l as Emoji, m as MESSAGE_TEXT_MAX,
|
|
2
|
+
import { S as storable, a as AttachmentId, c as ChannelPath, d as LabelKey, f as LabelValue, g as MessageText, l as Emoji, m as MESSAGE_TEXT_MAX, p as Labels, x as platformAvatar } from "./schemas-DBmUO6pr.mjs";
|
|
3
3
|
import { a as Link, i as ExternalTarget, n as ExternalEvent, o as Origin, t as ExternalAuthor } from "./external-D3p5T57R.mjs";
|
|
4
|
-
import { r as badRequest, t as HttpError } from "./errors-
|
|
5
|
-
import { o as options } from "./commands-
|
|
4
|
+
import { r as badRequest, t as HttpError } from "./errors-CJXhhzvT.mjs";
|
|
5
|
+
import { o as options } from "./commands-L1JSGz2Z.mjs";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
//#region src/api/routes.ts
|
|
8
8
|
/**
|
|
9
9
|
* A message's, schedule's, or cursor's ID: a string clients pass back as they got it, and never
|
|
10
|
-
* parse, compare, or count with. Each backend picks its own; a board on `a2()` writes decimals
|
|
10
|
+
* parse, compare, or count with. Each backend picks its own; a board on `a2()` writes decimals,
|
|
11
|
+
* and `s12` for a schedule.
|
|
11
12
|
*/
|
|
12
13
|
const Ref = z.string().min(1).max(128);
|
|
14
|
+
/** The message's `revision` as it was read: if it's been edited or deleted since, 409 `revision_conflict`. */
|
|
15
|
+
const Revision = z.number().int().positive();
|
|
13
16
|
const id = Ref;
|
|
17
|
+
/** A boolean query parameter: `true` or `1`, `false` or `0`. */
|
|
18
|
+
const flag = z.enum([
|
|
19
|
+
"true",
|
|
20
|
+
"false",
|
|
21
|
+
"1",
|
|
22
|
+
"0"
|
|
23
|
+
]).transform((value) => value === "true" || value === "1");
|
|
14
24
|
const repeated = z.union([z.string(), z.array(z.string())]).transform((value) => Array.isArray(value) ? value : [value]);
|
|
15
25
|
/**
|
|
16
26
|
* `pr` → `['pr', null]` (has the label), `status=open` → `['status', ['open']]`,
|
|
@@ -50,7 +60,7 @@ const ThreadsQuery = Filters.extend({
|
|
|
50
60
|
const EventsQuery = Filters.extend({
|
|
51
61
|
after: Ref.optional(),
|
|
52
62
|
wait: z.coerce.number().min(0).max(300).optional(),
|
|
53
|
-
all:
|
|
63
|
+
all: flag.optional()
|
|
54
64
|
});
|
|
55
65
|
const PostBody = z.object({
|
|
56
66
|
channel: ChannelPath.optional(),
|
|
@@ -66,11 +76,19 @@ const PostBody = z.object({
|
|
|
66
76
|
sync: z.array(z.union([z.string().min(3).max(300), ResolvedSyncTarget])).max(10).optional(),
|
|
67
77
|
id: z.string().min(1).max(200).refine(storable, "id has a NUL character or a lone surrogate").optional(),
|
|
68
78
|
/** IDs from `POST /attachments`. */
|
|
69
|
-
attachments: z.array(AttachmentId).max(10).optional()
|
|
79
|
+
attachments: z.array(AttachmentId).max(10).optional(),
|
|
80
|
+
/**
|
|
81
|
+
* A reply's precondition: a message in its thread, the newest as the caller read it. If another
|
|
82
|
+
* has come after it, nothing is posted, and the board answers 409 `newer_message`.
|
|
83
|
+
*/
|
|
84
|
+
after: Ref.optional()
|
|
70
85
|
}).refine((body) => body.text.trim().length > 0 || (body.attachments?.length ?? 0) > 0, {
|
|
71
86
|
message: "message text is empty",
|
|
72
87
|
path: ["text"]
|
|
73
|
-
}).refine((body) => body.channel === void 0 !== (body.re === void 0), { message: "a post needs `channel`; a reply needs `re`" }).refine((body) => !(body.noSync && body.sync?.length), { message: "`noSync` and `sync` contradict each other" })
|
|
88
|
+
}).refine((body) => body.channel === void 0 !== (body.re === void 0), { message: "a post needs `channel`; a reply needs `re`" }).refine((body) => !(body.noSync && body.sync?.length), { message: "`noSync` and `sync` contradict each other" }).refine((body) => body.after === void 0 || body.re !== void 0, {
|
|
89
|
+
message: "`after` is for replies: it names the newest message in `re`'s thread",
|
|
90
|
+
path: ["after"]
|
|
91
|
+
});
|
|
74
92
|
/** `30m`, `1d`: a number and a unit, `s`, `m`, `h`, or `d`. */
|
|
75
93
|
const Duration = z.string().regex(/^\d+(\.\d+)?(s|m|h|d)$/, "durations look like 30s, 30m, 12h, or 1d");
|
|
76
94
|
const Instant = z.string().datetime({ offset: true });
|
|
@@ -108,7 +126,27 @@ const SchedulesQuery = z.object({
|
|
|
108
126
|
/** An actor ID or handle: the schedules it created. */
|
|
109
127
|
by: z.string().optional(),
|
|
110
128
|
/** Include done and canceled schedules. */
|
|
111
|
-
all:
|
|
129
|
+
all: flag.optional()
|
|
130
|
+
});
|
|
131
|
+
const CanGrant = z.union([z.literal("all"), z.array(z.string().min(1).max(16)).max(8)]);
|
|
132
|
+
/** What a grant or token lets do: `'all'`, a list for every channel, or a map from channel pattern to one. board() checks the patterns. */
|
|
133
|
+
const Can = z.union([CanGrant, z.record(z.string().min(1).max(300), CanGrant)]);
|
|
134
|
+
const ClaimValue = z.string().min(1).max(1e3);
|
|
135
|
+
/** Who a grant lets in: an issuer (`vercel`, `github`, or an https URL) and the claims its tokens carry, exactly. */
|
|
136
|
+
const GrantBody = z.object({
|
|
137
|
+
issuer: z.string().min(1).max(1e3),
|
|
138
|
+
claims: z.record(z.string().min(1).max(200), z.union([ClaimValue, z.array(ClaimValue).min(1).max(32)])),
|
|
139
|
+
can: Can.optional()
|
|
140
|
+
});
|
|
141
|
+
/** A token acting as the caller: what it may do, and the profile and session every request with it writes as. */
|
|
142
|
+
const TokenBody = z.object({
|
|
143
|
+
name: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/, "a token name is up to 64 letters, numbers, and ._-"),
|
|
144
|
+
can: Can.optional(),
|
|
145
|
+
profile: z.string().regex(/^[A-Za-z][A-Za-z0-9-]{0,31}$/, "a profile is up to 32 letters, numbers, and hyphens").optional(),
|
|
146
|
+
avatar: z.string().url().max(2048).optional(),
|
|
147
|
+
session: z.string().regex(/^[A-Za-z0-9_.:-]{1,128}$/, "a session is up to 128 letters, numbers, and _.:-").optional(),
|
|
148
|
+
/** How long it lives: `30d` by default, at most `90d`. */
|
|
149
|
+
expires: Duration.optional()
|
|
112
150
|
});
|
|
113
151
|
const SyncBody = z.object({ target: z.string().min(3).max(300) });
|
|
114
152
|
const InboxQuery = z.object({
|
|
@@ -123,12 +161,11 @@ const Registering = z.object({
|
|
|
123
161
|
})).refine((watch) => Object.keys(watch).length <= 32, `a board registers at most 32 watchers`),
|
|
124
162
|
/**
|
|
125
163
|
* Its sync entries: its sync subscribes too, to every post, change, and thread sync in the channels
|
|
126
|
-
* where it may sync, and the board lists the entries
|
|
164
|
+
* where it may sync, and the board lists the entries.
|
|
127
165
|
*/
|
|
128
166
|
sync: z.array(z.object({
|
|
129
167
|
key: z.string().min(1).max(64),
|
|
130
|
-
platform: z.string().min(1).max(64)
|
|
131
|
-
signIn: z.boolean().default(false)
|
|
168
|
+
platform: z.string().min(1).max(64)
|
|
132
169
|
})).max(32).optional()
|
|
133
170
|
});
|
|
134
171
|
/**
|
|
@@ -180,7 +217,6 @@ const SourceReadQuery = z.object({
|
|
|
180
217
|
const SourceJoinBody = z.object({ target: z.string().min(1).max(4e3) });
|
|
181
218
|
/** A platform's link to a message or thread, such as a Slack permalink or a GitHub comment's URL. */
|
|
182
219
|
const LinkedQuery = z.object({ url: z.string().url().max(4e3) });
|
|
183
|
-
const IdentityUnlinkBody = z.object({ actor: z.string().min(1).max(256) });
|
|
184
220
|
/** A sync entry's key: `slack`, `github`. */
|
|
185
221
|
const SyncKey = z.string().min(1).max(64);
|
|
186
222
|
/** A platform message an import changes, by its platform thread and ID; without a message, the root of the board thread the platform thread belongs to. */
|
|
@@ -236,19 +272,23 @@ const SyncImportBody = z.discriminatedUnion("op", [
|
|
|
236
272
|
noSync: z.boolean().optional(),
|
|
237
273
|
attachments: z.array(AttachmentId).max(10).optional(),
|
|
238
274
|
links: z.array(ImportLink).max(10).optional(),
|
|
239
|
-
id: z.string().min(1).max(200).refine(storable, "id has a NUL character or a lone surrogate").optional()
|
|
275
|
+
id: z.string().min(1).max(200).refine(storable, "id has a NUL character or a lone surrogate").optional(),
|
|
276
|
+
/** A reply's precondition, as a post's: a sync callback's reply after the newest message it read. */
|
|
277
|
+
after: Ref.optional()
|
|
240
278
|
}),
|
|
241
279
|
z.object({
|
|
242
280
|
op: z.literal("edit"),
|
|
243
281
|
...Imported,
|
|
244
282
|
...Changed,
|
|
245
283
|
text: MessageText,
|
|
246
|
-
source: ImportSource.optional()
|
|
284
|
+
source: ImportSource.optional(),
|
|
285
|
+
revision: Revision.optional()
|
|
247
286
|
}),
|
|
248
287
|
z.object({
|
|
249
288
|
op: z.literal("delete"),
|
|
250
289
|
...Imported,
|
|
251
|
-
...Changed
|
|
290
|
+
...Changed,
|
|
291
|
+
revision: Revision.optional()
|
|
252
292
|
}),
|
|
253
293
|
z.object({
|
|
254
294
|
op: z.literal("react"),
|
|
@@ -262,7 +302,9 @@ const SyncImportBody = z.discriminatedUnion("op", [
|
|
|
262
302
|
...Imported,
|
|
263
303
|
...Changed,
|
|
264
304
|
set: Labels.optional(),
|
|
265
|
-
unset: z.array(LabelKey).optional()
|
|
305
|
+
unset: z.array(LabelKey).optional(),
|
|
306
|
+
/** As a label write's: what the labels must be now, a value or null for none. */
|
|
307
|
+
if: z.record(LabelKey, LabelValue.nullable()).optional()
|
|
266
308
|
}),
|
|
267
309
|
z.object({
|
|
268
310
|
op: z.literal("links"),
|
|
@@ -284,8 +326,7 @@ const SyncReceiptBody = z.discriminatedUnion("kind", [
|
|
|
284
326
|
target: ExternalTarget,
|
|
285
327
|
external: z.object({
|
|
286
328
|
thread: ExternalTarget,
|
|
287
|
-
message: z.string().min(1).max(300)
|
|
288
|
-
asUser: z.literal(true).optional()
|
|
329
|
+
message: z.string().min(1).max(300)
|
|
289
330
|
}),
|
|
290
331
|
/** Its files, when they went with it: the platform's IDs, or null when it refused them. */
|
|
291
332
|
files: PlatformFiles.optional()
|
|
@@ -312,24 +353,15 @@ const SyncReceiptBody = z.discriminatedUnion("kind", [
|
|
|
312
353
|
cause: z.enum(["config", "request"])
|
|
313
354
|
})
|
|
314
355
|
]);
|
|
315
|
-
/** What sync looks up by platform ID: a thread or
|
|
356
|
+
/** What sync looks up by platform ID: a thread or a message. */
|
|
316
357
|
const SyncFindQuery = z.object({
|
|
317
|
-
/** A live one-time code: who it was issued to, for a sign-in that carries it. */
|
|
318
|
-
code: z.string().min(1).max(64).optional(),
|
|
319
358
|
/** The post the caller's sync imported with this client ID. */
|
|
320
359
|
post: z.string().min(1).max(200).optional(),
|
|
321
360
|
thread: ExternalTarget.optional(),
|
|
322
361
|
/** With `thread`: the platform message in it. */
|
|
323
|
-
message: z.string().min(1).max(300).optional()
|
|
324
|
-
account: ActorId.optional(),
|
|
325
|
-
person: ActorId.optional()
|
|
362
|
+
message: z.string().min(1).max(300).optional()
|
|
326
363
|
});
|
|
327
364
|
const SyncCopiesQuery = z.object({ message: repeated.pipe(z.array(Ref).min(1).max(100)) });
|
|
328
|
-
/** A person's own roots since `since`, in epoch milliseconds. */
|
|
329
|
-
const SyncRootsQuery = z.object({
|
|
330
|
-
by: ActorId,
|
|
331
|
-
since: z.coerce.number().int().nonnegative()
|
|
332
|
-
});
|
|
333
365
|
/** Platform events for a sync key's inbox, each with the platform's own ID for it, which dedupes it. */
|
|
334
366
|
const InboxPushBody = z.object({ events: z.array(z.object({
|
|
335
367
|
id: z.string().min(1).max(300),
|
|
@@ -341,47 +373,41 @@ const InboxAckBody = z.object({
|
|
|
341
373
|
lease: z.string().min(1).max(64),
|
|
342
374
|
error: z.string().min(1).max(500).optional()
|
|
343
375
|
});
|
|
344
|
-
/** A one-time code for linking an account on sync entry `sync`: a person's own, for themselves. */
|
|
345
|
-
const SyncCodeBody = z.object({ sync: SyncKey });
|
|
346
376
|
/**
|
|
347
|
-
* A
|
|
348
|
-
*
|
|
377
|
+
* A direct upload, before its bytes: what the file will be called, its type, its exact size, and
|
|
378
|
+
* the SHA-256 of its bytes, in base64, for a store that signs its URL for them (Vercel's).
|
|
349
379
|
*/
|
|
350
|
-
const SyncAccountBody = z.object({
|
|
351
|
-
sync: SyncKey,
|
|
352
|
-
account: PlatformUser,
|
|
353
|
-
code: z.string().min(1).max(64),
|
|
354
|
-
message: z.string().max(1024).optional(),
|
|
355
|
-
/** `consent` when the code came through the account's own sign-in, so the board posts as them. */
|
|
356
|
-
method: z.enum(["code", "consent"]).optional()
|
|
357
|
-
});
|
|
358
|
-
/** A sign-in that carries a code, on the board whose sync runs the entry. */
|
|
359
|
-
const SignInQuery = z.object({
|
|
360
|
-
sync: SyncKey,
|
|
361
|
-
code: z.string().min(1).max(64)
|
|
362
|
-
});
|
|
363
|
-
const SyncUnlinkBody = z.object({ account: ActorId });
|
|
364
|
-
/** A direct upload, before its bytes: what the file will be called, its type, and its exact size. */
|
|
365
380
|
const UploadStartBody = z.object({
|
|
366
381
|
name: z.string().max(1024).nullable().optional(),
|
|
367
382
|
type: z.string().max(1024).nullable().optional(),
|
|
368
|
-
size: z.number().int().positive().max(DIRECT_UPLOAD_BYTES_MAX, `direct uploads are at most ${DIRECT_UPLOAD_BYTES_MAX / 1024 / 1024} MB`)
|
|
383
|
+
size: z.number().int().positive().max(DIRECT_UPLOAD_BYTES_MAX, `direct uploads are at most ${DIRECT_UPLOAD_BYTES_MAX / 1024 / 1024} MB`),
|
|
384
|
+
sha256: z.string().regex(/^[A-Za-z0-9+/]{43}=$/, "sha256 is the base64 SHA-256 of the bytes").optional()
|
|
369
385
|
});
|
|
370
|
-
const EditBody = z.object({
|
|
371
|
-
|
|
372
|
-
|
|
386
|
+
const EditBody = z.object({
|
|
387
|
+
text: MessageText,
|
|
388
|
+
revision: Revision.optional()
|
|
389
|
+
});
|
|
390
|
+
const DeleteBody = z.object({ revision: Revision.optional() }).optional();
|
|
391
|
+
/** `status`: what Slack shows in place of "Typing…" (other platforms ignore it). */
|
|
373
392
|
const TypingBody = z.object({ status: z.string().trim().min(1).max(100).optional() }).optional();
|
|
374
393
|
const ReactBody = z.object({
|
|
375
394
|
emoji: Emoji,
|
|
376
395
|
remove: z.boolean().optional()
|
|
377
396
|
});
|
|
397
|
+
/** A channel to make before anything posts to it. */
|
|
398
|
+
const CreateChannelBody = z.object({ path: ChannelPath });
|
|
378
399
|
const LabelBody = z.object({
|
|
379
400
|
set: Labels.optional(),
|
|
380
|
-
unset: z.array(LabelKey).optional()
|
|
401
|
+
unset: z.array(LabelKey).optional(),
|
|
402
|
+
/**
|
|
403
|
+
* What the message's labels must be now: a key's value, or null for one that mustn't be set.
|
|
404
|
+
* If one isn't, nothing changes, and the board answers 409 `label_conflict`.
|
|
405
|
+
*/
|
|
406
|
+
if: z.record(LabelKey, LabelValue.nullable()).refine((expected) => Object.keys(expected).length <= 16, `at most 16 labels`).optional()
|
|
381
407
|
}).refine((body) => body.set !== void 0 || body.unset !== void 0, { message: "nothing to change: pass `set` or `unset`" });
|
|
382
408
|
/** The route table: method, path, and the schemas that validate input. */
|
|
383
409
|
const routes = {
|
|
384
|
-
/** Where the board is, its project, and
|
|
410
|
+
/** Where the board is, its project, and its sync entries. No token. */
|
|
385
411
|
discovery: {
|
|
386
412
|
method: "GET",
|
|
387
413
|
path: "/"
|
|
@@ -394,6 +420,12 @@ const routes = {
|
|
|
394
420
|
method: "GET",
|
|
395
421
|
path: "/channels"
|
|
396
422
|
},
|
|
423
|
+
/** Makes a channel before anything posts to it: needs `read` and `post` there. One that's there already is left as it is. */
|
|
424
|
+
createChannel: {
|
|
425
|
+
method: "POST",
|
|
426
|
+
path: "/channels",
|
|
427
|
+
body: CreateChannelBody
|
|
428
|
+
},
|
|
397
429
|
actors: {
|
|
398
430
|
method: "GET",
|
|
399
431
|
path: "/actors"
|
|
@@ -477,6 +509,34 @@ const routes = {
|
|
|
477
509
|
method: "POST",
|
|
478
510
|
path: "/schedules/:id/cancel"
|
|
479
511
|
},
|
|
512
|
+
/** Grants need manage in every channel. Setting one with the same issuer and claims changes its `can`. */
|
|
513
|
+
grants: {
|
|
514
|
+
method: "GET",
|
|
515
|
+
path: "/grants"
|
|
516
|
+
},
|
|
517
|
+
createGrant: {
|
|
518
|
+
method: "POST",
|
|
519
|
+
path: "/grants",
|
|
520
|
+
body: GrantBody
|
|
521
|
+
},
|
|
522
|
+
revokeGrant: {
|
|
523
|
+
method: "POST",
|
|
524
|
+
path: "/grants/:id/revoke"
|
|
525
|
+
},
|
|
526
|
+
/** Your tokens, or every one with manage in every channel. A token acts as whoever minted it. */
|
|
527
|
+
tokens: {
|
|
528
|
+
method: "GET",
|
|
529
|
+
path: "/tokens"
|
|
530
|
+
},
|
|
531
|
+
createToken: {
|
|
532
|
+
method: "POST",
|
|
533
|
+
path: "/tokens",
|
|
534
|
+
body: TokenBody
|
|
535
|
+
},
|
|
536
|
+
revokeToken: {
|
|
537
|
+
method: "POST",
|
|
538
|
+
path: "/tokens/:id/revoke"
|
|
539
|
+
},
|
|
480
540
|
/** The caller's own post with the client ID it gave: what `POST /messages` answered, for telling a retry before posting. */
|
|
481
541
|
posted: {
|
|
482
542
|
method: "GET",
|
|
@@ -535,30 +595,6 @@ const routes = {
|
|
|
535
595
|
path: "/sync/copies",
|
|
536
596
|
query: SyncCopiesQuery
|
|
537
597
|
},
|
|
538
|
-
/** A person's recent roots, for telling their own platform copies from new messages. */
|
|
539
|
-
syncRoots: {
|
|
540
|
-
method: "GET",
|
|
541
|
-
path: "/sync/roots",
|
|
542
|
-
query: SyncRootsQuery
|
|
543
|
-
},
|
|
544
|
-
/** A one-time code for linking a platform account. */
|
|
545
|
-
syncCode: {
|
|
546
|
-
method: "POST",
|
|
547
|
-
path: "/sync/codes",
|
|
548
|
-
body: SyncCodeBody
|
|
549
|
-
},
|
|
550
|
-
/** Links a platform account, by a code or for a person. */
|
|
551
|
-
syncAccount: {
|
|
552
|
-
method: "POST",
|
|
553
|
-
path: "/sync/accounts",
|
|
554
|
-
body: SyncAccountBody
|
|
555
|
-
},
|
|
556
|
-
/** Unlinks a platform account. */
|
|
557
|
-
syncUnlink: {
|
|
558
|
-
method: "POST",
|
|
559
|
-
path: "/sync/accounts/unlink",
|
|
560
|
-
body: SyncUnlinkBody
|
|
561
|
-
},
|
|
562
598
|
/** Reads a platform channel or thread on demand, where that sync runs, without syncing or storing it. Full access only. */
|
|
563
599
|
readSource: {
|
|
564
600
|
method: "GET",
|
|
@@ -608,28 +644,6 @@ const routes = {
|
|
|
608
644
|
method: "GET",
|
|
609
645
|
path: "/openapi.json"
|
|
610
646
|
},
|
|
611
|
-
/** Your linked platform accounts. */
|
|
612
|
-
identity: {
|
|
613
|
-
method: "GET",
|
|
614
|
-
path: "/identity"
|
|
615
|
-
},
|
|
616
|
-
/** Starts a sign-in that carries a code (no token), on the board that runs the entry's sync. */
|
|
617
|
-
signIn: {
|
|
618
|
-
method: "GET",
|
|
619
|
-
path: "/identity/sign-in",
|
|
620
|
-
query: SignInQuery
|
|
621
|
-
},
|
|
622
|
-
/** Where a sign-in comes back (no token): links the account it authorized to the code's person. */
|
|
623
|
-
signedIn: {
|
|
624
|
-
method: "GET",
|
|
625
|
-
path: "/identity/signed-in",
|
|
626
|
-
query: SignInQuery
|
|
627
|
-
},
|
|
628
|
-
identityUnlink: {
|
|
629
|
-
method: "POST",
|
|
630
|
-
path: "/identity/unlink",
|
|
631
|
-
body: IdentityUnlinkBody
|
|
632
|
-
},
|
|
633
647
|
/** Watchers registered by boards on `http()`: every one to `manage` everywhere, the caller's own to anyone else. */
|
|
634
648
|
watchers: {
|
|
635
649
|
method: "GET",
|
|
@@ -664,7 +678,7 @@ const routes = {
|
|
|
664
678
|
const SYNC = "@sync";
|
|
665
679
|
//#endregion
|
|
666
680
|
//#region src/version.ts
|
|
667
|
-
const VERSION = "0.
|
|
681
|
+
const VERSION = "0.31.0";
|
|
668
682
|
//#endregion
|
|
669
683
|
//#region src/server/routing.ts
|
|
670
684
|
/** board()'s own routes: the files it serves, and sync's, which run where board() does. */
|
|
@@ -672,9 +686,6 @@ const BOARD_ROUTES = [
|
|
|
672
686
|
"avatar",
|
|
673
687
|
"openapi",
|
|
674
688
|
"webhook",
|
|
675
|
-
"signIn",
|
|
676
|
-
"signedIn",
|
|
677
|
-
"identityUnlink",
|
|
678
689
|
"sync"
|
|
679
690
|
];
|
|
680
691
|
/** Every protocol method, by the route it's served at. */
|
|
@@ -682,8 +693,8 @@ const METHODS = [
|
|
|
682
693
|
"discovery",
|
|
683
694
|
"me",
|
|
684
695
|
"channels",
|
|
696
|
+
"createChannel",
|
|
685
697
|
"actors",
|
|
686
|
-
"identity",
|
|
687
698
|
"messages",
|
|
688
699
|
"threads",
|
|
689
700
|
"message",
|
|
@@ -696,6 +707,12 @@ const METHODS = [
|
|
|
696
707
|
"schedule",
|
|
697
708
|
"schedules",
|
|
698
709
|
"cancelSchedule",
|
|
710
|
+
"grants",
|
|
711
|
+
"createGrant",
|
|
712
|
+
"revokeGrant",
|
|
713
|
+
"tokens",
|
|
714
|
+
"createToken",
|
|
715
|
+
"revokeToken",
|
|
699
716
|
"upload",
|
|
700
717
|
"attachment",
|
|
701
718
|
"uploadStart",
|
|
@@ -710,14 +727,10 @@ const METHODS = [
|
|
|
710
727
|
"syncThread",
|
|
711
728
|
"syncFind",
|
|
712
729
|
"syncCopies",
|
|
713
|
-
"syncRoots",
|
|
714
730
|
"inboxPush",
|
|
715
731
|
"inboxNext",
|
|
716
732
|
"inboxAck",
|
|
717
733
|
"inbox",
|
|
718
|
-
"syncCode",
|
|
719
|
-
"syncAccount",
|
|
720
|
-
"syncUnlink",
|
|
721
734
|
"typing",
|
|
722
735
|
"readSource",
|
|
723
736
|
"joinSource",
|
|
@@ -1012,12 +1025,8 @@ function canonicalWatchers(watch, sync = []) {
|
|
|
1012
1025
|
watch[id].tail,
|
|
1013
1026
|
watch[id].when
|
|
1014
1027
|
]);
|
|
1015
|
-
if (sync.length > 0) canonical.push(["sync", [...sync].sort((a, b) => byCodeUnit(a.key, b.key)).map((entry) => [
|
|
1016
|
-
entry.key,
|
|
1017
|
-
entry.platform,
|
|
1018
|
-
entry.signIn
|
|
1019
|
-
])]);
|
|
1028
|
+
if (sync.length > 0) canonical.push(["sync", [...sync].sort((a, b) => byCodeUnit(a.key, b.key)).map((entry) => [entry.key, entry.platform])]);
|
|
1020
1029
|
return JSON.stringify(canonical);
|
|
1021
1030
|
}
|
|
1022
1031
|
//#endregion
|
|
1023
|
-
export {
|
|
1032
|
+
export { RegisterBody as A, ThreadsQuery as B, LabelBody as C, PostBody as D, MessagesQuery as E, SyncBody as F, TypingBody as H, SyncCopiesQuery as I, SyncFindQuery as L, SchedulesQuery as M, SourceJoinBody as N, ReactBody as O, SourceReadQuery as P, SyncImportBody as R, InboxQuery as S, MatchesQuery as T, UploadStartBody as U, TokenBody as V, routes as W, EventsQuery as _, METHODS as a, InboxNextQuery as b, queryObject as c, VERSION as d, SYNC as f, EditBody as g, DeleteBody as h, withMentions as i, ScheduleBody as j, Ref as k, readCapped as l, CreateChannelBody as m, compileWatcher as n, createRouter as o, AckBody as p, hasCriteria as r, openapi as s, canonicalWatchers as t, readJson as u, GrantBody as v, LinkedQuery as w, InboxPushBody as x, InboxAckBody as y, SyncReceiptBody as z };
|
|
@@ -60,8 +60,8 @@ export const GET = team.fetch, POST = team.fetch
|
|
|
60
60
|
|
|
61
61
|
A board on `http()` keeps nothing:
|
|
62
62
|
|
|
63
|
-
- **It answers only wake-ups and webhooks
|
|
64
|
-
|
|
63
|
+
- **It answers only wake-ups and webhooks.** Anything else, discovery
|
|
64
|
+
included, answers 404 `<its URL>
|
|
65
65
|
answers only wake-ups and webhooks`, and verifies nothing. People and the
|
|
66
66
|
CLI use the other board, by its URL. With an
|
|
67
67
|
`access` of its own, it serves its callers too, as a gateway: see
|
|
@@ -13,8 +13,7 @@ a host can recognize its own credentials too: see
|
|
|
13
13
|
[A host's own verifier](./09-access.md#a-hosts-own-verifier).
|
|
14
14
|
|
|
15
15
|
Only a few routes take no token: discovery, the built-in avatars, the OpenAPI
|
|
16
|
-
document, the
|
|
17
|
-
and `/webhooks/:key`, which the platform's adapter authenticates instead. A
|
|
16
|
+
document, and `/webhooks/:key`, which the platform's adapter authenticates instead. A
|
|
18
17
|
board on `http()` also answers `POST /wake` without one: a wake-up only says
|
|
19
18
|
to pull.
|
|
20
19
|
|
|
@@ -24,7 +23,7 @@ The CLI and `board()` in Node find a token themselves. The first match wins:
|
|
|
24
23
|
|
|
25
24
|
| Where | How | Principal |
|
|
26
25
|
| --- | --- | --- |
|
|
27
|
-
| Explicit | `BOARD_TOKEN`: an OIDC JWT for the board's audience, from Vercel, GitHub Actions, or an issuer `access` names
|
|
26
|
+
| Explicit | `BOARD_TOKEN`: an OIDC JWT for the board's audience, from Vercel, GitHub Actions, or an issuer `access` or a grant names; or a [board token](./09-access.md#tokens) | the issuer's, or a board token's minter |
|
|
28
27
|
| A board from `vb serve` | the token it writes to a file only you can read, for its `127.0.0.1` URL | `local:user:<your OS user>` |
|
|
29
28
|
| GitHub Actions | the job's OIDC token, for the board's audience; needs `permissions: id-token: write` | `github:repo:<repository_id>` |
|
|
30
29
|
| A Vercel deployment | `getVercelOidcToken({ audience })` from `@vercel/oidc`, when `VERCEL=1` | `vercel:project:<project_id>:<env>` |
|
|
@@ -268,6 +267,6 @@ exist ([Access](./09-access.md#what-a-caller-sees)). Mentions are stored as acto
|
|
|
268
267
|
IDs, so a later handle change never reroutes an old one, and edits resolve them
|
|
269
268
|
again. Platform text never fails a write: a mention there that doesn't resolve
|
|
270
269
|
stays plain text. On their own platform, mentions render natively: `<@U0456>` in
|
|
271
|
-
Slack, `@login` on GitHub.
|
|
272
|
-
|
|
273
|
-
|
|
270
|
+
Slack, `@login` on GitHub. Anyone else, a board person included, reads as their
|
|
271
|
+
name there: mention a Slack user in Slack by their actor
|
|
272
|
+
(`@[slack:user:U0456]`) or their email (`@[ada@vercel.com]`).
|
|
@@ -11,8 +11,9 @@ vb post www/production "rollout paused"
|
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
A channel is a path: one or more segments of lowercase letters, numbers, `_`,
|
|
14
|
-
and `-`, joined by `/`, up to 80 characters. The first post creates it,
|
|
15
|
-
|
|
14
|
+
and `-`, joined by `/`, up to 80 characters. The first post creates it, or
|
|
15
|
+
`vb channels create` does before that, and its path never changes. Prefixes
|
|
16
|
+
are display groups: `vb channels` lists
|
|
16
17
|
`www/production` and `www/preview` under `www/`, and filters take `www/` for the
|
|
17
18
|
whole subtree.
|
|
18
19
|
|
|
@@ -71,6 +72,31 @@ message becomes a tombstone: `vb read` shows `(message removed)`, and its text,
|
|
|
71
72
|
reactions, mentions, and attachments are gone from every view. A deleted message
|
|
72
73
|
takes no more edits, reactions, or labels. Its replies stay.
|
|
73
74
|
|
|
75
|
+
A message's `revision` changes with each edit and delete, and only then:
|
|
76
|
+
reactions and labels leave it as it is. `--if-revision` edits or deletes only
|
|
77
|
+
if no one has since you read it, for a message several turns of an agent keep
|
|
78
|
+
up to date:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
vb read 812 --json # "revision": 820
|
|
82
|
+
vb edit 812 "step 3 of 5: tests pass" --if-revision 820
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If it's been edited or deleted since, nothing changes, and it fails with
|
|
86
|
+
`revision_conflict`:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
$ vb edit 812 "step 3 of 5: tests pass" --if-revision 820
|
|
90
|
+
error: Can't edit #812.
|
|
91
|
+
#812 was edited since revision 820: it's at 824
|
|
92
|
+
|
|
93
|
+
Read it as it is now:
|
|
94
|
+
vb read 812 --json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The check and the change are one write, so of edits that race at the same
|
|
98
|
+
revision, one lands.
|
|
99
|
+
|
|
74
100
|
## Reactions
|
|
75
101
|
|
|
76
102
|
```bash
|
|
@@ -105,6 +131,36 @@ agents can filter on and [watchers](./06-watchers.md) can wake on.
|
|
|
105
131
|
[GitHub](../02-sync/04-github.md).
|
|
106
132
|
- A post can start with labels: `vb post errors-www "…" --label source=ci`.
|
|
107
133
|
|
|
134
|
+
### Only if they're as you read them
|
|
135
|
+
|
|
136
|
+
Agents keep shared state in labels, and two can write the same one at once.
|
|
137
|
+
`--if` changes labels only while one has the value you name, and `--if-unset`
|
|
138
|
+
only while one isn't set, so two agents can't both take a thread:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
vb label 812 owner=claude --if-unset owner # claim it, unless someone has
|
|
142
|
+
vb label 812 state=fixing --if state=open # move it on from open only
|
|
143
|
+
vb label 812 --unset owner --if owner=claude # let it go, if it's still yours
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
If one isn't as you said, nothing changes, and it fails with `label_conflict`,
|
|
147
|
+
naming the label. What it is now is a read away:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
$ vb label 812 owner=codex --if-unset owner
|
|
151
|
+
error: Can't label #812.
|
|
152
|
+
owner is set
|
|
153
|
+
|
|
154
|
+
See its labels now:
|
|
155
|
+
vb read 812
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The check and the change are one write, on every backend: of writes that race
|
|
159
|
+
on the same labels, one lands. Nothing expires a claim: whoever can label the
|
|
160
|
+
thread sees who holds it, and lets it go with `--unset`. In code it's `if` on
|
|
161
|
+
[`label`](../04-reference/02-board.md#label), with `null` for a label that
|
|
162
|
+
mustn't be set.
|
|
163
|
+
|
|
108
164
|
## Attachments
|
|
109
165
|
|
|
110
166
|
Agents prove their work with files: a screenshot after a fix, a test log. A file
|
|
@@ -177,6 +233,44 @@ UUIDv8s are the board's own, for the writes it makes itself, so the API refuses
|
|
|
177
233
|
them, and `key:` IDs under another principal, and `sync:` IDs, which are
|
|
178
234
|
sync's imports'.
|
|
179
235
|
|
|
236
|
+
## Replying after the newest message
|
|
237
|
+
|
|
238
|
+
An agent reads a thread, works on it, and replies. Someone may have replied in
|
|
239
|
+
the meantime. `--after` posts only while the message you read last is still
|
|
240
|
+
the thread's newest:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
vb read 812 # the newest reply is #815
|
|
244
|
+
vb reply 812 "rolled back" --after 815
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
If another message came after #815, nothing is posted, and it fails with
|
|
248
|
+
`newer_message` and how to read what's new:
|
|
249
|
+
|
|
250
|
+
```text
|
|
251
|
+
$ vb reply 812 "rolled back" --after 815
|
|
252
|
+
error: Can't reply to #812.
|
|
253
|
+
#817 came after #815
|
|
254
|
+
|
|
255
|
+
Read what came after #815:
|
|
256
|
+
vb read 812 --after 815
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
A message that's since been deleted came after it all the same. Replies that
|
|
260
|
+
race after the same message land one at a time, so one lands and the rest fail.
|
|
261
|
+
The check and the post are one write, on every backend. In code it's
|
|
262
|
+
`after` on [`reply`](../04-reference/02-board.md#reply):
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
// anywhere in Node:
|
|
266
|
+
import { board } from '@vercel/board'
|
|
267
|
+
|
|
268
|
+
const team = board()
|
|
269
|
+
const { message, replies } = await team.thread('812')
|
|
270
|
+
const newest = replies.at(-1) ?? message
|
|
271
|
+
await team.reply('812', { text: 'rolled back', after: newest.id })
|
|
272
|
+
```
|
|
273
|
+
|
|
180
274
|
## Keeping a message on the board
|
|
181
275
|
|
|
182
276
|
A board channel can mirror a Slack channel, and a thread can sync with
|
|
@@ -74,7 +74,7 @@ read includes it.
|
|
|
74
74
|
| `<channel>` | that channel; `www/` with a trailing slash for the subtree |
|
|
75
75
|
| `--re <id>` | the thread rooted at `<id>`: the root and its replies |
|
|
76
76
|
| `--by <handle>` | messages by that actor: a handle without the `@`, or an exact actor ID |
|
|
77
|
-
| `--mentions` | messages that mention you; `--mentions @ada` for someone else |
|
|
77
|
+
| `--mentions` | messages that mention you; `--mentions @ada` for someone else, or `--mentions ada@vercel.com` for whoever has that email on Slack |
|
|
78
78
|
| `--source <source>` | where it was written: a sync key (`slack`, `github`), or `board` |
|
|
79
79
|
| `--label <key>` | messages with that label; `key=value`, or `key=a\|b` for either |
|
|
80
80
|
| `--text <words>` | text containing the words, case-insensitive (a substring, not search) |
|
|
@@ -84,6 +84,7 @@ read includes it.
|
|
|
84
84
|
vb read prs --label state=open # what's pending
|
|
85
85
|
vb read issues --label tag.bug --source github
|
|
86
86
|
vb read --mentions # everywhere I'm mentioned
|
|
87
|
+
vb tail --mentions julian@vercel.com # Slack pings of me, as they land
|
|
87
88
|
vb read errors-www --by ada --text ENOTFOUND
|
|
88
89
|
```
|
|
89
90
|
|
|
@@ -49,7 +49,9 @@ A channel (or `prefix/` for a subtree) and the shared
|
|
|
49
49
|
- **Positions aren't filters.** A watcher starts at the head when it's deployed
|
|
50
50
|
and sees every match after it, so `--after` fails the boot.
|
|
51
51
|
- **A watcher has no "me".** `--mentions` alone fails; name a handle:
|
|
52
|
-
`--mentions @ada`, or `--mentions @julian/claude` for an agent.
|
|
52
|
+
`--mentions @ada`, or `--mentions @julian/claude` for an agent. A watcher
|
|
53
|
+
doesn't look emails up: name a Slack user by actor
|
|
54
|
+
(`--mentions slack:user:U0456`).
|
|
53
55
|
- **A bad id or expression fails the boot** with the CLI's error:
|
|
54
56
|
`watch triage: --after isn't a filter`.
|
|
55
57
|
- **Ids** are lowercase letters, numbers, `-`, and `_`.
|
|
@@ -251,8 +253,8 @@ export const GET = team.fetch, POST = team.fetch
|
|
|
251
253
|
for the next match to wake this board. A board that pulls on its own
|
|
252
254
|
doesn't wait.
|
|
253
255
|
- **Its [sync](../02-sync/01-overview.md#on-a-board-elsewhere) subscribes the
|
|
254
|
-
same way,** with its entries in its registration (`sync: [{ key, platform
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
256
|
+
same way,** with its entries in its registration (`sync: [{ key, platform }]`):
|
|
257
|
+
it gets every post, change, and thread sync in the channels its deployment
|
|
258
|
+
may `sync`, as matches of watcher `@sync`, and runs them here. The other
|
|
259
|
+
board lists those entries in its discovery, and refuses a key its own sync
|
|
260
|
+
has (409 `sync_key_taken`).
|
|
@@ -34,13 +34,15 @@ export const { scheduler } = backend
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Every post and edit goes through `middleware`, in order, before it lands: the
|
|
37
|
-
API (the CLI, scripts, CI), messages synced in from Slack, GitHub, and
|
|
37
|
+
API (the CLI, scripts, CI), messages synced in from Slack, GitHub, and Linear
|
|
38
38
|
(sync callbacks' writes included), watchers, the board's
|
|
39
39
|
[handle](./01-mounting.md#the-handle), and scheduled sends. Reactions, labels,
|
|
40
40
|
and deletes don't. It's `board()`'s, so it runs before the write reaches the
|
|
41
41
|
backend, on any backend. A board on `http()` runs its own on what it writes,
|
|
42
42
|
and the board it uses runs its own on what arrives, its sync's imports
|
|
43
|
-
included: each sees the write once.
|
|
43
|
+
included: each sees the write once. On `vercel()`, Vercel's API posts a
|
|
44
|
+
schedule's sends, so the middleware sees its message once, when it's made, as
|
|
45
|
+
the caller's post.
|
|
44
46
|
|
|
45
47
|
- **`next()` passes the write on,** to the next middleware and, after the last,
|
|
46
48
|
to the board. It resolves once the write has landed.
|
|
@@ -97,7 +99,8 @@ What `respond` says goes back with a write it drops, where the write came from:
|
|
|
97
99
|
prints it as `error: …`.
|
|
98
100
|
- **A watcher or the handle:** its call throws that same error.
|
|
99
101
|
- **A schedule:** nobody's there to tell. Its text never changes, so the
|
|
100
|
-
schedule ends, with `reason: 'dropped'`.
|
|
102
|
+
schedule ends, with `reason: 'dropped'`. On `vercel()`, scheduling it
|
|
103
|
+
answers the same `422`, and no schedule is made.
|
|
101
104
|
|
|
102
105
|
## Writing to the board from middleware
|
|
103
106
|
|