@volter/world-platform 2.0.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 (75) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +29 -0
  3. package/client/platform.css +228 -0
  4. package/client/platform.tsx +1049 -0
  5. package/client/reserved.ts +3 -0
  6. package/dist/client/platform.bundle.js +237 -0
  7. package/dist/client/platform.css +228 -0
  8. package/dist/client/platform.tsx +1049 -0
  9. package/dist/client/reserved.d.ts +1 -0
  10. package/dist/client/reserved.js +3 -0
  11. package/dist/client/reserved.ts +3 -0
  12. package/dist/src/audit.d.ts +24 -0
  13. package/dist/src/audit.js +23 -0
  14. package/dist/src/backup.d.ts +37 -0
  15. package/dist/src/backup.js +94 -0
  16. package/dist/src/biller.d.ts +59 -0
  17. package/dist/src/biller.js +1 -0
  18. package/dist/src/cli.d.ts +2 -0
  19. package/dist/src/cli.js +112 -0
  20. package/dist/src/db/migrations/0000_init.sql +143 -0
  21. package/dist/src/db/migrations/meta/0000_snapshot.json +877 -0
  22. package/dist/src/db/migrations/meta/_journal.json +13 -0
  23. package/dist/src/db/migrations.d.ts +4 -0
  24. package/dist/src/db/migrations.js +33 -0
  25. package/dist/src/db/open.d.ts +11 -0
  26. package/dist/src/db/open.js +114 -0
  27. package/dist/src/db/pack-migrations.d.ts +1 -0
  28. package/dist/src/db/pack-migrations.js +33 -0
  29. package/dist/src/db/schema.d.ts +1803 -0
  30. package/dist/src/db/schema.js +123 -0
  31. package/dist/src/directory.d.ts +75 -0
  32. package/dist/src/directory.js +199 -0
  33. package/dist/src/doors.d.ts +16 -0
  34. package/dist/src/doors.js +123 -0
  35. package/dist/src/identity.d.ts +88 -0
  36. package/dist/src/identity.js +314 -0
  37. package/dist/src/labs.d.ts +14 -0
  38. package/dist/src/labs.js +7 -0
  39. package/dist/src/mail.d.ts +16 -0
  40. package/dist/src/mail.js +27 -0
  41. package/dist/src/pages.d.ts +15 -0
  42. package/dist/src/pages.js +73 -0
  43. package/dist/src/platform.d.ts +77 -0
  44. package/dist/src/platform.js +1845 -0
  45. package/dist/src/sample.d.ts +11 -0
  46. package/dist/src/sample.js +93 -0
  47. package/dist/src/store.d.ts +110 -0
  48. package/dist/src/store.js +142 -0
  49. package/dist/src/tokens.d.ts +69 -0
  50. package/dist/src/tokens.js +96 -0
  51. package/dist/src/webhooks.d.ts +60 -0
  52. package/dist/src/webhooks.js +92 -0
  53. package/package.json +78 -0
  54. package/src/audit.ts +26 -0
  55. package/src/backup.ts +73 -0
  56. package/src/biller.ts +45 -0
  57. package/src/cli.ts +99 -0
  58. package/src/db/migrations/0000_init.sql +143 -0
  59. package/src/db/migrations/meta/0000_snapshot.json +877 -0
  60. package/src/db/migrations/meta/_journal.json +13 -0
  61. package/src/db/migrations.ts +33 -0
  62. package/src/db/open.ts +92 -0
  63. package/src/db/pack-migrations.ts +19 -0
  64. package/src/db/schema.ts +137 -0
  65. package/src/directory.ts +216 -0
  66. package/src/doors.ts +138 -0
  67. package/src/identity.ts +279 -0
  68. package/src/labs.ts +8 -0
  69. package/src/mail.ts +27 -0
  70. package/src/pages.ts +69 -0
  71. package/src/platform.ts +1183 -0
  72. package/src/sample.ts +84 -0
  73. package/src/store.ts +154 -0
  74. package/src/tokens.ts +94 -0
  75. package/src/webhooks.ts +85 -0
@@ -0,0 +1,88 @@
1
+ import { type OpenIdMetadata } from '@volter/identity';
2
+ import type { SignedInPerson } from './directory.js';
3
+ /** The Volter provider's settings (each from its `VOLTER_*` name when absent). */
4
+ export type IdentityOptions = {
5
+ issuer?: string;
6
+ clientId?: string;
7
+ clientSecret?: string;
8
+ };
9
+ /** The access provider a platform signs people in with. */
10
+ export type ProviderOptions = ({
11
+ kind: 'volter';
12
+ } & IdentityOptions) | {
13
+ kind: 'oidc';
14
+ issuer?: string;
15
+ clientId?: string;
16
+ clientSecret?: string;
17
+ name?: string; /** the domains the issuer's organization owns and verified: an address in one counts as verified though the issuer sends no `email_verified` (Entra) */
18
+ trustEmailDomains?: string[];
19
+ } | {
20
+ kind: 'github';
21
+ clientId?: string;
22
+ clientSecret?: string; /** GitHub's web address (GitHub Enterprise Server's); https://github.com by default */
23
+ web?: string; /** its REST API; https://api.github.com by default, `<web>/api/v3` for an Enterprise Server */
24
+ api?: string;
25
+ };
26
+ /** The platform's client at its provider: who it is there, and (Volter) a call through the product door. */
27
+ export type Identity = {
28
+ kind: 'volter' | 'oidc' | 'github';
29
+ /** What a person is told they continue with: "Volter", "Google", the operator's name for their issuer. */
30
+ name: string;
31
+ issuer: string;
32
+ clientId: string;
33
+ clientSecret: string;
34
+ /** The person's own account page at the provider, when it has one. */
35
+ account?: string;
36
+ /** The issuer's discovery document (cached by `@volter/identity`; a failure is asked again). */
37
+ metadata: () => Promise<OpenIdMetadata>;
38
+ /** A product-door call with the platform's organizations token (Volter only); 404 answers null, any other refusal throws its words. */
39
+ call: <T>(method: 'GET' | 'POST' | 'PATCH' | 'DELETE', path: string, body?: unknown) => Promise<T | null>;
40
+ /** GitHub's web and API addresses, for the `github` provider. */
41
+ github?: {
42
+ web: string;
43
+ api: string;
44
+ };
45
+ /** The domains the operator says the issuer's organization owns (OIDC_TRUST_EMAIL_DOMAINS): an address in one, with no `email_verified`, counts as verified. */
46
+ trustEmailDomains?: string[];
47
+ };
48
+ export declare function providerClient(opts: ProviderOptions): Identity;
49
+ /** The Volter provider (the settings, else the `VOLTER_*` names). */
50
+ export declare const identityClient: (opts?: IdentityOptions) => Identity;
51
+ export declare const SESSION_COOKIE = "volter_console_session";
52
+ export declare const CALLBACK_PATH = "/-/sign-in/callback";
53
+ export type SessionRecord = {
54
+ subject: string;
55
+ email?: string;
56
+ name?: string;
57
+ expiresAt: string;
58
+ };
59
+ /** The platform's sessions, by the SHA-256 of the cookie's id, in its database (in memory when it keeps no state). */
60
+ export declare class Sessions {
61
+ private readonly db;
62
+ constructor(stateDir?: string);
63
+ private key;
64
+ start(person: {
65
+ subject: string;
66
+ email?: string;
67
+ name?: string;
68
+ }): string;
69
+ read(sessionId: string): SessionRecord | null;
70
+ end(sessionId: string): void;
71
+ }
72
+ export declare const sessionIdOf: (request: Request, origin: string) => string | undefined;
73
+ export type Signed = {
74
+ userId: string;
75
+ sessionId: string;
76
+ email?: string;
77
+ name?: string;
78
+ };
79
+ /** Who a request is: the platform's session its cookie names, while it lasts. */
80
+ export declare function whoIs(sessions: Sessions, request: Request, origin: string): Signed | null;
81
+ /** Where a sign-in may return a person: a path of this platform (its pages, or `/-/…`), never another origin or a scheme. */
82
+ export declare const signInReturn: (next: string | null | undefined) => string | undefined;
83
+ /** Start signing in: the authorization code with PKCE (S256) at the provider. */
84
+ export declare function beginSignIn(identity: Identity, origin: string, next?: string): Promise<Response>;
85
+ /** The issuer's answer: the code traded with the client secret, the id token verified, the session started. */
86
+ export declare function finishSignIn(identity: Identity, sessions: Sessions, origin: string, request: Request, landing: string, seen?: (person: SignedInPerson) => Promise<void>): Promise<Response>;
87
+ /** Sign out: the platform's session ends and its cookie goes. */
88
+ export declare function endSession(sessions: Sessions, origin: string, request: Request, landing: string): Response;
@@ -0,0 +1,314 @@
1
+ // SIGNING IN IS THE ACCESS PROVIDER'S (docs/contributing/architecture.md, "The hosted product"; docs/reference/
2
+ // platform-api.md). A person signs in at the provider's issuer (the authorization code with PKCE) and the platform keeps
3
+ // its own session for them, keyed by the issuer's `subject`, and nothing of the credential. A platform has one provider:
4
+ //
5
+ // - `volter`: Volter Identity. `VOLTER_ISSUER` (https://id.volter.ai by default), `VOLTER_CLIENT_ID` and
6
+ // `VOLTER_CLIENT_SECRET`, the client the operator registered for this platform. Its directory (directory.ts
7
+ // volterDirectory) is the service's, reached through its product door with this client's `client_credentials`
8
+ // token for `<issuer>/organizations` (volter-ai/identity ADR-0002). In a World the Volter identity twin is the
9
+ // issuer and the drive registers the client.
10
+ // - `oidc`: any OpenID Connect issuer (Google, Microsoft Entra, Okta, Keycloak): `OIDC_ISSUER`, `OIDC_CLIENT_ID`,
11
+ // `OIDC_CLIENT_SECRET` and optionally `OIDC_NAME` (what the sign-in button says) and `OIDC_TRUST_EMAIL_DOMAINS` (the
12
+ // domains the issuer's organization owns and verified: an address in one counts as verified where the issuer sends
13
+ // no `email_verified`, as Entra does, so invitations can match it; any other address never does). It only signs people in; the
14
+ // platform keeps the directory itself (directory.ts localDirectory).
15
+ // - `github`: GitHub, whose OAuth apps are OAuth 2.0 without OpenID Connect: `GITHUB_CLIENT_ID` and
16
+ // `GITHUB_CLIENT_SECRET`, and for GitHub Enterprise Server `GITHUB_URL` (its web address) and `GITHUB_API_URL`
17
+ // (`<GITHUB_URL>/api/v3`). The person is `GET /user` and their primary verified address `GET /user/emails`, read
18
+ // once with the token the code bought; the subject is `github:<numeric id>`, which a rename does not change.
19
+ // Sign-in only; the platform keeps the directory, as for `oidc`.
20
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
21
+ import { eq, lte } from 'drizzle-orm';
22
+ import { openDatabase } from "./db/open.js";
23
+ import { sessions as sessionRows } from "./db/schema.js";
24
+ import { resolveIdentity, verifyIdentityToken } from '@volter/identity';
25
+ /** The provider's name from its issuer, for the issuers people know by name. */
26
+ const nameOf = (issuer) => {
27
+ const host = (() => { try {
28
+ return new URL(issuer).hostname;
29
+ }
30
+ catch {
31
+ return issuer;
32
+ } })();
33
+ if (host === 'accounts.google.com')
34
+ return 'Google';
35
+ if (host === 'login.microsoftonline.com')
36
+ return 'Microsoft';
37
+ if (/\.okta\.com$/.test(host))
38
+ return 'Okta';
39
+ return host;
40
+ };
41
+ export function providerClient(opts) {
42
+ if (opts.kind === 'github')
43
+ return githubClient(opts);
44
+ const volter = opts.kind === 'volter';
45
+ const env = (name) => process.env[`${volter ? 'VOLTER' : 'OIDC'}_${name}`];
46
+ const issuer = (opts.issuer || env('ISSUER') || (volter ? 'https://id.volter.ai' : '')).replace(/\/+$/, '');
47
+ const clientId = opts.clientId ?? env('CLIENT_ID');
48
+ const clientSecret = opts.clientSecret ?? env('CLIENT_SECRET');
49
+ if (!issuer)
50
+ throw new Error('no OpenID Connect issuer: set OIDC_ISSUER (https://accounts.google.com, your Entra or Okta issuer…)');
51
+ if (!clientId || !clientSecret)
52
+ throw new Error(volter ? 'no Volter identity client: set VOLTER_CLIENT_ID and VOLTER_CLIENT_SECRET, the client registered for this platform at the identity service (VOLTER_ISSUER)' : `no client at ${issuer}: set OIDC_CLIENT_ID and OIDC_CLIENT_SECRET, the client registered for this platform there`);
53
+ const metadata = async () => {
54
+ const known = await resolveIdentity(issuer);
55
+ const found = known.available ? known : await resolveIdentity(issuer, { fresh: true });
56
+ if (!found.available)
57
+ throw new Error(found.reason);
58
+ return found.metadata;
59
+ };
60
+ // the organizations token (RFC 6749 §4.4), kept until a minute before it expires
61
+ let held = null;
62
+ const organizationsToken = async () => {
63
+ if (held && Date.now() < held.until)
64
+ return held.token;
65
+ const { token_endpoint } = await metadata();
66
+ const res = await fetch(token_endpoint, {
67
+ method: 'POST',
68
+ headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
69
+ body: new URLSearchParams({ grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret, scope: 'organizations', resource: `${issuer}/organizations` }).toString(),
70
+ });
71
+ const body = (await res.json().catch(() => ({})));
72
+ if (!res.ok || !body.access_token)
73
+ throw new Error(`the identity service refused the platform's organizations token: ${body.error_description ?? body.error ?? res.status}`);
74
+ held = { token: body.access_token, until: Date.now() + Math.max(0, (body.expires_in ?? 3600) - 60) * 1000 };
75
+ return held.token;
76
+ };
77
+ const call = async (method, path, body) => {
78
+ if (!volter)
79
+ throw new Error('an OpenID Connect provider has no product door: the platform keeps the directory');
80
+ const res = await fetch(`${issuer}${path}`, { method, headers: { authorization: `Bearer ${await organizationsToken()}`, accept: 'application/json', ...(body === undefined ? {} : { 'content-type': 'application/json' }) }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
81
+ if (res.status === 404)
82
+ return null;
83
+ const answer = (await res.json().catch(() => ({})));
84
+ if (!res.ok)
85
+ throw new Error(answer.error ?? `the identity service answered ${res.status}`);
86
+ return answer;
87
+ };
88
+ const name = volter ? 'Volter' : (opts.name || env('NAME') || nameOf(issuer));
89
+ // only the domains the operator names, never every address: a tenant's admins can set any address on a user (nOAuth)
90
+ const trustEmailDomains = volter ? [] : ((opts.kind === 'oidc' ? opts.trustEmailDomains : undefined) ?? (env('TRUST_EMAIL_DOMAINS') ?? '').split(',')).map((d) => d.trim().toLowerCase().replace(/^@/, '')).filter((d) => /^[a-z0-9.-]+\.[a-z]{2,}$/.test(d));
91
+ return { kind: opts.kind, name, issuer, clientId, clientSecret, ...(volter ? { account: `${issuer}/` } : {}), ...(trustEmailDomains.length ? { trustEmailDomains } : {}), metadata, call };
92
+ }
93
+ /** GitHub as the provider: OAuth 2.0 at its web address, the person read from its REST API. */
94
+ function githubClient(opts) {
95
+ // https, or http only on this machine: the client secret, the code and the person's token cross it
96
+ const url = (value, name) => { let u; try {
97
+ u = new URL(value);
98
+ }
99
+ catch {
100
+ throw new Error(`${name} is not an address: ${JSON.stringify(value)}`);
101
+ } const local = u.hostname === 'localhost' || u.hostname.endsWith('.localhost') || u.hostname === '127.0.0.1' || u.hostname === '[::1]'; if (u.protocol !== 'https:' && !(u.protocol === 'http:' && local))
102
+ throw new Error(`${name} must be https (or http on this machine): ${JSON.stringify(value)}`); return value.replace(/\/+$/, ''); };
103
+ const web = url(opts.web || process.env.GITHUB_URL || 'https://github.com', 'GITHUB_URL');
104
+ const api = url(opts.api || process.env.GITHUB_API_URL || (web === 'https://github.com' ? 'https://api.github.com' : `${web}/api/v3`), 'GITHUB_API_URL');
105
+ const clientId = opts.clientId ?? process.env.GITHUB_CLIENT_ID;
106
+ const clientSecret = opts.clientSecret ?? process.env.GITHUB_CLIENT_SECRET;
107
+ if (!clientId || !clientSecret)
108
+ throw new Error('no GitHub OAuth app: set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET, the OAuth app registered for this platform (its callback URL <platform>/-/sign-in/callback)');
109
+ const none = () => { throw new Error('GitHub is not an OpenID Connect issuer'); };
110
+ return { kind: 'github', name: 'GitHub', issuer: web, clientId, clientSecret, account: `${web}/settings/profile`, metadata: async () => none(), call: async () => none(), github: { web, api } };
111
+ }
112
+ /** The Volter provider (the settings, else the `VOLTER_*` names). */
113
+ export const identityClient = (opts = {}) => providerClient({ kind: 'volter', ...opts });
114
+ // ── the platform's own session: a random id in an HttpOnly cookie, the record beside the tokens ──
115
+ export const SESSION_COOKIE = 'volter_console_session';
116
+ const STATE_COOKIE = 'volter_console_sign_in';
117
+ const SESSION_SECONDS = 8 * 60 * 60;
118
+ const STATE_SECONDS = 10 * 60;
119
+ export const CALLBACK_PATH = '/-/sign-in/callback';
120
+ /** The platform's sessions, by the SHA-256 of the cookie's id, in its database (in memory when it keeps no state). */
121
+ export class Sessions {
122
+ db;
123
+ constructor(stateDir) { this.db = openDatabase(stateDir); }
124
+ key(sessionId) { return createHash('sha256').update(sessionId).digest('hex'); }
125
+ start(person) {
126
+ const sessionId = randomBytes(32).toString('base64url');
127
+ const now = Date.now();
128
+ // the expired go as new ones start: the table holds live sessions, not a history
129
+ this.db.delete(sessionRows).where(lte(sessionRows.expiresAt, new Date(now).toISOString())).run();
130
+ this.db.insert(sessionRows).values({ idHash: this.key(sessionId), subject: person.subject, email: person.email ?? null, name: person.name ?? null, expiresAt: new Date(now + SESSION_SECONDS * 1000).toISOString() }).run();
131
+ return sessionId;
132
+ }
133
+ read(sessionId) {
134
+ const r = this.db.select().from(sessionRows).where(eq(sessionRows.idHash, this.key(sessionId))).get();
135
+ if (!r)
136
+ return null;
137
+ if (Date.parse(r.expiresAt) <= Date.now()) {
138
+ this.end(sessionId);
139
+ return null;
140
+ }
141
+ return { subject: r.subject, ...(r.email ? { email: r.email } : {}), ...(r.name ? { name: r.name } : {}), expiresAt: r.expiresAt };
142
+ }
143
+ end(sessionId) { this.db.delete(sessionRows).where(eq(sessionRows.idHash, this.key(sessionId))).run(); }
144
+ }
145
+ const cookieOf = (request, name) => {
146
+ for (const part of (request.headers.get('cookie') ?? '').split(';')) {
147
+ const [k, ...v] = part.trim().split('=');
148
+ if (k === name)
149
+ return v.join('=');
150
+ }
151
+ return undefined;
152
+ };
153
+ // over https a cookie is `__Host-`: set only by this origin, never by a sibling page on the same site (a World origin
154
+ // under the same registrable domain cannot toss a session or a sign-in state of its own over the platform's)
155
+ const cookieName = (origin, base) => (origin.startsWith('https:') ? `__Host-${base}` : base);
156
+ const cookie = (origin, name, value, _path, seconds) => `${cookieName(origin, name)}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${seconds}${origin.startsWith('https:') ? '; Secure' : ''}`;
157
+ export const sessionIdOf = (request, origin) => cookieOf(request, cookieName(origin, SESSION_COOKIE));
158
+ /** Who a request is: the platform's session its cookie names, while it lasts. */
159
+ export function whoIs(sessions, request, origin) {
160
+ const sessionId = sessionIdOf(request, origin);
161
+ if (!sessionId)
162
+ return null;
163
+ const s = sessions.read(sessionId);
164
+ return s ? { userId: s.subject, sessionId, ...(s.email ? { email: s.email } : {}), ...(s.name ? { name: s.name } : {}) } : null;
165
+ }
166
+ // the sign-in state (the state and the PKCE verifier) rides a short-lived cookie signed with the client secret
167
+ const sign = (identity, payload) => { const body = Buffer.from(JSON.stringify(payload)).toString('base64url'); return `${body}.${createHmac('sha256', identity.clientSecret).update(body).digest('base64url')}`; };
168
+ const unsign = (identity, value) => {
169
+ if (!value)
170
+ return null;
171
+ const [body, mac, extra] = value.split('.');
172
+ if (!body || !mac || extra !== undefined)
173
+ return null;
174
+ const want = Buffer.from(createHmac('sha256', identity.clientSecret).update(body).digest('base64url'));
175
+ const got = Buffer.from(mac);
176
+ if (want.length !== got.length || !timingSafeEqual(want, got))
177
+ return null;
178
+ try {
179
+ const p = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
180
+ return typeof p.exp === 'number' && p.exp > Date.now() / 1000 ? p : null;
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ };
186
+ const text = (body, status) => new Response(body, { status, headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } });
187
+ /** Where a sign-in may return a person: a path of this platform (its pages, or `/-/…`), never another origin or a scheme. */
188
+ export const signInReturn = (next) => (next && /^\/(?![/\\])[A-Za-z0-9/_?=&%.~-]*$/.test(next) ? next : undefined);
189
+ /** Start signing in: the authorization code with PKCE (S256) at the provider. */
190
+ export async function beginSignIn(identity, origin, next) {
191
+ let authorize;
192
+ if (identity.github)
193
+ authorize = `${identity.github.web}/login/oauth/authorize`;
194
+ else {
195
+ try {
196
+ authorize = (await identity.metadata()).authorization_endpoint;
197
+ }
198
+ catch (e) {
199
+ return text(`Signing in is unavailable: ${e.message}`, 503);
200
+ }
201
+ }
202
+ const state = randomBytes(16).toString('base64url');
203
+ const verifier = randomBytes(48).toString('base64url');
204
+ const challenge = createHash('sha256').update(verifier).digest('base64url');
205
+ const target = new URL(authorize);
206
+ // GitHub: the profile and the addresses, nothing of the person's repositories
207
+ const asked = identity.github ? { scope: 'read:user user:email', allow_signup: 'true' } : { response_type: 'code', scope: 'openid profile email' };
208
+ target.search = new URLSearchParams({ client_id: identity.clientId, redirect_uri: `${origin}${CALLBACK_PATH}`, ...asked, state, code_challenge: challenge, code_challenge_method: 'S256' }).toString();
209
+ const back = signInReturn(next);
210
+ const pending = sign(identity, { state, verifier, exp: Math.floor(Date.now() / 1000) + STATE_SECONDS, ...(back ? { next: back } : {}) });
211
+ return new Response(null, { status: 302, headers: { location: target.toString(), 'cache-control': 'no-store', 'set-cookie': cookie(origin, STATE_COOKIE, pending, CALLBACK_PATH, STATE_SECONDS) } });
212
+ }
213
+ /** The issuer's answer: the code traded with the client secret, the id token verified, the session started. */
214
+ export async function finishSignIn(identity, sessions, origin, request, landing, seen) {
215
+ const url = new URL(request.url);
216
+ const expected = unsign(identity, cookieOf(request, cookieName(origin, STATE_COOKIE)));
217
+ const supplied = url.searchParams.get('state') ?? '';
218
+ if (!expected || !supplied || supplied.length !== expected.state.length || !timingSafeEqual(Buffer.from(supplied), Buffer.from(expected.state)))
219
+ return text('Sign-in refused: the sign-in expired or did not start here. Sign in again.', 401);
220
+ if (url.searchParams.get('error'))
221
+ return text(`Sign-in did not complete: ${url.searchParams.get('error_description') ?? url.searchParams.get('error')}`, 401);
222
+ const code = url.searchParams.get('code');
223
+ if (!code)
224
+ return text('Sign-in did not complete.', 401);
225
+ const person = identity.github ? await githubPerson(identity, identity.github, origin, code, expected.verifier) : await oidcPerson(identity, origin, code, expected.verifier);
226
+ if (person instanceof Response)
227
+ return person;
228
+ // the directory knows the person before their session opens: an invitation to their address lets them in now
229
+ if (seen) {
230
+ try {
231
+ await seen(person);
232
+ }
233
+ catch (e) {
234
+ return text(`Signing in is unavailable: ${e.message}`, 503);
235
+ }
236
+ }
237
+ const sessionId = sessions.start({ subject: person.subject, ...(person.email ? { email: person.email } : {}), ...(person.name ? { name: person.name } : {}) });
238
+ const headers = new Headers({ location: signInReturn(expected.next) ?? landing, 'cache-control': 'no-store' });
239
+ headers.append('set-cookie', cookie(origin, SESSION_COOKIE, sessionId, '/', SESSION_SECONDS));
240
+ headers.append('set-cookie', cookie(origin, STATE_COOKIE, '', CALLBACK_PATH, 0));
241
+ return new Response(null, { status: 302, headers });
242
+ }
243
+ /** An OpenID Connect provider's answer: the code traded with the client secret, the id token verified. */
244
+ async function oidcPerson(identity, origin, code, verifier) {
245
+ let meta;
246
+ try {
247
+ meta = await identity.metadata();
248
+ }
249
+ catch (e) {
250
+ return text(`Signing in is unavailable: ${e.message}`, 503);
251
+ }
252
+ const res = await fetch(meta.token_endpoint, {
253
+ method: 'POST',
254
+ headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
255
+ body: new URLSearchParams({ grant_type: 'authorization_code', code, code_verifier: verifier, redirect_uri: `${origin}${CALLBACK_PATH}`, client_id: identity.clientId, client_secret: identity.clientSecret }).toString(),
256
+ });
257
+ const tokens = (await res.json().catch(() => ({})));
258
+ if (!res.ok || !tokens.id_token)
259
+ return text('Sign-in refused: the authorization code was not accepted.', 401);
260
+ let person;
261
+ try {
262
+ person = await verifyIdentityToken(tokens.id_token, { issuer: identity.issuer, audience: identity.clientId });
263
+ }
264
+ catch {
265
+ return text('Sign-in refused: the identity token did not verify.', 401);
266
+ }
267
+ // the id token names the person; their email and name, when it does not carry them, are the userinfo's (the scopes granted them)
268
+ let known = {};
269
+ if ((!person.email || !person.name) && tokens.access_token && meta.userinfo_endpoint) {
270
+ const info = await fetch(meta.userinfo_endpoint, { headers: { authorization: `Bearer ${tokens.access_token}`, accept: 'application/json' } }).catch(() => null);
271
+ if (info?.ok)
272
+ known = (await info.json().catch(() => ({})));
273
+ if (known.sub !== person.subject)
274
+ known = {};
275
+ }
276
+ const email = person.email ?? known.email;
277
+ const name = person.name ?? known.name;
278
+ // whether the provider vouches for the address: the one that says it (the id token for its own email, else the userinfo)
279
+ // an issuer that says nothing about an address vouches for it only in a domain its operator named (OIDC_TRUST_EMAIL_DOMAINS)
280
+ const said = person.email ? person.emailVerified : known.email_verified;
281
+ const domain = (email ?? '').split('@')[1]?.toLowerCase() ?? '';
282
+ const emailVerified = said === true || (said === undefined && domain !== '' && (identity.trustEmailDomains ?? []).includes(domain));
283
+ return { subject: person.subject, ...(email ? { email } : {}), emailVerified, ...(name ? { name } : {}) };
284
+ }
285
+ /** GitHub's answer: the code traded for a token (JSON asked for; GitHub's default answer is a form), then the person
286
+ * and their primary verified address read with it. The token is used for these two reads and kept nowhere. */
287
+ async function githubPerson(identity, gh, origin, code, verifier) {
288
+ const res = await fetch(`${gh.web}/login/oauth/access_token`, {
289
+ method: 'POST',
290
+ headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
291
+ body: new URLSearchParams({ client_id: identity.clientId, client_secret: identity.clientSecret, code, code_verifier: verifier, redirect_uri: `${origin}${CALLBACK_PATH}` }).toString(),
292
+ }).catch(() => null);
293
+ const token = (await res?.json().catch(() => ({})) ?? {});
294
+ if (!res?.ok || !token.access_token)
295
+ return text(`Sign-in refused: GitHub did not accept the code${token.error_description ? ` (${token.error_description})` : ''}.`, 401);
296
+ const read = async (path) => {
297
+ const r = await fetch(`${gh.api}${path}`, { headers: { authorization: `Bearer ${token.access_token}`, accept: 'application/vnd.github+json', 'x-github-api-version': '2022-11-28', 'user-agent': 'volter-world-platform' } }).catch(() => null);
298
+ return r?.ok ? (await r.json().catch(() => null)) : null;
299
+ };
300
+ const user = await read('/user');
301
+ if (!user || typeof user.id !== 'number')
302
+ return text('Sign-in refused: GitHub did not say who signed in.', 401);
303
+ // the primary address only when GitHub has verified it: an unverified one could be anyone's
304
+ const emails = (await read('/user/emails')) ?? [];
305
+ const primary = emails.find((e) => e.primary && e.verified) ?? emails.find((e) => e.verified);
306
+ return { subject: `github:${user.id}`, ...(primary ? { email: primary.email, emailVerified: true } : {}), ...(user.name || user.login ? { name: user.name || user.login } : {}) };
307
+ }
308
+ /** Sign out: the platform's session ends and its cookie goes. */
309
+ export function endSession(sessions, origin, request, landing) {
310
+ const sessionId = sessionIdOf(request, origin);
311
+ if (sessionId)
312
+ sessions.end(sessionId);
313
+ return new Response(null, { status: 302, headers: { location: landing, 'cache-control': 'no-store', 'set-cookie': cookie(origin, SESSION_COOKIE, '', '/', 0) } });
314
+ }
@@ -0,0 +1,14 @@
1
+ /** Early-access features (layer 8: Twenty's lab of public flags with a title and a description, Sentry's Early Adopter switch,
2
+ * Cal.com's opt-in features). A new surface lands here first; an org turns it on, or turns on everything with Early adopter. */
3
+ export declare const EARLY_FEATURES: readonly [{
4
+ readonly key: "webhooks";
5
+ readonly name: "Webhooks";
6
+ readonly description: "Events the org raises — a world provisioned, a member added, the plan changed — sent to an address of yours, signed.";
7
+ readonly since: "2026-09-08";
8
+ }, {
9
+ readonly key: "usage_by_day";
10
+ readonly name: "Usage by day";
11
+ readonly description: "A bar per day of the period, under the org's meters.";
12
+ readonly since: "2026-09-08";
13
+ }];
14
+ export type EarlyFeatureKey = (typeof EARLY_FEATURES)[number]['key'];
@@ -0,0 +1,7 @@
1
+ // Labs: the early-access features an org turns on (platform.ts, `/-/orgs/<org>/labs`).
2
+ /** Early-access features (layer 8: Twenty's lab of public flags with a title and a description, Sentry's Early Adopter switch,
3
+ * Cal.com's opt-in features). A new surface lands here first; an org turns it on, or turns on everything with Early adopter. */
4
+ export const EARLY_FEATURES = [
5
+ { key: 'webhooks', name: 'Webhooks', description: 'Events the org raises — a world provisioned, a member added, the plan changed — sent to an address of yours, signed.', since: '2026-09-08' },
6
+ { key: 'usage_by_day', name: 'Usage by day', description: 'A bar per day of the period, under the org\'s meters.', since: '2026-09-08' },
7
+ ];
@@ -0,0 +1,16 @@
1
+ export type Mail = {
2
+ to: string[];
3
+ subject: string;
4
+ text: string;
5
+ };
6
+ export type Mailer = {
7
+ send: (mail: Mail) => Promise<{
8
+ id: string | null;
9
+ }>;
10
+ from: string;
11
+ };
12
+ export declare function mailer(opts?: {
13
+ apiKey?: string;
14
+ from?: string;
15
+ log?: (line: string) => void;
16
+ }): Mailer;
@@ -0,0 +1,27 @@
1
+ // The platform's own mail (punchlist 3.6): what the identity service and Polar do not send (an
2
+ // invitation is the identity service's mail, and a receipt Polar's) — a member added, a
3
+ // world deleted, the quota warning, the grace notice. Resend's UNMODIFIED SDK through the world's
4
+ // boundary: the resend twin in rehearsal (its inbox mirror shows every mail), real Resend in
5
+ // production with a verified sending domain (OWNER: the domain). No RESEND_API_KEY → the mail is
6
+ // written to the log, never invented.
7
+ import { Resend } from 'resend';
8
+ export function mailer(opts = {}) {
9
+ const from = opts.from ?? 'Volter World <noreply@volter.world>'; // --mail-from names another sender
10
+ const key = opts.apiKey ?? process.env.RESEND_API_KEY;
11
+ const log = opts.log ?? ((line) => process.stdout.write(`${line}\n`));
12
+ if (!key)
13
+ return { from, send: async (mail) => { log(`mail (no RESEND_API_KEY, not sent) to ${mail.to.join(', ')} ${mail.subject}`); return { id: null }; } };
14
+ const resend = new Resend(key);
15
+ return {
16
+ from,
17
+ send: async (mail) => {
18
+ const { data, error } = await resend.emails.send({ from, to: mail.to, subject: mail.subject, text: mail.text });
19
+ if (error) {
20
+ log(`mail failed to ${mail.to.join(', ')} ${mail.subject}: ${error.message}`);
21
+ return { id: null };
22
+ }
23
+ log(`mail sent to ${mail.to.join(', ')} ${mail.subject} ${data?.id ?? ''}`);
24
+ return { id: data?.id ?? null };
25
+ },
26
+ };
27
+ }
@@ -0,0 +1,15 @@
1
+ import { RESERVED_ORG_NAMES } from '../client/reserved.js';
2
+ export { RESERVED_ORG_NAMES };
3
+ export type Pages = {
4
+ handle: (request: Request) => Promise<Response | null>;
5
+ };
6
+ /** The platform's pages: `GET /-/app.js` (the client), `GET /-/styles.css` (the kit's, then the platform's),
7
+ * `GET /-/brand/*` (the brand's tokens and faces) and the shell for any other GET outside `/-/` and `/.well-known/`;
8
+ * null for anything else, which is a door's. */
9
+ export declare function createPages(): Pages;
10
+ /** A page that is one card, rendered on the server with the kit's styles: the command's approval, a refusal, a notice.
11
+ * `inner` is HTML the caller has escaped. `csp` replaces the default policy (no script at all). */
12
+ export declare function pageShell(title: string, inner: string, opts?: {
13
+ csp?: string;
14
+ status?: number;
15
+ }): Response;
@@ -0,0 +1,73 @@
1
+ // THE PLATFORM'S PAGES as the platform serves them (docs/contributing/architecture.md, "The hosted product"): one React
2
+ // client (client/platform.tsx) behind one shell at the root of the platform's origin — `/`, `/<org>`, `/<org>/members`,
3
+ // `/account`… — its bundle and styles under `/-/`, and a card page for what is served without the client (the command's
4
+ // approval, a refusal). The styles are the kit's (@volter/world-console, the dashboard's package) then the platform's
5
+ // own, so the platform and a World's pages read as one product. No third-party script is ever served here: the site's
6
+ // tags stay on the site.
7
+ import { readFileSync } from 'node:fs';
8
+ import { bundleClient } from '@volter/world-core';
9
+ import { brandAsset, kitCss, WORLD_LOGO } from '@volter/world-console';
10
+ import { RESERVED_ORG_NAMES } from "../client/reserved.js";
11
+ export { RESERVED_ORG_NAMES };
12
+ /** A file URL's path as the filesystem names it: decoded, and without the leading slash before a Windows drive (`/C:/…`). */
13
+ const pathOf = (url) => decodeURIComponent(url.pathname).replace(/^\/([A-Za-z]:)/, '$1');
14
+ const CLIENT_ENTRY = () => pathOf(new URL('../client/platform.tsx', import.meta.url)); // lazy: never a top-level import.meta.url URL
15
+ const CLIENT_CSS = () => pathOf(new URL('../client/platform.css', import.meta.url));
16
+ const attr = (v) => v.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
17
+ const HEAD = (title) => `<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${attr(title)}</title><link rel="icon" type="image/svg+xml" href="${WORLD_LOGO}"><link rel="stylesheet" href="/-/brand/tokens.css"><link rel="stylesheet" href="/-/styles.css">`;
18
+ const SHELL_CSP = "default-src 'self'; img-src 'self' data: https://brand.volter.ai; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'";
19
+ /** The catalog's vendors, most used first, for the page's vendor picker: `vendor:s` for a supported twin, `vendor:p` for a
20
+ * preview. Read once from the repo's generated index (generated/index.json) where it is beside the platform; elsewhere
21
+ * the page offers its short list and takes any name typed. */
22
+ let catalog = null;
23
+ const vendors = () => {
24
+ if (catalog !== null)
25
+ return catalog;
26
+ try {
27
+ const index = JSON.parse(readFileSync(pathOf(new URL('../../../generated/index.json', import.meta.url)), 'utf8'));
28
+ catalog = index.rows.filter((r) => r.exists && /^[a-z0-9-]+$/.test(r.vendor)).sort((a, b) => a.rank - b.rank).map((r) => `${r.vendor}:${r.status === 'Supported' ? 's' : 'p'}`).join(',');
29
+ }
30
+ catch {
31
+ catalog = '';
32
+ }
33
+ return catalog;
34
+ };
35
+ /** The operator's consented support view is opened with its token in the address (`/?support=tok_su_…`): only a
36
+ * support token's shape is carried into the page, and only as an attribute. */
37
+ const SUPPORT_TOKEN = /^tok_su_[A-Za-z0-9_-]{16,128}$/;
38
+ /** The platform's pages: `GET /-/app.js` (the client), `GET /-/styles.css` (the kit's, then the platform's),
39
+ * `GET /-/brand/*` (the brand's tokens and faces) and the shell for any other GET outside `/-/` and `/.well-known/`;
40
+ * null for anything else, which is a door's. */
41
+ export function createPages() {
42
+ return {
43
+ async handle(request) {
44
+ if (request.method !== 'GET' && request.method !== 'HEAD')
45
+ return null;
46
+ const url = new URL(request.url);
47
+ const path = url.pathname;
48
+ if (path === '/-/app.js') {
49
+ try {
50
+ return new Response(await bundleClient(CLIENT_ENTRY()), { headers: { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' } });
51
+ }
52
+ catch (error) {
53
+ return new Response(String(error), { status: 500 });
54
+ }
55
+ }
56
+ if (path === '/-/styles.css')
57
+ return new Response(`${kitCss()}\n${readFileSync(CLIENT_CSS(), 'utf8')}`, { headers: { 'content-type': 'text/css; charset=utf-8', 'cache-control': 'no-cache' } });
58
+ if (path.startsWith('/-/brand/'))
59
+ return brandAsset(path.slice('/-/brand/'.length));
60
+ if (path.startsWith('/-/') || path === '/-' || path.startsWith('/.well-known/'))
61
+ return null;
62
+ const support = url.searchParams.get('support') ?? '';
63
+ const token = SUPPORT_TOKEN.test(support) ? ` data-token="${attr(support)}"` : '';
64
+ return new Response(`<!doctype html>\n<html lang="en"><head>${HEAD('Volter World')}</head><body><div id="root"${token} data-vendors="${attr(vendors())}"></div><script type="module" src="/-/app.js"></script></body></html>`, { headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'referrer-policy': 'no-referrer', 'content-security-policy': SHELL_CSP } });
65
+ },
66
+ };
67
+ }
68
+ /** A page that is one card, rendered on the server with the kit's styles: the command's approval, a refusal, a notice.
69
+ * `inner` is HTML the caller has escaped. `csp` replaces the default policy (no script at all). */
70
+ export function pageShell(title, inner, opts = {}) {
71
+ const html = `<!doctype html>\n<html lang="en"><head>${HEAD(title)}</head><body class="portal"><main class="card"><p class="brand"><img src="${WORLD_LOGO}" alt="" width="22" height="22"> Volter World</p>${inner}</main></body></html>`;
72
+ return new Response(html, { status: opts.status ?? 200, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'content-security-policy': opts.csp ?? "default-src 'self'; img-src 'self' https://brand.volter.ai; style-src 'self' 'unsafe-inline'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'" } });
73
+ }