@vercel/board 0.0.0 → 0.16.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 +180 -0
- package/dist/a2-C41hD8SF.mjs +3045 -0
- package/dist/a2-D24cvwpN.d.mts +3359 -0
- package/dist/access-CJU2-vaa.mjs +226 -0
- package/dist/access-CV96SYgr.d.mts +32 -0
- package/dist/access.d.mts +30 -0
- package/dist/access.mjs +4 -0
- package/dist/actors-DjMeZpnU.mjs +224 -0
- package/dist/attempts-Bxp9KgPf.mjs +919 -0
- package/dist/auth-COzc09sn.mjs +276 -0
- package/dist/auth-CU56Q2Xh.d.mts +204 -0
- package/dist/backend-a2/blob-vercel.d.mts +8 -0
- package/dist/backend-a2/blob-vercel.mjs +81 -0
- package/dist/backend-a2/contract.d.mts +18 -0
- package/dist/backend-a2/contract.mjs +5 -0
- package/dist/backend-a2/scheduler-local.d.mts +11 -0
- package/dist/backend-a2/scheduler-local.mjs +2 -0
- package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
- package/dist/backend-a2/scheduler-qstash.mjs +2 -0
- package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
- package/dist/backend-a2/scheduler-vercel.mjs +2 -0
- package/dist/backend-a2/store-memory.d.mts +11 -0
- package/dist/backend-a2/store-memory.mjs +19 -0
- package/dist/backend-a2/store-postgres.d.mts +38 -0
- package/dist/backend-a2/store-postgres.mjs +110 -0
- package/dist/backend-a2/store-redis-http.d.mts +17 -0
- package/dist/backend-a2/store-redis-http.mjs +32 -0
- package/dist/backend-a2/store-redis.d.mts +23 -0
- package/dist/backend-a2/store-redis.mjs +39 -0
- package/dist/backend-a2/store-sqlite.d.mts +12 -0
- package/dist/backend-a2/store-sqlite.mjs +31 -0
- package/dist/backend-a2.d.mts +4 -0
- package/dist/backend-a2.mjs +2 -0
- package/dist/backend-http-0roEmjl0.mjs +613 -0
- package/dist/backend-http-BVRno-ca.mjs +469 -0
- package/dist/backend-http-Bid7RIpC.d.mts +630 -0
- package/dist/backend-http.d.mts +10 -0
- package/dist/backend-http.mjs +2 -0
- package/dist/backend-http.node.d.mts +10 -0
- package/dist/backend-http.node.mjs +2 -0
- package/dist/blob-C0J3wbGv.d.mts +39 -0
- package/dist/bots-CGkO76qH.mjs +67 -0
- package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
- package/dist/chat-provider-jFSeyNmB.mjs +389 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2252 -0
- package/dist/client-DGDPaN7U.mjs +380 -0
- package/dist/conformance-DRR23YNy.mjs +512 -0
- package/dist/conformance.d.mts +30 -0
- package/dist/conformance.mjs +2 -0
- package/dist/contract-CuJmlrhw.mjs +566 -0
- package/dist/emoji-CTLpvH-A.mjs +445 -0
- package/dist/engine-Bir5s8ts.mjs +1449 -0
- package/dist/errors-DX3hpwWt.mjs +64 -0
- package/dist/exports-DLiE7fLY.d.mts +4 -0
- package/dist/exports-Dvc1o52I.mjs +1285 -0
- package/dist/files-SniSmeEr.mjs +47 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.default-Bkg77Pxn.mjs +12 -0
- package/dist/index.mjs +19 -0
- package/dist/index.node.d.mts +14 -0
- package/dist/index.node.mjs +5 -0
- package/dist/inspect/app.js +416 -0
- package/dist/inspect/engine.js +5 -0
- package/dist/local-Cr_dSjvs.mjs +44 -0
- package/dist/own-OwgPWdmh.mjs +15 -0
- package/dist/project-CFiArraX.mjs +124 -0
- package/dist/provider-BGjz-mDa.d.mts +2473 -0
- package/dist/provider-CuewcDNZ.mjs +205 -0
- package/dist/redis-tables-B35vv4CZ.mjs +842 -0
- package/dist/reducers-B_vUGQRp.d.mts +1172 -0
- package/dist/reducers-mUPFuFJV.mjs +293 -0
- package/dist/schemas-Be08nyfW.mjs +74 -0
- package/dist/schemas-CWxs3ECJ.d.mts +50 -0
- package/dist/serve-BsOOc-6_.mjs +170 -0
- package/dist/server-jwPY7_wi.mjs +17 -0
- package/dist/serving-BNbzc96L.mjs +2 -0
- package/dist/sql-DbiEnqM0.mjs +571 -0
- package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
- package/dist/sync-github.d.mts +473 -0
- package/dist/sync-github.mjs +603 -0
- package/dist/sync-kapso.d.mts +175 -0
- package/dist/sync-kapso.mjs +86 -0
- package/dist/sync-slack.d.mts +364 -0
- package/dist/sync-slack.mjs +728 -0
- package/dist/sync.chat.d.mts +2 -0
- package/dist/sync.chat.mjs +2 -0
- package/dist/sync.d.mts +3 -0
- package/dist/sync.mjs +4 -0
- package/dist/targets-yM5JBUgr.mjs +94 -0
- package/dist/tokens-DKLn5P9B.mjs +256 -0
- package/dist/types-BHCjTZdB.d.mts +104 -0
- package/dist/users-Dls7_ANV.mjs +42 -0
- package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
- package/dist/watchers-hash-CLjsa6od.mjs +225 -0
- package/docs/01-board/01-mounting.md +506 -0
- package/docs/01-board/02-database.md +258 -0
- package/docs/01-board/03-identity.md +264 -0
- package/docs/01-board/04-messages.md +192 -0
- package/docs/01-board/05-reading.md +177 -0
- package/docs/01-board/06-watchers.md +249 -0
- package/docs/01-board/07-schedules.md +133 -0
- package/docs/01-board/08-middleware.md +139 -0
- package/docs/01-board/09-access.md +501 -0
- package/docs/01-quickstart.md +196 -0
- package/docs/02-sync/01-overview.md +290 -0
- package/docs/02-sync/02-callbacks.md +265 -0
- package/docs/02-sync/03-slack.md +248 -0
- package/docs/02-sync/04-github.md +201 -0
- package/docs/02-sync/05-whatsapp.md +148 -0
- package/docs/02-sync/06-providers.md +228 -0
- package/docs/03-guides/01-coding-agents.md +226 -0
- package/docs/03-guides/02-inspector.md +104 -0
- package/docs/03-guides/03-ci.md +251 -0
- package/docs/03-guides/04-triage.md +233 -0
- package/docs/03-guides/05-agent-factory.md +142 -0
- package/docs/04-reference/01-cli.md +606 -0
- package/docs/04-reference/02-board.md +579 -0
- package/docs/04-reference/03-http.md +489 -0
- package/docs/04-reference/04-log.md +250 -0
- package/docs/04-reference/05-conformance.md +97 -0
- package/docs/index.md +186 -0
- package/package.json +168 -1
- package/sql/postgres/board.sql +145 -0
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +451 -0
package/dist/sync.d.mts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { _ as ExternalFile, g as ExternalEvent, h as ExternalAuthor } from "./reducers-B_vUGQRp.mjs";
|
|
2
|
+
import { C as SyncResult, E as eventSchema, S as SyncOverrides, T as envelope, _ as connectUserTokens, a as ProviderFile, b as ProviderEvent, c as Receive, d as ResolvedTarget, f as SyncClient, g as UserTokens, h as SyncProvider, i as ProviderDelivery, l as ReceivedEvent, m as SyncHandler, o as ProviderOptions, p as SyncContext, r as FileDelivery, s as ProviderPost, t as EventMapping, u as RenderInput, v as withUserTokens, w as SyncRoot, x as RootOverride, y as EventBase } from "./provider-BGjz-mDa.mjs";
|
|
3
|
+
export { EventBase, type EventMapping, type ExternalAuthor, ExternalEvent, type ExternalFile, type FileDelivery, type ProviderDelivery, type ProviderEvent, type ProviderFile, type ProviderOptions, type ProviderPost, type Receive, ReceivedEvent, type RenderInput, ResolvedTarget, type RootOverride, type SyncClient, type SyncContext, type SyncHandler, SyncOverrides, SyncProvider, type SyncResult, type SyncRoot, type UserTokens, connectUserTokens, envelope, eventSchema, withUserTokens };
|
package/dist/sync.mjs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { r as ExternalEvent } from "./contract-CuJmlrhw.mjs";
|
|
2
|
+
import { a as EventBase, c as eventSchema, n as ResolvedTarget, o as SyncOverrides, r as SyncProvider, s as envelope, t as ReceivedEvent } from "./provider-CuewcDNZ.mjs";
|
|
3
|
+
import { r as withUserTokens, t as connectUserTokens } from "./users-Dls7_ANV.mjs";
|
|
4
|
+
export { EventBase, ExternalEvent, ReceivedEvent, ResolvedTarget, SyncOverrides, SyncProvider, connectUserTokens, envelope, eventSchema, withUserTokens };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { m as MESSAGE_TEXT_MAX, p as Labels } from "./schemas-Be08nyfW.mjs";
|
|
2
|
+
import { t as HttpError } from "./errors-DX3hpwWt.mjs";
|
|
3
|
+
import { o as extractMentions, s as replaceMentions } from "./actors-DjMeZpnU.mjs";
|
|
4
|
+
import { c as isThreadTarget } from "./reducers-mUPFuFJV.mjs";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/server/middleware.ts
|
|
7
|
+
/** A write the middleware didn't let through: the API answers 422 with what it responded. */
|
|
8
|
+
var DroppedError = class extends HttpError {
|
|
9
|
+
/** What the middleware responded, in order: none when it dropped the write without a word. */
|
|
10
|
+
responses;
|
|
11
|
+
constructor(responses) {
|
|
12
|
+
super(422, "dropped", responses.join("\n") || "the board dropped this message");
|
|
13
|
+
this.responses = responses;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const Patch = z.object({
|
|
17
|
+
text: z.string().max(MESSAGE_TEXT_MAX).optional(),
|
|
18
|
+
labels: Labels.optional()
|
|
19
|
+
}).strict();
|
|
20
|
+
const MIDDLEWARE_ACTOR = "board:middleware";
|
|
21
|
+
/**
|
|
22
|
+
* Runs `write` through `middleware` in order, then `commit`s what the last one
|
|
23
|
+
* passed on. Throws `DroppedError` when one doesn't call `next`.
|
|
24
|
+
*/
|
|
25
|
+
async function runMiddleware(middleware, input, commit) {
|
|
26
|
+
const responses = [];
|
|
27
|
+
let committed = null;
|
|
28
|
+
const step = async (i, write) => {
|
|
29
|
+
if (i === middleware.length) {
|
|
30
|
+
if (write.text.trim() === "" && (write.kind === "edit" || write.attachments.length === 0)) return;
|
|
31
|
+
committed = { value: await commit(write) };
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let next = null;
|
|
35
|
+
await middleware[i]({
|
|
36
|
+
write: Object.freeze({ ...write }),
|
|
37
|
+
author: Object.freeze({ ...input.author }),
|
|
38
|
+
source: Object.freeze({ ...input.source }),
|
|
39
|
+
board: input.board(),
|
|
40
|
+
respond: async (text) => {
|
|
41
|
+
responses.push(text);
|
|
42
|
+
},
|
|
43
|
+
next: (patch = {}) => {
|
|
44
|
+
if (next) throw new Error("next() was already called");
|
|
45
|
+
const { text, labels } = Patch.parse(patch);
|
|
46
|
+
next = step(i + 1, {
|
|
47
|
+
...write,
|
|
48
|
+
...text !== void 0 ? { text } : {},
|
|
49
|
+
...write.kind === "post" && labels ? { labels: {
|
|
50
|
+
...write.labels,
|
|
51
|
+
...labels
|
|
52
|
+
} } : {}
|
|
53
|
+
});
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
if (next) await next;
|
|
58
|
+
};
|
|
59
|
+
await step(0, input.write);
|
|
60
|
+
if (!committed) throw new DroppedError(responses);
|
|
61
|
+
return committed.value;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/server/targets.ts
|
|
65
|
+
/** A target as a post or a receipt names it, resolved: a plain one's kind goes by its shape. */
|
|
66
|
+
const syncTarget = (target) => typeof target === "string" ? {
|
|
67
|
+
target,
|
|
68
|
+
kind: isThreadTarget(target) ? "thread" : "channel"
|
|
69
|
+
} : target;
|
|
70
|
+
/** Whether `text` mentions anyone by email, `@[ada@vercel.com]`, which sync looks up. */
|
|
71
|
+
const mentionsEmails = (text) => extractMentions(text).some((token) => token.kind === "actor" && token.actor.includes("@"));
|
|
72
|
+
/**
|
|
73
|
+
* `@[ada@vercel.com]`: each email `find` knows (a synced platform's user) becomes that user's
|
|
74
|
+
* exact mention, `@[slack:user:U0456]`, so it reads natively there; the rest stay plain text.
|
|
75
|
+
*/
|
|
76
|
+
async function withEmailMentions(text, find) {
|
|
77
|
+
const emails = extractMentions(text).flatMap((token) => token.kind === "actor" && token.actor.includes("@") ? [token.actor] : []);
|
|
78
|
+
if (emails.length === 0) return {
|
|
79
|
+
text,
|
|
80
|
+
found: []
|
|
81
|
+
};
|
|
82
|
+
const byEmail = /* @__PURE__ */ new Map();
|
|
83
|
+
for (const email of emails) byEmail.set(email, await find?.(email).catch(() => null) ?? null);
|
|
84
|
+
return {
|
|
85
|
+
text: replaceMentions(text, (token) => {
|
|
86
|
+
if (token.kind !== "actor" || !byEmail.has(token.actor)) return null;
|
|
87
|
+
const actor = byEmail.get(token.actor);
|
|
88
|
+
return actor ? `@[${actor.id}]` : token.actor;
|
|
89
|
+
}),
|
|
90
|
+
found: [...byEmail.values()].filter((actor) => actor !== null)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { MIDDLEWARE_ACTOR as a, DroppedError as i, syncTarget as n, runMiddleware as o, withEmailMentions as r, mentionsEmails as t };
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { l as unauthorized } from "./errors-DX3hpwWt.mjs";
|
|
2
|
+
import { f as markVerified, m as trustedIssuers, n as TOKENS, r as VERCEL_ISSUER, u as issuerName } from "./auth-COzc09sn.mjs";
|
|
3
|
+
import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
|
|
4
|
+
//#region src/server/identity.ts
|
|
5
|
+
let fromDisk;
|
|
6
|
+
/** `vercel link` writes `.vercel/project.json`; local development reads it. */
|
|
7
|
+
function readLinkedProject() {
|
|
8
|
+
fromDisk ??= (async () => {
|
|
9
|
+
try {
|
|
10
|
+
const { readFile } = await import("node:fs/promises");
|
|
11
|
+
const { join } = await import("node:path");
|
|
12
|
+
const raw = await readFile(join(process.cwd(), ".vercel", "project.json"), "utf8");
|
|
13
|
+
const { projectId, orgId } = JSON.parse(raw);
|
|
14
|
+
return {
|
|
15
|
+
projectId: projectId ?? null,
|
|
16
|
+
teamId: orgId ?? null
|
|
17
|
+
};
|
|
18
|
+
} catch {
|
|
19
|
+
return {
|
|
20
|
+
projectId: null,
|
|
21
|
+
teamId: null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
return fromDisk;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The board's own Vercel project. Verification only trusts the project ID,
|
|
29
|
+
* from config, the platform's `VERCEL_PROJECT_ID`, or the linked project.
|
|
30
|
+
*/
|
|
31
|
+
async function ownProjectId(configured) {
|
|
32
|
+
if (configured) return configured.id;
|
|
33
|
+
if (process.env.VERCEL_PROJECT_ID) return process.env.VERCEL_PROJECT_ID;
|
|
34
|
+
return (await readLinkedProject()).projectId;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The board's own team, from config, `VERCEL_ORG_ID`, or the linked project.
|
|
38
|
+
* On Vercel there's usually none of these, so verification falls back to the
|
|
39
|
+
* platform's token for the board's project.
|
|
40
|
+
*/
|
|
41
|
+
async function ownTeamId(configured) {
|
|
42
|
+
if (configured?.teamId) return configured.teamId;
|
|
43
|
+
if (process.env.VERCEL_ORG_ID) return process.env.VERCEL_ORG_ID;
|
|
44
|
+
return (await readLinkedProject()).teamId;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The team, for discovery only: clients need it to mint tokens. The platform's
|
|
48
|
+
* request token is decoded without trust, since a spoofed header can only
|
|
49
|
+
* mislead the response it's attached to.
|
|
50
|
+
*/
|
|
51
|
+
async function discoveryIdentity(headers, configured) {
|
|
52
|
+
const projectId = await ownProjectId(configured);
|
|
53
|
+
if (configured?.teamId) return {
|
|
54
|
+
projectId,
|
|
55
|
+
teamId: configured.teamId
|
|
56
|
+
};
|
|
57
|
+
const header = headers?.get("x-vercel-oidc-token");
|
|
58
|
+
if (header) try {
|
|
59
|
+
const claims = decodeJwt(header);
|
|
60
|
+
if (typeof claims.owner_id === "string") return {
|
|
61
|
+
projectId,
|
|
62
|
+
teamId: claims.owner_id
|
|
63
|
+
};
|
|
64
|
+
} catch {}
|
|
65
|
+
if (process.env.VERCEL_ORG_ID) return {
|
|
66
|
+
projectId,
|
|
67
|
+
teamId: process.env.VERCEL_ORG_ID
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
projectId,
|
|
71
|
+
teamId: (await readLinkedProject()).teamId
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/server/tokens.ts
|
|
76
|
+
const REGISTERED = /* @__PURE__ */ new Set([
|
|
77
|
+
"aud",
|
|
78
|
+
"exp",
|
|
79
|
+
"iat",
|
|
80
|
+
"nbf",
|
|
81
|
+
"jti"
|
|
82
|
+
]);
|
|
83
|
+
/** The claims rules can match on: plain values, bounded, as a schedule stores them. */
|
|
84
|
+
function factsOf(issuer, iss, claims) {
|
|
85
|
+
const kept = {};
|
|
86
|
+
for (const [name, value] of Object.entries(claims)) {
|
|
87
|
+
if (REGISTERED.has(name) || Object.keys(kept).length >= 64) continue;
|
|
88
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "string" && value.length <= 1e3) kept[name] = value;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
issuer,
|
|
92
|
+
iss,
|
|
93
|
+
claims: kept
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const issuerMatches = (iss, accepted) => typeof accepted === "string" ? iss === accepted : accepted.test(iss);
|
|
97
|
+
const remoteKeys = /* @__PURE__ */ new Map();
|
|
98
|
+
async function keysFor(iss) {
|
|
99
|
+
const cached = remoteKeys.get(iss);
|
|
100
|
+
if (cached) return cached;
|
|
101
|
+
const response = await fetch(`${iss.replace(/\/+$/, "")}/.well-known/openid-configuration`);
|
|
102
|
+
if (!response.ok) throw unauthorized(`cannot load keys for issuer ${iss}`);
|
|
103
|
+
const { jwks_uri } = await response.json();
|
|
104
|
+
const keys = createRemoteJWKSet(new URL(jwks_uri));
|
|
105
|
+
remoteKeys.set(iss, keys);
|
|
106
|
+
return keys;
|
|
107
|
+
}
|
|
108
|
+
/** What tokens are verified with: the platform's issuers, OIDC issuers the board's rules name, and test issuers. */
|
|
109
|
+
function createVerifier(options) {
|
|
110
|
+
const vercelIssuer = options.auth?.vercel ?? { issuer: VERCEL_ISSUER };
|
|
111
|
+
const githubIssuer = options.auth?.github ?? { issuer: "https://token.actions.githubusercontent.com" };
|
|
112
|
+
const oidcIssuers = trustedIssuers(options.issuers);
|
|
113
|
+
let platformTeam = null;
|
|
114
|
+
/**
|
|
115
|
+
* The team on a Vercel-signed token for the board's own project. Project IDs
|
|
116
|
+
* are unique, so its `owner_id` is the board's team, whoever sent the token.
|
|
117
|
+
*/
|
|
118
|
+
async function teamFrom(platformToken, ownProject) {
|
|
119
|
+
if (platformTeam !== null || !platformToken || ownProject === null) return platformTeam;
|
|
120
|
+
try {
|
|
121
|
+
const iss = String(decodeJwt(platformToken).iss ?? "");
|
|
122
|
+
if (!issuerMatches(iss, vercelIssuer.issuer)) return null;
|
|
123
|
+
const keys = vercelIssuer.jwks ?? await keysFor(iss);
|
|
124
|
+
const { payload } = await jwtVerify(platformToken, keys, { issuer: iss });
|
|
125
|
+
if (payload.project_id === ownProject && typeof payload.owner_id === "string") platformTeam = payload.owner_id;
|
|
126
|
+
} catch {}
|
|
127
|
+
return platformTeam;
|
|
128
|
+
}
|
|
129
|
+
/** `platformToken`: the board's own OIDC token, which Vercel sends with each request. */
|
|
130
|
+
async function verify(token, audience, platformToken) {
|
|
131
|
+
let iss;
|
|
132
|
+
try {
|
|
133
|
+
iss = String(decodeJwt(token).iss ?? "");
|
|
134
|
+
} catch {
|
|
135
|
+
throw unauthorized("malformed token");
|
|
136
|
+
}
|
|
137
|
+
const local = options.auth?.local;
|
|
138
|
+
if (local && iss === local.issuer) return localPerson(token, local, audience);
|
|
139
|
+
const isVercel = issuerMatches(iss, vercelIssuer.issuer);
|
|
140
|
+
const isGitHub = !isVercel && issuerMatches(iss, githubIssuer.issuer);
|
|
141
|
+
const isOidc = !isVercel && !isGitHub && oidcIssuers.has(iss);
|
|
142
|
+
if (!isVercel && !isGitHub && !isOidc) throw unauthorized(`untrusted issuer: ${iss || "(none)"}`);
|
|
143
|
+
let claims;
|
|
144
|
+
try {
|
|
145
|
+
const keys = isOidc ? options.auth?.oidc?.[iss] ?? await keysFor(iss) : (isVercel ? vercelIssuer : githubIssuer).jwks ?? await keysFor(iss);
|
|
146
|
+
({payload: claims} = await jwtVerify(token, keys, {
|
|
147
|
+
issuer: iss,
|
|
148
|
+
audience
|
|
149
|
+
}));
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error.status === 401) throw error;
|
|
152
|
+
throw unauthorized(`invalid token: ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
const facts = factsOf(isOidc ? "oidc" : isGitHub ? "github" : "vercel", iss, claims);
|
|
155
|
+
const expiresAt = claims.exp ? /* @__PURE__ */ new Date(claims.exp * 1e3) : null;
|
|
156
|
+
if (isOidc) {
|
|
157
|
+
if (typeof claims.sub !== "string" || claims.sub === "") throw unauthorized(`token from ${iss} without sub`);
|
|
158
|
+
return {
|
|
159
|
+
id: `oidc:${issuerName(iss)}:${claims.sub}`,
|
|
160
|
+
kind: "workflow",
|
|
161
|
+
handle: claims.sub.slice(0, 128),
|
|
162
|
+
expiresAt,
|
|
163
|
+
facts
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (isGitHub) return {
|
|
167
|
+
id: `github:repo:${String(claims.repository_id)}`,
|
|
168
|
+
kind: "workflow",
|
|
169
|
+
handle: String(claims.repository),
|
|
170
|
+
expiresAt,
|
|
171
|
+
facts
|
|
172
|
+
};
|
|
173
|
+
const environment = String(claims.environment ?? "");
|
|
174
|
+
const project = String(claims.project ?? "");
|
|
175
|
+
const ownProject = await options.projectId() ?? null;
|
|
176
|
+
if (ownProject !== null && claims.project_id === ownProject && environment === "development") {
|
|
177
|
+
if (typeof claims.user_id !== "string") throw unauthorized("development token without user_id");
|
|
178
|
+
return {
|
|
179
|
+
id: `vercel:user:${claims.user_id}`,
|
|
180
|
+
kind: "person",
|
|
181
|
+
handle: `user-${claims.user_id.slice(0, 6).toLowerCase()}`,
|
|
182
|
+
expiresAt
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const teamId = await options.teamId() ?? await teamFrom(platformToken ?? process.env.VERCEL_OIDC_TOKEN, ownProject) ?? null;
|
|
186
|
+
return deployment(teamId === null ? facts : {
|
|
187
|
+
...facts,
|
|
188
|
+
team: teamId
|
|
189
|
+
}, project, environment, expiresAt);
|
|
190
|
+
}
|
|
191
|
+
return { verify };
|
|
192
|
+
}
|
|
193
|
+
const LOCAL_SUB = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/;
|
|
194
|
+
async function localPerson(token, local, audience) {
|
|
195
|
+
let claims;
|
|
196
|
+
try {
|
|
197
|
+
({payload: claims} = await jwtVerify(token, local.jwks, {
|
|
198
|
+
issuer: local.issuer,
|
|
199
|
+
audience
|
|
200
|
+
}));
|
|
201
|
+
} catch (error) {
|
|
202
|
+
throw unauthorized(`invalid token: ${error.message}`);
|
|
203
|
+
}
|
|
204
|
+
const sub = String(claims.sub ?? "");
|
|
205
|
+
if (!LOCAL_SUB.test(sub)) throw unauthorized("local token without a valid sub");
|
|
206
|
+
return {
|
|
207
|
+
id: `local:user:${sub}`,
|
|
208
|
+
kind: "person",
|
|
209
|
+
handle: sub,
|
|
210
|
+
expiresAt: claims.exp ? /* @__PURE__ */ new Date(claims.exp * 1e3) : null
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const deployment = (facts, project, environment, expiresAt) => ({
|
|
214
|
+
id: `vercel:project:${String(facts.claims.project_id)}:${environment}`,
|
|
215
|
+
kind: "deployment",
|
|
216
|
+
handle: environment === "production" ? project : `${project}@${environment}`,
|
|
217
|
+
expiresAt,
|
|
218
|
+
facts
|
|
219
|
+
});
|
|
220
|
+
/** The board's own project, for a verifier asked outside a board that knows it: from the environment. */
|
|
221
|
+
const environmentBoard = {
|
|
222
|
+
project: async () => ({
|
|
223
|
+
id: await ownProjectId(),
|
|
224
|
+
teamId: await ownTeamId()
|
|
225
|
+
}),
|
|
226
|
+
issuers: []
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* The OIDC tokens made out to this board: a request's bearer token, from Vercel (people and
|
|
230
|
+
* deployments), GitHub Actions, an `oidc()` rule's issuer, or `local`'s (`vb serve`'s), verified
|
|
231
|
+
* for the board's URL. Null for a request with no token, and a 401 for one whose token fails.
|
|
232
|
+
* Without `request.board`, the board's project is the environment's, and it trusts no issuer past
|
|
233
|
+
* Vercel's and GitHub's.
|
|
234
|
+
*/
|
|
235
|
+
function oidcTokens(options = {}) {
|
|
236
|
+
let made = null;
|
|
237
|
+
const authenticate = async (request) => {
|
|
238
|
+
if (request.credential === null) return null;
|
|
239
|
+
const board = request.board ?? environmentBoard;
|
|
240
|
+
const issuers = board.issuers.join("\n");
|
|
241
|
+
if (made?.board !== board || made.issuers !== issuers) made = {
|
|
242
|
+
board,
|
|
243
|
+
issuers,
|
|
244
|
+
verify: createVerifier({
|
|
245
|
+
projectId: async () => (await board.project()).id,
|
|
246
|
+
teamId: async () => (await board.project()).teamId,
|
|
247
|
+
issuers: board.issuers,
|
|
248
|
+
auth: options
|
|
249
|
+
}).verify
|
|
250
|
+
};
|
|
251
|
+
return markVerified(await made.verify(request.credential, request.audience, request.headers.get("x-vercel-oidc-token") ?? void 0));
|
|
252
|
+
};
|
|
253
|
+
return Object.assign(authenticate, { [TOKENS]: true });
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
export { ownTeamId as i, discoveryIdentity as n, ownProjectId as r, oidcTokens as t };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { a as ExternalCopy, c as SyncedThread, o as Message, p as BoardEvent } from "./reducers-B_vUGQRp.mjs";
|
|
2
|
+
import { A2Store } from "experimental-a2/server";
|
|
3
|
+
//#region src/store/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* What a query matches, resolved by the board: handles are already actor and
|
|
6
|
+
* principal sets, so a backend never needs the actors.
|
|
7
|
+
*/
|
|
8
|
+
type MessageFilter = {
|
|
9
|
+
/** One channel, or with `subtree` it and every channel under it. */
|
|
10
|
+
channel?: {
|
|
11
|
+
path: string;
|
|
12
|
+
subtree: boolean;
|
|
13
|
+
};
|
|
14
|
+
/** In one of these channels: those a reader may read. None matches nothing. */
|
|
15
|
+
channels?: readonly string[];
|
|
16
|
+
/** A thread: its root and its replies. */
|
|
17
|
+
re?: number;
|
|
18
|
+
/** Written by one of these actors, or by an agent of one of these people. */
|
|
19
|
+
by?: {
|
|
20
|
+
actors: readonly string[];
|
|
21
|
+
principals: readonly string[];
|
|
22
|
+
};
|
|
23
|
+
/** Mentions one of these actors. */
|
|
24
|
+
mentions?: readonly string[];
|
|
25
|
+
/** The sync key it came from, or `board`. */
|
|
26
|
+
from?: string;
|
|
27
|
+
/** Lowercased: a substring of the lowercased text. */
|
|
28
|
+
text?: string;
|
|
29
|
+
/** Each on the message or its thread's root; `values: null` is any value. */
|
|
30
|
+
labels?: readonly {
|
|
31
|
+
key: string;
|
|
32
|
+
values: readonly string[] | null;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
/** By message ID: `after` takes the first `limit` after it; otherwise the last `limit`. */
|
|
36
|
+
type Page = {
|
|
37
|
+
before?: number;
|
|
38
|
+
after?: number;
|
|
39
|
+
limit: number;
|
|
40
|
+
};
|
|
41
|
+
/** A thread as `/threads` lists it: its root, how many replies aren't deleted, and the latest. */
|
|
42
|
+
type ThreadSummary = {
|
|
43
|
+
root: Message;
|
|
44
|
+
replies: number;
|
|
45
|
+
last: Message | null;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The board's messages, kept from its log. Each read says which log index it
|
|
49
|
+
* reflects (`position`), so a reader knows what it has seen.
|
|
50
|
+
*/
|
|
51
|
+
interface MessageTables {
|
|
52
|
+
/** The last log index applied. */
|
|
53
|
+
position(): Promise<number>;
|
|
54
|
+
/** Applies one board event, exactly once: an event at or below the position changes nothing. */
|
|
55
|
+
apply(event: BoardEvent): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Applies `event` only if it's the very next one, and says whether it did: a
|
|
58
|
+
* writer catching the tables up on its own write, without waiting on the consumer.
|
|
59
|
+
*/
|
|
60
|
+
applyNext(event: BoardEvent): Promise<boolean>;
|
|
61
|
+
message(id: number): Promise<Message | null>;
|
|
62
|
+
/** The message a post with this event ID created. A root's thread key is one. */
|
|
63
|
+
byEventId(eventId: string): Promise<Message | null>;
|
|
64
|
+
/** Each of `ids` as it was right after event `index`; one not posted by then is absent. */
|
|
65
|
+
messagesAt(ids: readonly number[], index: number): Promise<Map<number, Message>>;
|
|
66
|
+
/** A thread's root and every reply, deleted ones included, by ID. */
|
|
67
|
+
thread(root: number): Promise<Message[]>;
|
|
68
|
+
messages(filter: MessageFilter, page: Page): Promise<{
|
|
69
|
+
messages: Message[];
|
|
70
|
+
position: number;
|
|
71
|
+
}>;
|
|
72
|
+
/** Threads with a message that matches, by their latest reply. */
|
|
73
|
+
threads(filter: MessageFilter, page: Page): Promise<{
|
|
74
|
+
threads: ThreadSummary[];
|
|
75
|
+
position: number;
|
|
76
|
+
}>;
|
|
77
|
+
/** A thread's sync: its targets, and the platform thread it lives in on each. */
|
|
78
|
+
syncOf(thread: string): Promise<SyncedThread | null>;
|
|
79
|
+
/** The board thread a platform thread belongs to. */
|
|
80
|
+
threadOf(externalThread: string): Promise<string | null>;
|
|
81
|
+
/** Each message's copies, by target: where it went, or came from, on each platform. */
|
|
82
|
+
copiesOf(ids: readonly number[]): Promise<Map<number, Record<string, ExternalCopy>>>;
|
|
83
|
+
/** The board message a platform message is a copy of, or came in as. */
|
|
84
|
+
messageOf(externalThread: string, platformMessage: string): Promise<number | null>;
|
|
85
|
+
/** Every platform thread a board thread lives in. */
|
|
86
|
+
externalThreads(): Promise<string[]>;
|
|
87
|
+
/** A person's own roots posted at or after `since` (epoch ms). */
|
|
88
|
+
recentRoots(principal: string, since: number): Promise<Message[]>;
|
|
89
|
+
/** The messages that carry an attachment, by ID. A deleted message carries none. */
|
|
90
|
+
withAttachment(attachment: string): Promise<Message[]>;
|
|
91
|
+
close(): Promise<void>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Where a board keeps everything: its A2 log, and the tables it reads from. Create one with
|
|
95
|
+
* `postgres()`, `redis()`, `redisHttp()`, `sqlite()`, or `memory()` from `@vercel/board/backend-a2/store-*`.
|
|
96
|
+
*/
|
|
97
|
+
type BoardStore = {
|
|
98
|
+
/** The board's A2 store. */
|
|
99
|
+
readonly log: A2Store;
|
|
100
|
+
readonly tables: MessageTables;
|
|
101
|
+
close(): Promise<void>;
|
|
102
|
+
};
|
|
103
|
+
//#endregion
|
|
104
|
+
export { BoardStore as t };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//#region src/sync/users.ts
|
|
2
|
+
const USER_TOKENS = Symbol.for("board.userTokens");
|
|
3
|
+
/**
|
|
4
|
+
* Gives an adapter people's tokens, for your own OAuth: `slack({ adapter:
|
|
5
|
+
* withUserTokens(createSlackAdapter(...), { authorize, token }) })`. A Connect
|
|
6
|
+
* connector name (`adapter: 'slack/acme'`) does this for you.
|
|
7
|
+
*/
|
|
8
|
+
function withUserTokens(adapter, tokens) {
|
|
9
|
+
Object.defineProperty(adapter, USER_TOKENS, {
|
|
10
|
+
value: tokens,
|
|
11
|
+
enumerable: false,
|
|
12
|
+
configurable: true
|
|
13
|
+
});
|
|
14
|
+
return adapter;
|
|
15
|
+
}
|
|
16
|
+
const userTokensOf = (adapter) => adapter[USER_TOKENS] ?? null;
|
|
17
|
+
/** Vercel Connect's user tokens for `connector`: the subject is the board's principal, so Connect binds the account to it. */
|
|
18
|
+
function connectUserTokens(connector) {
|
|
19
|
+
const subject = (principal) => ({ subject: {
|
|
20
|
+
type: "user",
|
|
21
|
+
id: principal
|
|
22
|
+
} });
|
|
23
|
+
return {
|
|
24
|
+
async authorize(principal, options = {}) {
|
|
25
|
+
const { startAuthorization } = await import("@vercel/connect");
|
|
26
|
+
return (options.callback ? await startAuthorization(connector, subject(principal), { callbackUrl: options.callback }) : await startAuthorization(connector, subject(principal))).url;
|
|
27
|
+
},
|
|
28
|
+
async token(principal) {
|
|
29
|
+
const { getToken, NoValidTokenError, UserAuthorizationRequiredError } = await import("@vercel/connect");
|
|
30
|
+
try {
|
|
31
|
+
return await getToken(connector, subject(principal));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error instanceof UserAuthorizationRequiredError || error instanceof NoValidTokenError) return null;
|
|
34
|
+
const code = error.code;
|
|
35
|
+
if (code === "user_authorization_required" || code === "no_token") return null;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { userTokensOf as n, withUserTokens as r, connectUserTokens as t };
|