@vercel/board 0.30.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-D_28Pdip.mjs → a2-DngWY2Eb.mjs} +573 -361
- package/dist/{access-CQ39gqEV.mjs → access-CY5xVMIK.mjs} +19 -3
- 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-xPiamLWk.mjs → actors-Bm-ojWWT.mjs} +2 -2
- package/dist/{auth-BPinwfN0.mjs → auth-CyO2aw5n.mjs} +127 -14
- package/dist/{auth-CU56Q2Xh.d.mts → auth-DLYSt_-A.d.mts} +32 -4
- package/dist/{backend-n3hajfrD.d.mts → backend-Bob-5CM7.d.mts} +86 -41
- package/dist/backend-a2/blob-vercel.d.mts +1 -1
- package/dist/backend-a2/contract.d.mts +37 -97
- package/dist/backend-a2/contract.mjs +1 -1
- package/dist/backend-a2/store-memory.d.mts +1 -1
- package/dist/backend-a2/store-memory.mjs +2 -2
- package/dist/backend-a2/store-postgres.d.mts +1 -1
- package/dist/backend-a2/store-postgres.mjs +1 -1
- package/dist/backend-a2/store-redis-http.d.mts +1 -1
- package/dist/backend-a2/store-redis-http.mjs +1 -1
- package/dist/backend-a2/store-redis.d.mts +1 -1
- package/dist/backend-a2/store-redis.mjs +1 -1
- package/dist/backend-a2/store-sqlite.d.mts +1 -1
- package/dist/backend-a2/store-sqlite.mjs +2 -2
- package/dist/backend-a2.d.mts +131 -240
- package/dist/backend-a2.mjs +2 -2
- package/dist/{backend-http-3PoBYQoo.d.mts → backend-http-BRrRbjRx.d.mts} +49 -45
- package/dist/{backend-http-BPaNYLWj.mjs → backend-http-CvUKOn-m.mjs} +11 -9
- package/dist/{backend-http-16SbvnAK.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-Kd8nvW5A.mjs → backend-vercel-Ba8JDtqW.mjs} +3 -3
- package/dist/{backend-vercel-qznuDxnN.d.mts → backend-vercel-BaSEUdwg.d.mts} +49 -45
- package/dist/{backend-vercel-C4Kee2-K.mjs → backend-vercel-CmY9yZ9m.mjs} +80 -15
- 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-CqILvdmr.d.mts → blob-Bx8QPWEd.d.mts} +2 -2
- package/dist/{blob-DQK5prXT.mjs → blob-WbB2DJKn.mjs} +30 -11
- package/dist/{chat-provider-D6jVT4wS.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-CjAatpV4.d.mts → client-COfYrgfV.d.mts} +184 -221
- package/dist/{commands-BtH69yqt.d.mts → commands-DK4W3HCJ.d.mts} +30 -10
- package/dist/{commands-B5DHjZKK.mjs → commands-L1JSGz2Z.mjs} +162 -45
- package/dist/{conformance-C7fwIvmN.mjs → conformance-DK43HiHf.mjs} +224 -20
- package/dist/conformance.d.mts +8 -2
- package/dist/conformance.mjs +1 -1
- package/dist/{engine-DkdkXQEy.mjs → engine-DurYIRt8.mjs} +29 -210
- package/dist/{errors-87TVtXeb.d.mts → errors-2vGs49Db.d.mts} +1 -1
- package/dist/{errors-C_5rB8Iz.mjs → errors-CJXhhzvT.mjs} +28 -23
- package/dist/{provider-BaVmi4Hi.mjs → events-lujPA3gQ.mjs} +80 -17
- package/dist/{exports-CG0P8Qi4.mjs → exports-CWIdN0Ub.mjs} +146 -80
- package/dist/exports-FBOQWT7O.d.mts +5 -0
- package/dist/{folds-DGYkMW19.mjs → folds-Cu9Q9zyQ.mjs} +2 -22
- package/dist/{folds-8wRRXjAt.d.mts → folds-DLUzfibJ.d.mts} +13 -40
- package/dist/{index-BygAXP7t.d.mts → index-Ho-ayyQL.d.mts} +2 -2
- package/dist/index.d.mts +7 -7
- package/dist/{index.default-DhkgpfR2.mjs → index.default-BUcwDaAe.mjs} +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.node.d.mts +7 -7
- package/dist/index.node.mjs +3 -3
- package/dist/inspect/app.js +62 -61
- package/dist/inspect/engine.js +3 -3
- package/dist/{main-D2s-QBOK.mjs → main-Dz-8komG.mjs} +189 -112
- package/dist/{middleware-DlJQYiwV.mjs → middleware-BavYSDUa.mjs} +1 -1
- package/dist/{node-D8Rtg1su.mjs → node-BgeGcwX0.mjs} +7 -3
- package/dist/{project-C_2xs57L.mjs → project-CQ4VUibA.mjs} +1 -1
- package/dist/{provider-Bm46LAdp.d.mts → provider-gynDUoyW.d.mts} +13 -43
- package/dist/{redis-tables-BWJZHUGg.mjs → redis-tables-DHf85ixI.mjs} +1 -19
- package/dist/{reducers-8jqLewgw.mjs → reducers-DcENwtwZ.mjs} +72 -75
- package/dist/{serve-lPoBDEds.mjs → serve-CTmow-YH.mjs} +6 -6
- package/dist/{server-D82a4uMe.mjs → server-DG8D6VV0.mjs} +4 -4
- package/dist/{serving-DKny38LK.mjs → serving-DBFRo6U_.mjs} +1 -1
- package/dist/{sql-Bj0fk6dn.mjs → sql-CT5xVuGr.mjs} +2 -8
- package/dist/{sqlite-db-Dc8G78XV.mjs → sqlite-db-0WeSOEZN.mjs} +1 -1
- 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 +2 -3
- 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-C5_yGRlL.mjs → tokens-Dnyd_y1n.mjs} +15 -6
- package/dist/{types-shZmGZ-q.d.mts → types-CmrFjpml.d.mts} +1 -3
- package/dist/{watchers-hash-B_5b0-ZG.mjs → watchers-hash-BjwlnGD9.mjs} +85 -105
- 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 +3 -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 +1 -1
- 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 +86 -41
- package/docs/04-reference/02-board.md +56 -23
- package/docs/04-reference/03-http.md +59 -32
- package/docs/04-reference/04-log.md +8 -11
- package/docs/04-reference/05-conformance.md +35 -6
- package/docs/index.md +2 -2
- package/package.json +6 -9
- package/sql/postgres/board.sql +1 -1
- package/sql/postgres/schema.sql +1 -1
- package/dist/events-qf5XG5VQ.mjs +0 -75
- package/dist/exports-CrGA5_yN.d.mts +0 -5
- 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
|
@@ -40,8 +40,10 @@ type CliCommand = {
|
|
|
40
40
|
readonly subcommands?: readonly CliCommand[];
|
|
41
41
|
/** Its arguments come before its subcommand, as in `sources <key> read <target>`. */
|
|
42
42
|
readonly argumentsBeforeSubcommand?: true;
|
|
43
|
-
/** It does something without a subcommand too, as `
|
|
43
|
+
/** It does something without a subcommand too, as `sources` lists the sync entries. */
|
|
44
44
|
readonly runsWithoutSubcommand?: true;
|
|
45
|
+
/** The subcommand its command runs when given none, as `channels` runs `channels list`. */
|
|
46
|
+
readonly default?: true;
|
|
45
47
|
/** Out of help: it still runs, for what calls it. */
|
|
46
48
|
readonly hidden?: true;
|
|
47
49
|
};
|
|
@@ -61,7 +63,7 @@ declare const options: {
|
|
|
61
63
|
readonly type: 'string';
|
|
62
64
|
readonly value: '@handle';
|
|
63
65
|
readonly optionalValue: true;
|
|
64
|
-
readonly description: 'mentioning a handle, or you without one';
|
|
66
|
+
readonly description: 'mentioning a handle or email, or you without one';
|
|
65
67
|
};
|
|
66
68
|
readonly label: {
|
|
67
69
|
readonly type: 'string';
|
|
@@ -202,14 +204,6 @@ declare const options: {
|
|
|
202
204
|
readonly value: 'duration';
|
|
203
205
|
readonly description: 'how long to wait (default: 30m)';
|
|
204
206
|
};
|
|
205
|
-
readonly code: {
|
|
206
|
-
readonly type: 'boolean';
|
|
207
|
-
readonly description: 'post a code from your account instead of signing in';
|
|
208
|
-
};
|
|
209
|
-
readonly 'no-wait': {
|
|
210
|
-
readonly type: 'boolean';
|
|
211
|
-
readonly description: "start linking, and don't wait for it to finish";
|
|
212
|
-
};
|
|
213
207
|
readonly 'no-open': {
|
|
214
208
|
readonly type: 'boolean';
|
|
215
209
|
readonly description: "don't open the browser";
|
|
@@ -245,6 +239,32 @@ declare const options: {
|
|
|
245
239
|
readonly value: 'N';
|
|
246
240
|
readonly description: 'repeating this many times';
|
|
247
241
|
};
|
|
242
|
+
readonly can: {
|
|
243
|
+
readonly type: 'string';
|
|
244
|
+
readonly value: 'scope';
|
|
245
|
+
readonly description: 'what it may do (default: *=read,post,react,label)';
|
|
246
|
+
};
|
|
247
|
+
readonly claim: {
|
|
248
|
+
readonly type: 'string';
|
|
249
|
+
readonly multiple: true;
|
|
250
|
+
readonly value: 'k=v';
|
|
251
|
+
readonly description: 'a claim its tokens carry; the same name again is any of them';
|
|
252
|
+
};
|
|
253
|
+
readonly profile: {
|
|
254
|
+
readonly type: 'string';
|
|
255
|
+
readonly value: 'name';
|
|
256
|
+
readonly description: 'the profile every request with it writes as';
|
|
257
|
+
};
|
|
258
|
+
readonly session: {
|
|
259
|
+
readonly type: 'string';
|
|
260
|
+
readonly value: 'id';
|
|
261
|
+
readonly description: 'the session every request with it writes from';
|
|
262
|
+
};
|
|
263
|
+
readonly expires: {
|
|
264
|
+
readonly type: 'string';
|
|
265
|
+
readonly value: 'duration';
|
|
266
|
+
readonly description: 'how long it lives (default: 30d, at most 90d)';
|
|
267
|
+
};
|
|
248
268
|
readonly port: {
|
|
249
269
|
readonly type: 'string';
|
|
250
270
|
readonly value: 'N';
|
|
@@ -15,7 +15,7 @@ const options = {
|
|
|
15
15
|
type: "string",
|
|
16
16
|
value: "@handle",
|
|
17
17
|
optionalValue: true,
|
|
18
|
-
description: "mentioning a handle, or you without one"
|
|
18
|
+
description: "mentioning a handle or email, or you without one"
|
|
19
19
|
},
|
|
20
20
|
label: {
|
|
21
21
|
type: "string",
|
|
@@ -156,14 +156,6 @@ const options = {
|
|
|
156
156
|
value: "duration",
|
|
157
157
|
description: "how long to wait (default: 30m)"
|
|
158
158
|
},
|
|
159
|
-
code: {
|
|
160
|
-
type: "boolean",
|
|
161
|
-
description: "post a code from your account instead of signing in"
|
|
162
|
-
},
|
|
163
|
-
"no-wait": {
|
|
164
|
-
type: "boolean",
|
|
165
|
-
description: "start linking, and don't wait for it to finish"
|
|
166
|
-
},
|
|
167
159
|
"no-open": {
|
|
168
160
|
type: "boolean",
|
|
169
161
|
description: "don't open the browser"
|
|
@@ -199,6 +191,32 @@ const options = {
|
|
|
199
191
|
value: "N",
|
|
200
192
|
description: "repeating this many times"
|
|
201
193
|
},
|
|
194
|
+
can: {
|
|
195
|
+
type: "string",
|
|
196
|
+
value: "scope",
|
|
197
|
+
description: "what it may do (default: *=read,post,react,label)"
|
|
198
|
+
},
|
|
199
|
+
claim: {
|
|
200
|
+
type: "string",
|
|
201
|
+
multiple: true,
|
|
202
|
+
value: "k=v",
|
|
203
|
+
description: "a claim its tokens carry; the same name again is any of them"
|
|
204
|
+
},
|
|
205
|
+
profile: {
|
|
206
|
+
type: "string",
|
|
207
|
+
value: "name",
|
|
208
|
+
description: "the profile every request with it writes as"
|
|
209
|
+
},
|
|
210
|
+
session: {
|
|
211
|
+
type: "string",
|
|
212
|
+
value: "id",
|
|
213
|
+
description: "the session every request with it writes from"
|
|
214
|
+
},
|
|
215
|
+
expires: {
|
|
216
|
+
type: "string",
|
|
217
|
+
value: "duration",
|
|
218
|
+
description: "how long it lives (default: 30d, at most 90d)"
|
|
219
|
+
},
|
|
202
220
|
port: {
|
|
203
221
|
type: "string",
|
|
204
222
|
value: "N",
|
|
@@ -284,70 +302,169 @@ const commands = [
|
|
|
284
302
|
}]
|
|
285
303
|
},
|
|
286
304
|
{
|
|
287
|
-
group: "
|
|
288
|
-
name: "
|
|
289
|
-
description: "
|
|
305
|
+
group: "you",
|
|
306
|
+
name: "tokens",
|
|
307
|
+
description: "tokens that act as you, narrowed, for a widget, a sandbox, or a script",
|
|
290
308
|
arguments: [],
|
|
291
309
|
options: [],
|
|
310
|
+
runsWithoutSubcommand: true,
|
|
292
311
|
examples: [{
|
|
293
|
-
name: "list
|
|
294
|
-
value: "
|
|
295
|
-
}]
|
|
312
|
+
name: "list your tokens",
|
|
313
|
+
value: "tokens"
|
|
314
|
+
}],
|
|
315
|
+
subcommands: [
|
|
316
|
+
{
|
|
317
|
+
name: "list",
|
|
318
|
+
default: true,
|
|
319
|
+
description: "your live tokens, or with manage, everyone's",
|
|
320
|
+
arguments: [],
|
|
321
|
+
options: [],
|
|
322
|
+
examples: [{
|
|
323
|
+
name: "list your tokens",
|
|
324
|
+
value: "tokens list"
|
|
325
|
+
}]
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: "create",
|
|
329
|
+
description: "mint one, and print its secret once",
|
|
330
|
+
arguments: [{
|
|
331
|
+
name: "name",
|
|
332
|
+
required: true,
|
|
333
|
+
description: "what it is for, e.g. feedback-widget"
|
|
334
|
+
}],
|
|
335
|
+
options: [
|
|
336
|
+
option("can"),
|
|
337
|
+
option("profile"),
|
|
338
|
+
option("session"),
|
|
339
|
+
option("expires")
|
|
340
|
+
],
|
|
341
|
+
examples: [{
|
|
342
|
+
name: "a token that only posts feedback",
|
|
343
|
+
value: "tokens create feedback-widget --can feedback/=post"
|
|
344
|
+
}, {
|
|
345
|
+
name: "a week's token for a sandbox agent",
|
|
346
|
+
value: "tokens create sandbox-7 --can dev/=read,post,react --profile lemur --expires 7d"
|
|
347
|
+
}]
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "revoke",
|
|
351
|
+
description: "stop one, and every token minted with it",
|
|
352
|
+
arguments: [{
|
|
353
|
+
name: "token",
|
|
354
|
+
required: true,
|
|
355
|
+
description: "its ID, as tokens shows it (t12)"
|
|
356
|
+
}],
|
|
357
|
+
options: [],
|
|
358
|
+
examples: [{
|
|
359
|
+
name: "revoke a token",
|
|
360
|
+
value: "tokens revoke t12"
|
|
361
|
+
}]
|
|
362
|
+
}
|
|
363
|
+
]
|
|
296
364
|
},
|
|
297
365
|
{
|
|
298
366
|
group: "the board",
|
|
299
|
-
name: "
|
|
300
|
-
description: "
|
|
367
|
+
name: "grants",
|
|
368
|
+
description: "who the board lets in beside its code's rules: tokens from an OIDC issuer with these claims",
|
|
301
369
|
arguments: [],
|
|
302
370
|
options: [],
|
|
371
|
+
runsWithoutSubcommand: true,
|
|
303
372
|
examples: [{
|
|
304
|
-
name: "list
|
|
305
|
-
value: "
|
|
306
|
-
}]
|
|
373
|
+
name: "list the grants",
|
|
374
|
+
value: "grants"
|
|
375
|
+
}],
|
|
376
|
+
subcommands: [
|
|
377
|
+
{
|
|
378
|
+
name: "list",
|
|
379
|
+
default: true,
|
|
380
|
+
description: "the board's grants",
|
|
381
|
+
arguments: [],
|
|
382
|
+
options: [],
|
|
383
|
+
examples: [{
|
|
384
|
+
name: "list the grants",
|
|
385
|
+
value: "grants list"
|
|
386
|
+
}]
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "create",
|
|
390
|
+
description: "let them in, or change what they may do",
|
|
391
|
+
arguments: [{
|
|
392
|
+
name: "issuer",
|
|
393
|
+
required: true,
|
|
394
|
+
description: "vercel, github, or an https URL"
|
|
395
|
+
}],
|
|
396
|
+
options: [option("claim"), option("can")],
|
|
397
|
+
examples: [{
|
|
398
|
+
name: "let a workflow post in ci/",
|
|
399
|
+
value: "grants create github --claim repository=acme/web --can ci/=read,post"
|
|
400
|
+
}, {
|
|
401
|
+
name: "let a project's production post feedback",
|
|
402
|
+
value: "grants create vercel --claim project_id=prj_123 --claim environment=production --can feedback/=post"
|
|
403
|
+
}]
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: "revoke",
|
|
407
|
+
description: "stop letting them in",
|
|
408
|
+
arguments: [{
|
|
409
|
+
name: "grant",
|
|
410
|
+
required: true,
|
|
411
|
+
description: "its ID, as grants shows it (g3)"
|
|
412
|
+
}],
|
|
413
|
+
options: [],
|
|
414
|
+
examples: [{
|
|
415
|
+
name: "revoke a grant",
|
|
416
|
+
value: "grants revoke g3"
|
|
417
|
+
}]
|
|
418
|
+
}
|
|
419
|
+
]
|
|
307
420
|
},
|
|
308
421
|
{
|
|
309
422
|
group: "the board",
|
|
310
|
-
name: "
|
|
311
|
-
description: "
|
|
423
|
+
name: "channels",
|
|
424
|
+
description: "channels, grouped by path prefix: a first post makes one, and so does create",
|
|
312
425
|
arguments: [],
|
|
313
426
|
options: [],
|
|
314
427
|
runsWithoutSubcommand: true,
|
|
315
428
|
examples: [{
|
|
316
|
-
name: "list
|
|
317
|
-
value: "
|
|
429
|
+
name: "list the channels",
|
|
430
|
+
value: "channels"
|
|
318
431
|
}],
|
|
319
432
|
subcommands: [{
|
|
320
|
-
name: "
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
description: "the sync entry, e.g. slack"
|
|
326
|
-
}],
|
|
327
|
-
options: [
|
|
328
|
-
option("code"),
|
|
329
|
-
option("no-wait"),
|
|
330
|
-
option("no-open")
|
|
331
|
-
],
|
|
433
|
+
name: "list",
|
|
434
|
+
default: true,
|
|
435
|
+
description: "channels, grouped by path prefix",
|
|
436
|
+
arguments: [],
|
|
437
|
+
options: [],
|
|
332
438
|
examples: [{
|
|
333
|
-
name: "
|
|
334
|
-
value: "
|
|
439
|
+
name: "list the channels",
|
|
440
|
+
value: "channels list"
|
|
335
441
|
}]
|
|
336
442
|
}, {
|
|
337
|
-
name: "
|
|
338
|
-
description: "
|
|
443
|
+
name: "create",
|
|
444
|
+
description: "a channel, before anything posts to it (one that exists stays as it is)",
|
|
339
445
|
arguments: [{
|
|
340
|
-
name: "
|
|
446
|
+
name: "channel",
|
|
341
447
|
required: true,
|
|
342
|
-
description: "
|
|
448
|
+
description: "its path, such as dev/alerts"
|
|
343
449
|
}],
|
|
344
450
|
options: [],
|
|
345
451
|
examples: [{
|
|
346
|
-
name: "
|
|
347
|
-
value: "
|
|
452
|
+
name: "make a channel for alerts",
|
|
453
|
+
value: "channels create dev/alerts"
|
|
348
454
|
}]
|
|
349
455
|
}]
|
|
350
456
|
},
|
|
457
|
+
{
|
|
458
|
+
group: "the board",
|
|
459
|
+
name: "actors",
|
|
460
|
+
description: "actors and their handles",
|
|
461
|
+
arguments: [],
|
|
462
|
+
options: [],
|
|
463
|
+
examples: [{
|
|
464
|
+
name: "list who has written here",
|
|
465
|
+
value: "actors"
|
|
466
|
+
}]
|
|
467
|
+
},
|
|
351
468
|
{
|
|
352
469
|
group: "messages",
|
|
353
470
|
name: "read",
|
|
@@ -558,7 +675,7 @@ const commands = [
|
|
|
558
675
|
{
|
|
559
676
|
group: "messages",
|
|
560
677
|
name: "typing",
|
|
561
|
-
description: "show typing in the thread's synced platform threads
|
|
678
|
+
description: "show typing in the thread's synced platform threads",
|
|
562
679
|
arguments: [id(), {
|
|
563
680
|
name: "status",
|
|
564
681
|
required: false,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { u as BoardError } from "./errors-
|
|
2
|
-
import { r as compileAccess } from "./access-
|
|
1
|
+
import { u as BoardError } from "./errors-CJXhhzvT.mjs";
|
|
2
|
+
import { r as compileAccess } from "./access-CY5xVMIK.mjs";
|
|
3
3
|
//#region src/conformance/index.ts
|
|
4
4
|
var Mismatch = class extends Error {};
|
|
5
5
|
const show = (value) => JSON.stringify(value) ?? String(value);
|
|
@@ -18,6 +18,16 @@ async function rejects(attempt, status, code, what) {
|
|
|
18
18
|
}
|
|
19
19
|
throw new Mismatch(`${what}: expected ${status} ${code}, and it succeeded`);
|
|
20
20
|
}
|
|
21
|
+
/** That `attempt` is refused with `status`, whatever code the board gives it. */
|
|
22
|
+
async function refused(attempt, status, what) {
|
|
23
|
+
try {
|
|
24
|
+
await attempt;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof BoardError && error.status === status) return;
|
|
27
|
+
throw new Mismatch(`${what}: expected ${status}, got ${error instanceof BoardError ? `${error.status} ${error.code}` : String(error)}`);
|
|
28
|
+
}
|
|
29
|
+
throw new Mismatch(`${what}: expected ${status}, and it succeeded`);
|
|
30
|
+
}
|
|
21
31
|
/** Where a registration is woken, to put back after a check replaces it: its path (and branch) on a board that wakes by path, or its url. */
|
|
22
32
|
function wokenAt(registered) {
|
|
23
33
|
if (!registered?.path) return { url: registered?.url ?? null };
|
|
@@ -32,9 +42,16 @@ function isRef(value, what) {
|
|
|
32
42
|
}
|
|
33
43
|
/**
|
|
34
44
|
* The groups a board may keep none of, saying so with a 501: a board on `a2()` without a scheduler
|
|
35
|
-
* answers `no_scheduler`, and without a blob store `no_files
|
|
45
|
+
* answers `no_scheduler`, and without a blob store `no_files`; one whose `access` is one function keeps
|
|
46
|
+
* no grants, `no_grants`. A board that's meant to keep something and doesn't yet answers otherwise,
|
|
47
|
+
* and fails.
|
|
36
48
|
*/
|
|
37
|
-
const OPTIONAL = /* @__PURE__ */ new Set([
|
|
49
|
+
const OPTIONAL = /* @__PURE__ */ new Set([
|
|
50
|
+
"schedules",
|
|
51
|
+
"grants",
|
|
52
|
+
"tokens",
|
|
53
|
+
"attachments"
|
|
54
|
+
]);
|
|
38
55
|
const needRoot = (ctx) => {
|
|
39
56
|
check(ctx.root && ctx.reply, "posting failed, so there's nothing to read");
|
|
40
57
|
return {
|
|
@@ -42,6 +59,23 @@ const needRoot = (ctx) => {
|
|
|
42
59
|
reply: ctx.reply
|
|
43
60
|
};
|
|
44
61
|
};
|
|
62
|
+
/** A grant for a project that doesn't exist, from an environment its deployments would have: it lets nobody in. */
|
|
63
|
+
const grantFor = (ctx) => ({
|
|
64
|
+
issuer: "vercel",
|
|
65
|
+
claims: {
|
|
66
|
+
environment: "preview",
|
|
67
|
+
project_id: `prj_conformance${ctx.labels.conformance}`
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
/** Sets the check's grant, or says why it can't: changing who the board trusts takes `manage` in every channel. */
|
|
71
|
+
async function firstGrant(ctx) {
|
|
72
|
+
try {
|
|
73
|
+
return await ctx.board.createGrant(grantFor(ctx));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error instanceof BoardError && error.status === 403) return { skip: "this token can't change the board's grants" };
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
45
79
|
/** The sync key the sync checks register, a platform of their own, so nothing reaches a real one. */
|
|
46
80
|
const SYNC_KEY = "conformance";
|
|
47
81
|
/**
|
|
@@ -354,10 +388,25 @@ const CHECKS = [
|
|
|
354
388
|
run: async (ctx) => {
|
|
355
389
|
const found = (await ctx.board.channels()).find((c) => c.path === ctx.channel);
|
|
356
390
|
check(found, `the channels should include ${ctx.channel}`);
|
|
357
|
-
|
|
391
|
+
for (const field of [
|
|
392
|
+
"lastIndex",
|
|
393
|
+
"messages",
|
|
394
|
+
"threads"
|
|
395
|
+
]) check(found[field] === void 0 || typeof found[field] === "number", `a channel's ${field} should be a number or left out, got ${show(found[field])}`);
|
|
358
396
|
check((await ctx.board.actors()).some((a) => a.id === ctx.me.actor), `the actors should include ${ctx.me.actor}`);
|
|
359
397
|
}
|
|
360
398
|
},
|
|
399
|
+
{
|
|
400
|
+
group: "core",
|
|
401
|
+
name: "creates a channel only once: one a post made is there already",
|
|
402
|
+
run: async (ctx) => {
|
|
403
|
+
needRoot(ctx);
|
|
404
|
+
const { channel, created } = await ctx.board.createChannel(ctx.channel);
|
|
405
|
+
same(created, false, `creating ${ctx.channel}, which this run posted to`);
|
|
406
|
+
same(channel.path, ctx.channel, "the channel's path");
|
|
407
|
+
await rejects(ctx.board.createChannel("Not A Channel"), 400, "invalid_request", "creating a malformed channel");
|
|
408
|
+
}
|
|
409
|
+
},
|
|
361
410
|
{
|
|
362
411
|
group: "core",
|
|
363
412
|
name: "answers 404 for an ID that names nothing",
|
|
@@ -411,6 +460,174 @@ const CHECKS = [
|
|
|
411
460
|
}
|
|
412
461
|
}
|
|
413
462
|
},
|
|
463
|
+
{
|
|
464
|
+
group: "grants",
|
|
465
|
+
name: "grants a project, lists it, takes the same grant again as one, and revokes it",
|
|
466
|
+
run: async (ctx) => {
|
|
467
|
+
const asked = grantFor(ctx);
|
|
468
|
+
const first = await firstGrant(ctx);
|
|
469
|
+
if ("skip" in first) return first.skip;
|
|
470
|
+
try {
|
|
471
|
+
isRef(first.grant.id, "a grant's id");
|
|
472
|
+
same(first.created, true, "a new grant: created");
|
|
473
|
+
same(first.grant.issuer, "vercel", "the grant's issuer");
|
|
474
|
+
same(first.grant.claims.project_id, asked.claims.project_id, "the grant's project_id");
|
|
475
|
+
check(first.grant.can["*"]?.includes("post"), `a grant with no can should let them post everywhere, got ${show(first.grant.can)}`);
|
|
476
|
+
same(first.grant.by, ctx.me.actor, "the grant's by");
|
|
477
|
+
const again = await ctx.board.createGrant(asked);
|
|
478
|
+
same(again.created, false, "the same issuer and claims again: created");
|
|
479
|
+
same(again.grant.id, first.grant.id, "the same issuer and claims again: its id");
|
|
480
|
+
same((await ctx.board.grants()).filter((grant) => grant.id === first.grant.id).length, 1, "grants listing it");
|
|
481
|
+
} catch (error) {
|
|
482
|
+
await ctx.board.revokeGrant(first.grant.id).catch(() => {});
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
same((await ctx.board.revokeGrant(first.grant.id)).revoked, true, "revoking it");
|
|
486
|
+
same((await ctx.board.revokeGrant(first.grant.id)).revoked, false, "revoking it again");
|
|
487
|
+
check(!(await ctx.board.grants()).some((grant) => grant.id === first.grant.id), "grants shouldn't list it once it's revoked");
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
group: "grants",
|
|
492
|
+
name: "changes a grant's can when it's set again",
|
|
493
|
+
run: async (ctx) => {
|
|
494
|
+
const first = await firstGrant(ctx);
|
|
495
|
+
if ("skip" in first) return first.skip;
|
|
496
|
+
const { grant } = first;
|
|
497
|
+
try {
|
|
498
|
+
const narrowed = await ctx.board.createGrant({
|
|
499
|
+
...grantFor(ctx),
|
|
500
|
+
can: { [ctx.channel]: ["post"] }
|
|
501
|
+
});
|
|
502
|
+
same(narrowed.created, false, "setting its can: created");
|
|
503
|
+
same(narrowed.grant.id, grant.id, "setting its can: its id");
|
|
504
|
+
same(show(narrowed.grant.can), show({ [ctx.channel]: ["post"] }), "the grant's can");
|
|
505
|
+
} finally {
|
|
506
|
+
await ctx.board.revokeGrant(grant.id).catch(() => {});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
group: "grants",
|
|
512
|
+
name: "grants a GitHub repository's workflows, and another team's project",
|
|
513
|
+
run: async (ctx) => {
|
|
514
|
+
const first = await firstGrant(ctx);
|
|
515
|
+
if ("skip" in first) return first.skip;
|
|
516
|
+
const made = [first.grant.id];
|
|
517
|
+
try {
|
|
518
|
+
const github = await ctx.board.createGrant({
|
|
519
|
+
issuer: "github",
|
|
520
|
+
claims: { repository: `acme/conformance-${ctx.labels.conformance}` },
|
|
521
|
+
can: { [ctx.channel]: ["post"] }
|
|
522
|
+
});
|
|
523
|
+
made.push(github.grant.id);
|
|
524
|
+
same(github.grant.issuer, "github", "the GitHub grant's issuer");
|
|
525
|
+
const theirs = await ctx.board.createGrant({
|
|
526
|
+
issuer: "vercel",
|
|
527
|
+
claims: {
|
|
528
|
+
environment: "production",
|
|
529
|
+
owner_id: `team_conformance${ctx.labels.conformance}`,
|
|
530
|
+
project_id: `prj_conformance${ctx.labels.conformance}x`
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
made.push(theirs.grant.id);
|
|
534
|
+
same(theirs.grant.claims.owner_id, `team_conformance${ctx.labels.conformance}`, "another team's grant's owner_id");
|
|
535
|
+
const listed = new Set((await ctx.board.grants()).map((grant) => grant.id));
|
|
536
|
+
check(made.every((id) => listed.has(id)), `grants should list ${made.join(", ")}`);
|
|
537
|
+
} finally {
|
|
538
|
+
for (const id of made) await ctx.board.revokeGrant(id).catch(() => {});
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
group: "grants",
|
|
544
|
+
name: "keeps two environments of one project as two grants, and revokes one without the other",
|
|
545
|
+
run: async (ctx) => {
|
|
546
|
+
const first = await firstGrant(ctx);
|
|
547
|
+
if ("skip" in first) return first.skip;
|
|
548
|
+
const made = [first.grant.id];
|
|
549
|
+
try {
|
|
550
|
+
const production = await ctx.board.createGrant({
|
|
551
|
+
...grantFor(ctx),
|
|
552
|
+
claims: {
|
|
553
|
+
...grantFor(ctx).claims,
|
|
554
|
+
environment: "production"
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
made.push(production.grant.id);
|
|
558
|
+
same(production.created, true, "another environment: created");
|
|
559
|
+
check(production.grant.id !== first.grant.id, "another environment should be another grant");
|
|
560
|
+
same((await ctx.board.revokeGrant(first.grant.id)).revoked, true, "revoking the preview grant");
|
|
561
|
+
const listed = (await ctx.board.grants()).map((grant) => grant.id);
|
|
562
|
+
check(listed.includes(production.grant.id) && !listed.includes(first.grant.id), `grants should list ${production.grant.id} and not ${first.grant.id}, got ${show(listed)}`);
|
|
563
|
+
} finally {
|
|
564
|
+
for (const id of made) await ctx.board.revokeGrant(id).catch(() => {});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
{
|
|
569
|
+
group: "tokens",
|
|
570
|
+
name: "mints a token, shows its secret only then, lists it, and revokes it",
|
|
571
|
+
run: async (ctx) => {
|
|
572
|
+
const name = `conformance-${ctx.labels.conformance}`;
|
|
573
|
+
const { token, secret } = await ctx.board.createToken({
|
|
574
|
+
name,
|
|
575
|
+
can: { [ctx.channel]: ["read"] },
|
|
576
|
+
expires: "1h"
|
|
577
|
+
});
|
|
578
|
+
try {
|
|
579
|
+
isRef(token.id, "a token's id");
|
|
580
|
+
check(typeof secret === "string" && secret.length >= 32, `a token's secret should be a string of at least 32 characters, got ${typeof secret}`);
|
|
581
|
+
same(token.name, name, "the token's name");
|
|
582
|
+
same(show(token.can), show({ [ctx.channel]: ["read"] }), "the token's can");
|
|
583
|
+
const left = Date.parse(token.expiresAt) - Date.now();
|
|
584
|
+
check(left > 33e5 && left <= 3605e3, `a token minted for 1h should expire in an hour, got ${token.expiresAt}`);
|
|
585
|
+
const listed = await ctx.board.tokens();
|
|
586
|
+
check(listed.some((found) => found.id === token.id), `tokens should list ${token.id}`);
|
|
587
|
+
check(!JSON.stringify(listed).includes(secret), "tokens shouldn't show a token's secret");
|
|
588
|
+
} catch (error) {
|
|
589
|
+
await ctx.board.revokeToken(token.id).catch(() => {});
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
same((await ctx.board.revokeToken(token.id)).revoked, true, "revoking it");
|
|
593
|
+
same((await ctx.board.revokeToken(token.id)).revoked, false, "revoking it again");
|
|
594
|
+
check(!(await ctx.board.tokens()).some((found) => found.id === token.id), "tokens shouldn't list it once it's revoked");
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
group: "tokens",
|
|
599
|
+
name: "lets a token do what its can says, as whoever minted it, and nothing once it's revoked",
|
|
600
|
+
run: async (ctx) => {
|
|
601
|
+
if (!ctx.withToken) return "this run can't call the board with a token's secret";
|
|
602
|
+
const { root } = needRoot(ctx);
|
|
603
|
+
const { token, secret } = await ctx.board.createToken({
|
|
604
|
+
name: `conformance-${ctx.labels.conformance}-reads`,
|
|
605
|
+
can: { [ctx.channel]: ["read"] },
|
|
606
|
+
expires: "1h"
|
|
607
|
+
});
|
|
608
|
+
const holder = ctx.withToken(secret);
|
|
609
|
+
try {
|
|
610
|
+
const me = await holder.me();
|
|
611
|
+
same(me.principal, ctx.me.principal, "a token's principal, its minter's");
|
|
612
|
+
const want = { [ctx.channel]: ["read"] };
|
|
613
|
+
check(show(me.can) === show(want), `what a token that may only read says it may do: expected ${show(want)}, got ${show(me.can)}`);
|
|
614
|
+
same((await holder.thread(root.id)).message.id, root.id, "a thread read with a token that may read it");
|
|
615
|
+
await refused(holder.post(ctx.channel, {
|
|
616
|
+
text: "conformance: a token that may only read",
|
|
617
|
+
labels: ctx.labels,
|
|
618
|
+
noSync: true
|
|
619
|
+
}).then((message) => {
|
|
620
|
+
ctx.posted.push(message.id);
|
|
621
|
+
return message;
|
|
622
|
+
}), 403, "a post with a token that may only read");
|
|
623
|
+
} catch (error) {
|
|
624
|
+
await ctx.board.revokeToken(token.id).catch(() => {});
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
627
|
+
same((await ctx.board.revokeToken(token.id)).revoked, true, "revoking it");
|
|
628
|
+
await refused(holder.me(), 401, "a request with a revoked token");
|
|
629
|
+
}
|
|
630
|
+
},
|
|
414
631
|
{
|
|
415
632
|
group: "attachments",
|
|
416
633
|
name: "takes a file, posts it, and gives it back",
|
|
@@ -703,20 +920,6 @@ const CHECKS = [
|
|
|
703
920
|
same(thread.message.noSync, true, "a post's noSync, read back");
|
|
704
921
|
same(thread.replies.find((m) => m.id === reply.id)?.noSync, true, "a reply's noSync, read back");
|
|
705
922
|
}
|
|
706
|
-
},
|
|
707
|
-
{
|
|
708
|
-
group: "sync",
|
|
709
|
-
name: "finds no one for a platform account nobody linked",
|
|
710
|
-
run: async (ctx) => {
|
|
711
|
-
let found;
|
|
712
|
-
try {
|
|
713
|
-
found = await ctx.board.syncFind({ account: `${SYNC_KEY}:user:${ctx.labels.conformance}` });
|
|
714
|
-
} catch (error) {
|
|
715
|
-
if (error instanceof BoardError && error.status === 403) return "this token can't look up a sync here";
|
|
716
|
-
throw error;
|
|
717
|
-
}
|
|
718
|
-
same(found.person, null, "who an account nobody linked is");
|
|
719
|
-
}
|
|
720
923
|
}
|
|
721
924
|
];
|
|
722
925
|
/** Runs the checks against `board`, in order, and reports each. It never throws for what the board answers. */
|
|
@@ -754,7 +957,8 @@ async function conformance(board, options = {}) {
|
|
|
754
957
|
limit: 1
|
|
755
958
|
})).cursor,
|
|
756
959
|
me,
|
|
757
|
-
posted: []
|
|
960
|
+
posted: [],
|
|
961
|
+
...options.withToken ? { withToken: options.withToken } : {}
|
|
758
962
|
};
|
|
759
963
|
try {
|
|
760
964
|
for (const { group, name, run } of CHECKS) try {
|
package/dist/conformance.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { f as Board } from "./backend-Bob-5CM7.mjs";
|
|
2
2
|
//#region src/conformance/index.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Checks that a board speaks the protocol, through a handle on it (`conformance(board())`), as
|
|
@@ -7,7 +7,7 @@ import { d as Board } from "./backend-n3hajfrD.mjs";
|
|
|
7
7
|
* (`conformance` by default), keeps every post on the board, and deletes what it posted.
|
|
8
8
|
*/
|
|
9
9
|
/** What a check is about, for the report's headings. */
|
|
10
|
-
export type CheckGroup = 'core' | 'schedules' | 'attachments' | 'watchers' | 'sync';
|
|
10
|
+
export type CheckGroup = 'core' | 'schedules' | 'grants' | 'tokens' | 'attachments' | 'watchers' | 'sync';
|
|
11
11
|
export type CheckResult = {
|
|
12
12
|
group: CheckGroup;
|
|
13
13
|
name: string;
|
|
@@ -24,6 +24,12 @@ export type ConformanceReport = {
|
|
|
24
24
|
export type ConformanceOptions = {
|
|
25
25
|
/** Where it posts. Default `conformance`. */
|
|
26
26
|
channel?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The same board, with a board token's secret as the credential (as `BOARD_TOKEN` sends it):
|
|
29
|
+
* the tokens check calls it so to see that a token does only what its `can` says. Without it,
|
|
30
|
+
* that check is skipped.
|
|
31
|
+
*/
|
|
32
|
+
withToken?: (secret: string) => Board;
|
|
27
33
|
};
|
|
28
34
|
/** Runs the checks against `board`, in order, and reports each. It never throws for what the board answers. */
|
|
29
35
|
export declare function conformance(board: Board, options?: ConformanceOptions): Promise<ConformanceReport>;
|
package/dist/conformance.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as conformance } from "./conformance-
|
|
1
|
+
import { t as conformance } from "./conformance-DK43HiHf.mjs";
|
|
2
2
|
export { conformance };
|