@vercel/board 0.25.1 → 0.26.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 (37) hide show
  1. package/dist/{a2-A830f5NS.mjs → a2-Qzj0kCWU.mjs} +1 -1
  2. package/dist/backend-a2.mjs +1 -1
  3. package/dist/{backend-http-CYtfwqAW.mjs → backend-http-BB0Gld6Q.mjs} +1 -1
  4. package/dist/{backend-http-CJd4Fu0U.mjs → backend-http-F0W6e36G.mjs} +2 -2
  5. package/dist/backend-http.mjs +1 -1
  6. package/dist/backend-http.node.mjs +1 -1
  7. package/dist/{backend-vercel-BhwPExmE.mjs → backend-vercel-CtB46vMQ.mjs} +2 -2
  8. package/dist/{backend-vercel-Bqg2QfTd.mjs → backend-vercel-DUWIcFi9.mjs} +19 -6
  9. package/dist/backend-vercel.mjs +1 -1
  10. package/dist/backend-vercel.node.d.mts +5 -0
  11. package/dist/backend-vercel.node.mjs +2 -2
  12. package/dist/cli-commands.d.mts +1 -1
  13. package/dist/cli-commands.mjs +1 -1
  14. package/dist/cli-embedded.d.mts +5 -1
  15. package/dist/cli-embedded.mjs +8 -4
  16. package/dist/cli.mjs +4 -4
  17. package/dist/{commands-D0wXq6CY.d.mts → commands-D6gvrOh3.d.mts} +4 -4
  18. package/dist/{commands-BtL4jFq8.mjs → commands-nEG47RM_.mjs} +7 -7
  19. package/dist/{exports-DCqgO_mr.mjs → exports-NFjBgIT2.mjs} +1 -1
  20. package/dist/{index.default-BejExznB.mjs → index.default-Boe7x-mK.mjs} +1 -1
  21. package/dist/index.mjs +2 -2
  22. package/dist/index.node.mjs +1 -1
  23. package/dist/inspect/app.js +1 -1
  24. package/dist/{main-DOhNCWMd.mjs → main-BuahKR16.mjs} +19 -9
  25. package/dist/{node-Bkd1x0QI.mjs → node-Ybi9E6bO.mjs} +67 -37
  26. package/dist/{serve-D2O0C6Kk.mjs → serve-BbHEQk_Y.mjs} +3 -3
  27. package/dist/{server-BF4WJ7qi.mjs → server-Bphio_82.mjs} +2 -2
  28. package/dist/{watchers-hash-BHNXy_N7.mjs → watchers-hash-BfDPW95j.mjs} +4 -4
  29. package/docs/01-board/04-messages.md +1 -1
  30. package/docs/01-board/05-reading.md +2 -2
  31. package/docs/01-board/06-watchers.md +3 -3
  32. package/docs/02-sync/01-overview.md +2 -2
  33. package/docs/03-guides/03-ci.md +2 -2
  34. package/docs/03-guides/04-triage.md +5 -5
  35. package/docs/04-reference/01-cli.md +9 -4
  36. package/docs/index.md +2 -2
  37. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { t as BoardError } from "./client-D0Px7KzF.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
- import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { mkdir, readFile, readdir, stat, unlink, writeFile } from "node:fs/promises";
5
5
  import { existsSync, readFileSync, statSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { dirname, join, resolve } from "node:path";
@@ -118,54 +118,89 @@ const expiry = (token) => {
118
118
  }
119
119
  };
120
120
  /**
121
- * Where `audience`'s token is cached: one per board, and with a login the Vercel CLI hands over
122
- * (`VERCEL_AUTH_TOKEN`), one per login too, since it can be someone else's than `vercel login`'s.
121
+ * Where `audience`'s token is cached: one per board, and with a login a CLI hands over, one per
122
+ * login too, since it can be someone else's than `vercel login`'s.
123
123
  */
124
- const cacheFile = (audience) => {
125
- const login = process.env.VERCEL_AUTH_TOKEN;
124
+ const cacheFile = (audience, login) => {
126
125
  const key = login ? `${audience}\n${login}` : audience;
127
126
  return join(configDir(), "tokens", `${createHash("sha256").update(key).digest("hex").slice(0, 32)}.json`);
128
127
  };
129
- async function readCache(audience) {
128
+ async function readCache(audience, login) {
130
129
  try {
131
- const cached = JSON.parse(await readFile(cacheFile(audience), "utf8"));
130
+ const cached = JSON.parse(await readFile(cacheFile(audience, login), "utf8"));
132
131
  return cached.expiresAt - 3e5 > Date.now() ? cached : null;
133
132
  } catch {
134
133
  return null;
135
134
  }
136
135
  }
137
- async function writeCache(audience, cached) {
138
- await mkdir(join(configDir(), "tokens"), {
136
+ async function writeCache(audience, login, cached) {
137
+ const tokens = join(configDir(), "tokens");
138
+ await mkdir(tokens, {
139
139
  recursive: true,
140
140
  mode: 448
141
141
  });
142
- await writeFile(cacheFile(audience), JSON.stringify(cached), { mode: 384 });
142
+ await writeFile(cacheFile(audience, login), JSON.stringify(cached), { mode: 384 });
143
+ const dead = Date.now() - 864e5;
144
+ for (const name of await readdir(tokens).catch(() => [])) {
145
+ const file = join(tokens, name);
146
+ const written = (await stat(file).catch(() => null))?.mtimeMs;
147
+ if (written !== void 0 && written < dead) await unlink(file).catch(() => {});
148
+ }
143
149
  }
150
+ /** The login a CLI that runs board's hands it to ask for (`vercel board`'s), while one does. */
151
+ let handed = null;
144
152
  /**
145
- * Your Vercel login: what the Vercel CLI hands a CLI it runs, as `vercel board` (`VERCEL_AUTH_TOKEN`,
146
- * from its `--token` or its own login), or else what `vercel login` saved.
153
+ * Hands board a login to ask for when a request needs one, as `vercel board` does with its own:
154
+ * nothing asks for it before then, so help, a typo, or misuse never refreshes or prompts. Null
155
+ * takes it back.
147
156
  */
148
- const vercelLogin = () => process.env.VERCEL_AUTH_TOKEN ? Promise.resolve(process.env.VERCEL_AUTH_TOKEN) : getVercelToken();
157
+ function handLogin(login) {
158
+ handed = login;
159
+ }
160
+ /** Whether the login is one a CLI handed over (`createApp`'s `login`, or `VERCEL_AUTH_TOKEN`). */
161
+ const loginHanded = () => handed !== null || !!process.env.VERCEL_AUTH_TOKEN;
162
+ /**
163
+ * Your Vercel login: what the CLI that runs board's hands it, as `vercel board` does (its `login`,
164
+ * or `VERCEL_AUTH_TOKEN` from its `--token`), or else what `vercel login` saved.
165
+ */
166
+ const vercelLogin = () => handed ? handed() : process.env.VERCEL_AUTH_TOKEN ? Promise.resolve(process.env.VERCEL_AUTH_TOKEN) : getVercelToken();
167
+ /**
168
+ * A handed login's token, which its board tokens are cached by: null for `vercel login`'s own. One
169
+ * that fails fails the request: `vercel login`'s, cached or in the `vercel` on PATH, can be someone
170
+ * else's.
171
+ */
172
+ async function handedToken() {
173
+ if (!loginHanded()) return null;
174
+ try {
175
+ return await vercelLogin();
176
+ } catch {
177
+ throw new BoardError("not_logged_in", "You're not logged in to Vercel.", 0, { hint: "Run vercel login." });
178
+ }
179
+ }
180
+ /** The login to call Vercel's API with: a handed one, or `vercel login`'s, or none it can read (the keychain's). */
181
+ const apiLogin = async (handed) => handed ?? await getVercelToken().catch(() => null);
149
182
  /**
150
183
  * A person's token: their Vercel CLI login mints a development token for the
151
184
  * project the board's host is a deployment of, exchanged so its audience is the
152
185
  * board's URL. Cached on disk for its 12-hour life.
153
186
  */
154
187
  async function personToken(audience, discovery) {
155
- const cached = await readCache(audience);
188
+ const login = await handedToken();
189
+ const cached = await readCache(audience, login);
156
190
  if (cached && cached.name === void 0) {
157
- const user = await vercelLogin().then(vercelUser).catch(() => null);
191
+ const authToken = await apiLogin(login);
192
+ const user = authToken ? await vercelUser(authToken) : null;
158
193
  const filled = {
159
194
  ...cached,
160
195
  name: user?.name ?? null,
161
196
  avatar: user?.avatar ?? null
162
197
  };
163
- await writeCache(audience, filled).catch(() => {});
198
+ await writeCache(audience, login, filled).catch(() => {});
164
199
  return filled;
165
200
  }
166
201
  if (cached) return cached;
167
202
  if (!discovery.project_id) throw new BoardError("no_project", `The board at ${audience} doesn't know its Vercel project, so there's no Vercel login token to mint for it.`, 0, { hint: "Pass a token it accepts in BOARD_TOKEN." });
168
- const authToken = await vercelLogin().catch(() => null);
203
+ const authToken = await apiLogin(login);
169
204
  const project = await boardProject(audience, discovery, authToken);
170
205
  let development;
171
206
  try {
@@ -178,7 +213,7 @@ async function personToken(audience, discovery) {
178
213
  if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
179
214
  development = (await response.json()).token;
180
215
  } catch (error) {
181
- if (error instanceof BoardError) throw error;
216
+ if (error instanceof BoardError || login) throw error;
182
217
  development = await tokenFromVercelCli(project);
183
218
  }
184
219
  const token = await exchangeVercelOidcToken({
@@ -193,7 +228,7 @@ async function personToken(audience, discovery) {
193
228
  name: user?.name ?? null,
194
229
  avatar: user?.avatar ?? null
195
230
  };
196
- await writeCache(audience, fresh).catch(() => {});
231
+ await writeCache(audience, login, fresh).catch(() => {});
197
232
  return fresh;
198
233
  }
199
234
  /**
@@ -546,23 +581,18 @@ const automationBypass = (bypasses) => Object.entries(bypasses ?? {}).find(([, v
546
581
  */
547
582
  async function projectBypass(url, host, doFetch) {
548
583
  const blocked = (why, fix) => new BoardError("protected", `${url} is behind Vercel Deployment Protection, and your Vercel login ${why}.`, 0, { hint: `${fix}, or set VERCEL_AUTOMATION_BYPASS_SECRET (${WHERE}).` });
549
- let api;
550
- try {
551
- const token = await vercelLogin();
552
- api = async (method, path) => {
553
- const response = await doFetch(`https://api.vercel.com${path}`, {
554
- method,
555
- headers: {
556
- authorization: `Bearer ${token}`,
557
- ...method === "PATCH" ? { "content-type": "application/json" } : {}
558
- },
559
- ...method === "PATCH" ? { body: "{}" } : {}
560
- });
561
- return response.ok ? await response.json() : null;
562
- };
563
- } catch {
564
- api = vercelCliApi;
565
- }
584
+ const token = await apiLogin(await handedToken());
585
+ const api = token ? async (method, path) => {
586
+ const response = await doFetch(`https://api.vercel.com${path}`, {
587
+ method,
588
+ headers: {
589
+ authorization: `Bearer ${token}`,
590
+ ...method === "PATCH" ? { "content-type": "application/json" } : {}
591
+ },
592
+ ...method === "PATCH" ? { body: "{}" } : {}
593
+ });
594
+ return response.ok ? await response.json() : null;
595
+ } : vercelCliApi;
566
596
  const project = await findProject(api, host);
567
597
  if (!project) throw blocked("can't see its project", "Log in as someone on its project's team (vercel login), ask for access");
568
598
  const scope = project.ownerId.startsWith("team_") ? `?teamId=${project.ownerId}` : "";
@@ -628,4 +658,4 @@ const vercelCliApi = async (method, path) => {
628
658
  }
629
659
  };
630
660
  //#endregion
631
- export { projectDir as a, localTokenFile as i, connect as n, vercelLogin as o, environmentProfile as r, vercelProjectFile as s, boardUrl as t };
661
+ export { localTokenFile as a, vercelLogin as c, handLogin as i, vercelProjectFile as l, connect as n, loginHanded as o, environmentProfile as r, projectDir as s, boardUrl as t };
@@ -1,8 +1,8 @@
1
1
  import { t as BoardError } from "./client-D0Px7KzF.mjs";
2
- import { i as localTokenFile } from "./node-Bkd1x0QI.mjs";
3
- import { t as board } from "./server-BF4WJ7qi.mjs";
2
+ import { a as localTokenFile } from "./node-Ybi9E6bO.mjs";
3
+ import { t as board } from "./server-Bphio_82.mjs";
4
4
  import { t as oidcTokens } from "./tokens-BuCU-61R.mjs";
5
- import { n as fsBlob, t as a2 } from "./a2-A830f5NS.mjs";
5
+ import { n as fsBlob, t as a2 } from "./a2-Qzj0kCWU.mjs";
6
6
  import { t as localScheduler } from "./local-DhkrAqPP.mjs";
7
7
  import { mkdir, rm, writeFile } from "node:fs/promises";
8
8
  import { userInfo } from "node:os";
@@ -1,5 +1,5 @@
1
- import { t as createBoard } from "./exports-DCqgO_mr.mjs";
2
- import { t as http } from "./backend-http-CJd4Fu0U.mjs";
1
+ import { t as createBoard } from "./exports-NFjBgIT2.mjs";
2
+ import { t as http } from "./backend-http-F0W6e36G.mjs";
3
3
  //#region src/server/index.ts
4
4
  /**
5
5
  * The handle on a board. With a `backend`, the board lives there: `a2()` keeps it in this
@@ -2,7 +2,7 @@ import { n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files
2
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, n as ActorId, p as Labels, x as platformAvatar } from "./schemas-Be08nyfW.mjs";
3
3
  import { a as Link, i as ExternalTarget, n as ExternalEvent, o as Origin, t as ExternalAuthor } from "./external-BV_cUncs.mjs";
4
4
  import { r as badRequest, t as HttpError } from "./errors-CusN4x2l.mjs";
5
- import { o as options } from "./commands-BtL4jFq8.mjs";
5
+ import { o as options } from "./commands-nEG47RM_.mjs";
6
6
  import { z } from "zod";
7
7
  //#region src/api/routes.ts
8
8
  /**
@@ -662,7 +662,7 @@ const routes = {
662
662
  const SYNC = "@sync";
663
663
  //#endregion
664
664
  //#region src/version.ts
665
- const VERSION = "0.25.1";
665
+ const VERSION = "0.26.0";
666
666
  //#endregion
667
667
  //#region src/server/routing.ts
668
668
  /** board()'s own routes: the files it serves, and sync's, which run where board() does. */
@@ -865,7 +865,7 @@ const FILTER_FLAGS = {
865
865
  by: options.by,
866
866
  mentions: options.mentions,
867
867
  label: options.label,
868
- from: options.from,
868
+ source: options.source,
869
869
  text: options.text
870
870
  };
871
871
  /** `--mentions` alone means mentions of me; `--mentions @ada`, mentions of ada. */
@@ -961,7 +961,7 @@ function parseWatchTail(expression) {
961
961
  ...values.by ? { by: values.by.replace(/^@/, "") } : {},
962
962
  ...values.mentions ? { mentions: values.mentions.replace(/^@/, "") } : {},
963
963
  ...values.label?.length ? { label: values.label } : {},
964
- ...values.from ? { from: values.from } : {},
964
+ ...values.source ? { from: values.source } : {},
965
965
  ...values.text ? { text: values.text } : {}
966
966
  });
967
967
  if (!parsed.success) {
@@ -164,7 +164,7 @@ await team.reply('812', { text: 'fixed, see after', attachments: [shot] })
164
164
  A retry shouldn't post twice. Give a post or reply an ID of your own:
165
165
 
166
166
  ```bash
167
- vb post deploys "a1b2c3 is live" --id deploy-a1b2c3
167
+ vb post deploys "a1b2c3 is live" --idempotency-key deploy-a1b2c3
168
168
  ```
169
169
 
170
170
  The same ID with the same write returns the message it already posted. The same
@@ -75,14 +75,14 @@ read includes it.
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
77
  | `--mentions` | messages that mention you; `--mentions @ada` for someone else |
78
- | `--from <source>` | where it was written: a sync key (`slack`, `github`), or `board` |
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) |
81
81
  | `--after <id>` | messages after that message ID or cursor; for `read`, threads with activity after it |
82
82
 
83
83
  ```bash
84
84
  vb read prs --label state=open # what's pending
85
- vb read issues --label tag.bug --from github
85
+ vb read issues --label tag.bug --source github
86
86
  vb read --mentions # everywhere I'm mentioned
87
87
  vb read errors-www --by ada --text ENOTFOUND
88
88
  ```
@@ -21,7 +21,7 @@ const team = board({
21
21
  // sync as in Sync
22
22
  watch: {
23
23
  triage: {
24
- tail: 'firehose --from slack',
24
+ tail: 'firehose --source slack',
25
25
  run: async ({ event, board }) => {
26
26
  if (event.type === 'message.posted' && event.message?.re === null)
27
27
  await board.reply(event.message.id, 'Thanks! Tracking it.') // synced to the Slack thread
@@ -37,13 +37,13 @@ export const { scheduler } = backend
37
37
  would print, exactly as `vb tail --json` prints it. Try it in a terminal first:
38
38
 
39
39
  ```bash
40
- vb tail firehose --from slack --json
40
+ vb tail firehose --source slack --json
41
41
  ```
42
42
 
43
43
  ## The expression
44
44
 
45
45
  A channel (or `prefix/` for a subtree) and the shared
46
- [filters](./05-reading.md#filters): `--re`, `--by`, `--mentions`, `--from`,
46
+ [filters](./05-reading.md#filters): `--re`, `--by`, `--mentions`, `--source`,
47
47
  `--label`, `--text`, and `--all`.
48
48
 
49
49
  - **Positions aren't filters.** A watcher starts at the head when it's deployed
@@ -95,7 +95,7 @@ see [Sync functions](./02-functions.md).
95
95
 
96
96
  **Entry keys** (`slack`, `github`, or `acme` for a second Slack workspace) are
97
97
  names you choose; the adapter decides the platform. The key appears in external
98
- IDs (`acme:C0999XYZ:1727.9`), in sync targets, in `--from` filters, and in the
98
+ IDs (`acme:C0999XYZ:1727.9`), in sync targets, in `--source` filters, and in the
99
99
  webhook path (`/webhooks/acme`).
100
100
 
101
101
  **Map keys** are the platform's names or IDs: `'#prs'` or `'C0123ABC'` for
@@ -303,7 +303,7 @@ written to the log. It needs `post` and `read` in the thread's channel.
303
303
  vb sources # the board's sync entries and their platforms
304
304
  vb inbox slack # raw platform events, newest first: did Slack send it?
305
305
  vb inbox slack '#errors-www' --limit 5
306
- vb tail --from slack --all # board events from Slack, delivery receipts included
306
+ vb tail --source slack --all # board events from Slack, delivery receipts included
307
307
  ```
308
308
 
309
309
  `vb inbox` needs `sync` in every channel for the board's own inbox, and every
@@ -24,7 +24,7 @@ jobs:
24
24
  - uses: actions/checkout@v4
25
25
  # your deploy steps…
26
26
  - if: failure()
27
- run: npx -y @vercel/board post errors-www "deploy failed on $SHA" --label source=ci --id "deploy-$SHA"
27
+ run: npx -y @vercel/board post errors-www "deploy failed on $SHA" --label source=ci --idempotency-key "deploy-$SHA"
28
28
  env:
29
29
  BOARD_URL: https://team.example/api/board
30
30
  SHA: ${{ github.sha }}
@@ -34,7 +34,7 @@ jobs:
34
34
  whose audience is the board's URL, so it's good for that board and nothing
35
35
  else. Without `id-token: write`, GitHub gives the job no token, so the CLI
36
36
  falls through to a `vercel login` the runner doesn't have, and fails.
37
- - **`--id` makes a retried step post once.** The same ID with the same write
37
+ - **`--idempotency-key` makes a retried step post once.** The same ID with the same write
38
38
  returns the message already posted.
39
39
  - **The post is an ordinary thread.** If `errors-www` mirrors a Slack channel,
40
40
  it lands there, and a reply in Slack reaches the board.
@@ -18,10 +18,10 @@ messages.
18
18
 
19
19
  ```bash
20
20
  # in a terminal:
21
- vb tail firehose --from slack --json
21
+ vb tail firehose --source slack --json
22
22
  ```
23
23
 
24
- `tail` prints one line of JSON per event, live, until Ctrl-C. `--from slack`
24
+ `tail` prints one line of JSON per event, live, until Ctrl-C. `--source slack`
25
25
  keeps messages that came from Slack. Each line carries the event and the
26
26
  message it concerns, as it is after the event:
27
27
 
@@ -38,7 +38,7 @@ report is a `message.posted` whose message has `re: null`: a thread root.
38
38
 
39
39
  ```bash
40
40
  # triage.sh: runs until you stop it
41
- vb tail firehose --from slack --json | while read -r event; do
41
+ vb tail firehose --source slack --json | while read -r event; do
42
42
  [ "$(jq -r '.type' <<<"$event")" = "message.posted" ] || continue
43
43
  [ "$(jq -r '.message.re' <<<"$event")" = "null" ] || continue
44
44
  id=$(jq -r '.message.id' <<<"$event")
@@ -63,7 +63,7 @@ prints it, and exits, so each turn handles one event. Every event carries a
63
63
  ```bash
64
64
  # triage-turn.sh: one event per run
65
65
  last=$(cat .triage-cursor 2>/dev/null)
66
- next=$(vb wait firehose --from slack ${last:+--after "$last"} --json --timeout 1h) || exit 0
66
+ next=$(vb wait firehose --source slack ${last:+--after "$last"} --json --timeout 1h) || exit 0
67
67
  jq -r '.cursor' <<<"$next" > .triage-cursor
68
68
 
69
69
  [ "$(jq -r '.type' <<<"$next")" = "message.posted" ] || exit 0
@@ -117,7 +117,7 @@ const team = board({
117
117
  // sync: { slack: slack('slack/acme', channels({ '#firehose': 'firehose' })) },
118
118
  watch: {
119
119
  triage: {
120
- tail: 'firehose --from slack',
120
+ tail: 'firehose --source slack',
121
121
  run: async ({ event, board }) => {
122
122
  const message = event.message
123
123
  if (event.type !== 'message.posted' || !message || message.re !== null) return
@@ -47,6 +47,11 @@ const code = await createApp({
47
47
  onError: (failure, { json }) => console.error(json ? JSON.stringify(failure) : failureLines(failure).join('\n')),
48
48
  // { reason, message }: a write that went through, with something to say. With --json, it's in the result.
49
49
  onWarning: (warning) => console.error(warning.message),
50
+ // Your Vercel login, and the team whose board to use when --scope names none, as this CLI
51
+ // resolves its own: asked for only when a request needs them. Help, a typo, or misuse never asks.
52
+ // A login that throws fails the request: board never falls back to `vercel login`'s.
53
+ login: async () => process.env.VERCEL_AUTH_TOKEN!,
54
+ team: async () => process.env.VERCEL_ORG_ID ?? null,
50
55
  }).run(process.argv.slice(3))
51
56
  ```
52
57
 
@@ -485,7 +490,7 @@ schedules that reply in that thread. `--all` adds done and canceled ones.
485
490
  ### post
486
491
 
487
492
  ```text
488
- vb post <channel|id> ["<text>"] [--label k=v]... [--attach <file>]... [--sync <target>]... [--no-sync] [--id <key>]
493
+ vb post <channel|id> ["<text>"] [--label k=v]... [--attach <file>]... [--sync <target>]... [--no-sync] [--idempotency-key <key>]
489
494
  ```
490
495
 
491
496
  Starts a thread. The channel is created by its first post. Given a message ID
@@ -499,7 +504,7 @@ for new threads only.
499
504
  | `--attach <file>` | uploads a file (at most 4 MB, or 100 MB on a board whose blob store takes direct uploads; 10 per message) and attaches it. Every file is checked before any uploads. A board that keeps no files refuses: ``error: this board keeps no files: pass `blob` to `a2()` `` |
500
505
  | `--sync <target>` | also starts or joins a thread there: `slack:#incidents`, `github:owner/repo:12`; new threads only |
501
506
  | `--no-sync` | keeps the message on the board: it reaches none of the channel's mirrored channels |
502
- | `--id <key>` | an idempotency key: the same key never posts twice |
507
+ | `--idempotency-key <key>` | the same key never posts twice, so a retried step posts once |
503
508
 
504
509
  `--sync` and `--no-sync` together are a usage error. See
505
510
  [Messages](../01-board/04-messages.md) and
@@ -508,7 +513,7 @@ for new threads only.
508
513
  ### reply
509
514
 
510
515
  ```text
511
- vb reply <id> ["<text>"] [--attach <file>]... [--no-sync] [--id <key>]
516
+ vb reply <id> ["<text>"] [--attach <file>]... [--no-sync] [--idempotency-key <key>]
512
517
  ```
513
518
 
514
519
  Replies in a thread. `<id>` may be any message in it: the board stores the
@@ -739,7 +744,7 @@ of them must match.
739
744
  | `--by <handle>` | messages by an actor, by handle or exact actor ID. A person's handle covers their agents (`julian/claude`) and linked accounts |
740
745
  | `--mentions` | messages that mention you |
741
746
  | `--mentions @handle` | messages that mention that actor |
742
- | `--from <source>` | where a message came from: a sync key (`slack`, `github`), or `board` for messages written on the board |
747
+ | `--source <source>` | where a message came from: a sync key (`slack`, `github`), or `board` for messages written on the board |
743
748
  | `--label <key>` | messages with the label, any value |
744
749
  | `--label <key>=<value>` | that value; `state=open\|draft` means either |
745
750
  | `--text <words>` | a case-insensitive substring of the text (1 to 200 characters), not search |
package/docs/index.md CHANGED
@@ -110,7 +110,7 @@ const team = board({
110
110
  // sync as above
111
111
  watch: {
112
112
  triage: {
113
- tail: 'errors-www --from slack',
113
+ tail: 'errors-www --source slack',
114
114
  run: async ({ event, board }) => {
115
115
  if (event.type !== 'message.posted' || event.message?.re !== null) return
116
116
  // the reply reaches the Slack thread too:
@@ -124,7 +124,7 @@ export const { scheduler } = backend
124
124
  ```
125
125
 
126
126
  A watcher is a `vb tail` the board runs for you. Try the expression in a
127
- terminal first (`vb tail errors-www --from slack --json`); `run` gets exactly
127
+ terminal first (`vb tail errors-www --source slack --json`); `run` gets exactly
128
128
  those events, durably, with retries and nothing kept open. See
129
129
  [Watchers](./01-board/06-watchers.md).
130
130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/board",
3
- "version": "0.25.1",
3
+ "version": "0.26.0",
4
4
  "description": "A message board you mount in one route: one durable log, verified authors, live tail, and two-way sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",