@hraness/suite-accounts 0.9.20

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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +496 -0
  3. package/compiler-foundation.css +1 -0
  4. package/dist/auth-client.js +16 -0
  5. package/dist/auth-proxy.js +757 -0
  6. package/dist/bearer-verifier.js +1119 -0
  7. package/dist/browser-session.js +208 -0
  8. package/dist/client-configuration.js +470 -0
  9. package/dist/convex-browser-auth-browser.js +923 -0
  10. package/dist/convex-browser-auth-server.js +846 -0
  11. package/dist/convex-browser-auth.js +597 -0
  12. package/dist/entitlements.js +502 -0
  13. package/dist/identity/functions.js +44 -0
  14. package/dist/identity/index.js +1716 -0
  15. package/dist/identity/return-targets.js +33 -0
  16. package/dist/index.js +1133 -0
  17. package/dist/oidc-device-code.js +253 -0
  18. package/dist/oidc-rp.js +2259 -0
  19. package/dist/oidc-session-policy.js +5 -0
  20. package/dist/oidc-surface-server.js +2332 -0
  21. package/dist/profile-form.js +1934 -0
  22. package/dist/profile.js +17 -0
  23. package/dist/public-config.js +462 -0
  24. package/dist/react.js +137 -0
  25. package/dist/receipt-verifier.js +619 -0
  26. package/dist/registry.js +327 -0
  27. package/dist/stylex-manifest.json +1 -0
  28. package/dist/stylex.css +435 -0
  29. package/dist/urls.js +391 -0
  30. package/package.json +263 -0
  31. package/portfolio-inventory.json +37 -0
  32. package/src/auth-client.ts +37 -0
  33. package/src/auth-proxy.ts +473 -0
  34. package/src/bearer-verifier.ts +478 -0
  35. package/src/browser-session.ts +260 -0
  36. package/src/client-configuration.ts +172 -0
  37. package/src/convex-browser-auth-browser.ts +238 -0
  38. package/src/convex-browser-auth-server.ts +492 -0
  39. package/src/convex-browser-auth.ts +208 -0
  40. package/src/convex-url.ts +74 -0
  41. package/src/entitlements.ts +317 -0
  42. package/src/identity/catalog.ts +227 -0
  43. package/src/identity/consumers.ts +53 -0
  44. package/src/identity/functions.ts +70 -0
  45. package/src/identity/identifiers.ts +82 -0
  46. package/src/identity/index.ts +10 -0
  47. package/src/identity/links.ts +280 -0
  48. package/src/identity/membership.ts +156 -0
  49. package/src/identity/principals.ts +301 -0
  50. package/src/identity/profiles-v2.ts +330 -0
  51. package/src/identity/profiles.ts +524 -0
  52. package/src/identity/return-targets.ts +20 -0
  53. package/src/identity/usernames.ts +101 -0
  54. package/src/identity/views.ts +289 -0
  55. package/src/immutable.ts +25 -0
  56. package/src/index.ts +7 -0
  57. package/src/oidc-continuation.ts +59 -0
  58. package/src/oidc-device-code.ts +401 -0
  59. package/src/oidc-rp.ts +2217 -0
  60. package/src/oidc-session-policy.ts +6 -0
  61. package/src/oidc-surface-server.ts +186 -0
  62. package/src/profile-form.css +1 -0
  63. package/src/profile-form.stylex.ts +226 -0
  64. package/src/profile-form.tsx +314 -0
  65. package/src/profile.ts +33 -0
  66. package/src/public-config.ts +294 -0
  67. package/src/public-profile-form-result.ts +82 -0
  68. package/src/public-profile-form.tsx +319 -0
  69. package/src/public-profile-links.ts +100 -0
  70. package/src/react.tsx +196 -0
  71. package/src/receipt-verifier.ts +273 -0
  72. package/src/registry.ts +600 -0
  73. package/src/urls.ts +234 -0
@@ -0,0 +1,846 @@
1
+ // src/identity/consumers.ts
2
+ import { err, ok } from "@hraness/result";
3
+
4
+ // src/immutable.ts
5
+ function deepFreeze(value) {
6
+ const visited = new WeakSet;
7
+ function freezeOwned(current) {
8
+ if (current === null || typeof current !== "object")
9
+ return;
10
+ if (visited.has(current))
11
+ return;
12
+ visited.add(current);
13
+ for (const key of Reflect.ownKeys(current)) {
14
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
15
+ if (descriptor !== undefined && "value" in descriptor) {
16
+ freezeOwned(descriptor.value);
17
+ }
18
+ }
19
+ Object.freeze(current);
20
+ }
21
+ freezeOwned(value);
22
+ return value;
23
+ }
24
+
25
+ // src/identity/consumers.ts
26
+ var SUITE_CONSUMER_IDS = deepFreeze([
27
+ "accounts",
28
+ "act60",
29
+ "elders",
30
+ "soundfish",
31
+ "oh-computer",
32
+ "draw-money",
33
+ "oprte",
34
+ "sponge"
35
+ ]);
36
+ var LEGACY_SUITE_CONSUMER_IDS = deepFreeze([
37
+ "kitchen"
38
+ ]);
39
+ function parseSuiteConsumerId(value) {
40
+ switch (value) {
41
+ case "accounts":
42
+ case "act60":
43
+ case "elders":
44
+ case "soundfish":
45
+ case "oh-computer":
46
+ case "draw-money":
47
+ case "oprte":
48
+ case "sponge":
49
+ return ok(value);
50
+ case "kitchen":
51
+ return ok("oprte");
52
+ default:
53
+ return err("invalid-consumer");
54
+ }
55
+ }
56
+
57
+ // src/registry.ts
58
+ var SUITE_ACCOUNTS_REMOTE_ENVIRONMENTS = deepFreeze([
59
+ "production"
60
+ ]);
61
+ var accountsCookies = deepFreeze({
62
+ chunked: ["account_data", "session_data"],
63
+ names: [
64
+ "account_data",
65
+ "convex_jwt",
66
+ "dont_remember",
67
+ "session_data",
68
+ "session_token"
69
+ ]
70
+ });
71
+ var consumerCookies = deepFreeze({
72
+ chunked: ["session_data"],
73
+ names: ["dont_remember", "session_data", "session_token"]
74
+ });
75
+ var SUITE_ACCOUNTS_DEPLOYMENTS = deepFreeze({
76
+ production: {
77
+ accountsOrigin: "https://account.hraness.com",
78
+ convexSiteUrl: "https://qualified-marmot-22.convex.site",
79
+ convexUrl: "https://qualified-marmot-22.convex.cloud"
80
+ }
81
+ });
82
+ function unsupported(siteUrl) {
83
+ return { billingReturn: { kind: "unsupported" }, siteUrl };
84
+ }
85
+ function oidcSite(id, displayName, productionSiteUrl) {
86
+ return {
87
+ auth: { basePath: "/api/suite-auth", kind: "oidc-rp" },
88
+ displayName,
89
+ environments: {
90
+ production: unsupported(productionSiteUrl)
91
+ },
92
+ id
93
+ };
94
+ }
95
+ var SUITE_ACCOUNTS_REGISTERED_CONSUMER_IDS = deepFreeze([
96
+ "accounts",
97
+ "act60",
98
+ "soundfish",
99
+ "oh-computer",
100
+ "draw-money",
101
+ "sponge"
102
+ ]);
103
+ var SUITE_ACCOUNTS_CONSUMERS = deepFreeze({
104
+ accounts: {
105
+ auth: {
106
+ basePath: "/api/auth",
107
+ cookies: accountsCookies,
108
+ kind: "authority"
109
+ },
110
+ displayName: "Accounts",
111
+ environments: {
112
+ production: {
113
+ billingReturn: { kind: "supported", path: "/account" },
114
+ siteUrl: "https://account.hraness.com"
115
+ }
116
+ },
117
+ id: "accounts"
118
+ },
119
+ act60: oidcSite("act60", "ACT60", "https://act60.me"),
120
+ soundfish: {
121
+ auth: { basePath: "/api/suite-auth", kind: "oidc-rp" },
122
+ displayName: "Soundfish",
123
+ environments: {
124
+ production: unsupported("https://sound.fish")
125
+ },
126
+ id: "soundfish"
127
+ },
128
+ "oh-computer": oidcSite("oh-computer", "Oh", "https://oh.computer"),
129
+ "draw-money": {
130
+ auth: {
131
+ basePath: "/api/auth",
132
+ cookies: consumerCookies,
133
+ kind: "proxy"
134
+ },
135
+ displayName: "Draw Money",
136
+ environments: {
137
+ production: unsupported("https://draw.money")
138
+ },
139
+ id: "draw-money"
140
+ },
141
+ sponge: oidcSite("sponge", "Sponge", "https://spongesearch.com")
142
+ });
143
+ var SUITE_ACCOUNTS_CURRENT_ORIGIN_OVERRIDES = deepFreeze({
144
+ sponge: {
145
+ production: unsupported("https://sponge.computer")
146
+ }
147
+ });
148
+ var SUITE_ACCOUNTS_CURRENT_CONSUMER_IDS = deepFreeze([
149
+ "accounts",
150
+ "act60",
151
+ "soundfish",
152
+ "oh-computer",
153
+ "sponge",
154
+ "peopleblade",
155
+ "aicharts",
156
+ "hraness",
157
+ "soulscrape",
158
+ "platonik",
159
+ "ghostget",
160
+ "alt",
161
+ "algal"
162
+ ]);
163
+ function currentOidcSite(id, displayName, productionSiteUrl) {
164
+ return {
165
+ auth: { basePath: "/api/suite-auth", kind: "oidc-rp" },
166
+ displayName,
167
+ environments: {
168
+ production: unsupported(productionSiteUrl)
169
+ },
170
+ id
171
+ };
172
+ }
173
+ var SUITE_ACCOUNTS_CURRENT_CONSUMERS = deepFreeze({
174
+ accounts: SUITE_ACCOUNTS_CONSUMERS.accounts,
175
+ act60: SUITE_ACCOUNTS_CONSUMERS.act60,
176
+ soundfish: SUITE_ACCOUNTS_CONSUMERS.soundfish,
177
+ "oh-computer": SUITE_ACCOUNTS_CONSUMERS["oh-computer"],
178
+ sponge: currentOidcSite("sponge", "Sponge", SUITE_ACCOUNTS_CURRENT_ORIGIN_OVERRIDES.sponge.production.siteUrl),
179
+ peopleblade: currentOidcSite("peopleblade", "PeopleBlade", "https://peopleblade.com"),
180
+ aicharts: currentOidcSite("aicharts", "AI Charts", "https://aicharts.io"),
181
+ hraness: currentOidcSite("hraness", "Hraness", "https://hraness.com"),
182
+ soulscrape: currentOidcSite("soulscrape", "Soulscrape", "https://soulscrape.com"),
183
+ platonik: currentOidcSite("platonik", "Platonik", "https://platonik.space"),
184
+ ghostget: currentOidcSite("ghostget", "Ghostget", "https://ghostget.com"),
185
+ alt: currentOidcSite("alt", "Alt", "https://alt.dog"),
186
+ algal: currentOidcSite("algal", "Algal", "https://algal.cloud")
187
+ });
188
+ var SUITE_ACCOUNTS_ACTIVE_CONSUMER_IDS = deepFreeze([
189
+ "accounts",
190
+ "act60",
191
+ "soundfish",
192
+ "oh-computer",
193
+ "sponge"
194
+ ]);
195
+ var SUITE_EMAIL_OTP_REQUIRED_OIDC_CONSUMER_IDS = deepFreeze(SUITE_ACCOUNTS_REGISTERED_CONSUMER_IDS.filter((consumer) => SUITE_ACCOUNTS_CONSUMERS[consumer].auth.kind === "oidc-rp"));
196
+ function suiteAccountsConsumerRequiresEmailOtp(consumer) {
197
+ return SUITE_EMAIL_OTP_REQUIRED_OIDC_CONSUMER_IDS.includes(consumer);
198
+ }
199
+ var SUITE_ACCOUNTS_LINKED_OIDC_CONSUMER_IDS = deepFreeze([
200
+ "soundfish"
201
+ ]);
202
+ var SUITE_ACCOUNTS_CURRENT_EMAIL_OTP_REQUIRED_OIDC_CONSUMER_IDS = deepFreeze(SUITE_ACCOUNTS_CURRENT_CONSUMER_IDS.filter((consumer) => SUITE_ACCOUNTS_CURRENT_CONSUMERS[consumer].auth.kind === "oidc-rp"));
203
+ var SUITE_ACCOUNTS_CURRENT_LINKED_OIDC_CONSUMER_IDS = deepFreeze([
204
+ "soundfish",
205
+ "peopleblade",
206
+ "ghostget"
207
+ ]);
208
+ var SUITE_ACCOUNTS_CURRENT_BROWSER_DEVICE_CODE_CONSUMER_IDS = deepFreeze([
209
+ "act60",
210
+ "soundfish",
211
+ "oh-computer",
212
+ "sponge",
213
+ "peopleblade",
214
+ "aicharts",
215
+ "hraness",
216
+ "soulscrape",
217
+ "platonik",
218
+ "ghostget",
219
+ "alt"
220
+ ]);
221
+ var SUITE_ACCOUNTS_CURRENT_DEVICE_CLIENT_IDS = deepFreeze([
222
+ "algal-cli"
223
+ ]);
224
+ var SUITE_ACCOUNTS_CURRENT_DEVICE_CLIENTS = deepFreeze({
225
+ "algal-cli": {
226
+ consumer: "algal",
227
+ displayName: "Algal CLI",
228
+ environments: ["production"],
229
+ id: "algal-cli"
230
+ }
231
+ });
232
+ function isSuiteAccountsConsumerId(value) {
233
+ return typeof value === "string" && SUITE_CONSUMER_IDS.includes(value);
234
+ }
235
+ function isSuiteAccountsRegisteredConsumerId(value) {
236
+ return typeof value === "string" && SUITE_ACCOUNTS_REGISTERED_CONSUMER_IDS.includes(value);
237
+ }
238
+ function isSuiteAccountsOidcConsumerId(value) {
239
+ return isSuiteAccountsRegisteredConsumerId(value) && SUITE_ACCOUNTS_CONSUMERS[value].auth.kind === "oidc-rp";
240
+ }
241
+ function isSuiteAccountsLinkedOidcConsumerId(value) {
242
+ return SUITE_ACCOUNTS_LINKED_OIDC_CONSUMER_IDS.includes(value);
243
+ }
244
+ function isSuiteAccountsOAuthConsumerId(value) {
245
+ return isSuiteAccountsRegisteredConsumerId(value) && SUITE_ACCOUNTS_CONSUMERS[value].auth.kind === "oidc-rp";
246
+ }
247
+ function isSuiteAccountsCurrentConsumerId(value) {
248
+ return typeof value === "string" && SUITE_ACCOUNTS_CURRENT_CONSUMER_IDS.includes(value);
249
+ }
250
+ function isSuiteAccountsCurrentOidcConsumerId(value) {
251
+ return getSuiteAccountsCurrentConsumer(value).auth.kind === "oidc-rp";
252
+ }
253
+ function isSuiteAccountsCurrentLinkedOidcConsumerId(value) {
254
+ return SUITE_ACCOUNTS_CURRENT_LINKED_OIDC_CONSUMER_IDS.includes(value);
255
+ }
256
+ function isSuiteAccountsCurrentOAuthConsumerId(value) {
257
+ return getSuiteAccountsCurrentConsumer(value).auth.kind === "oidc-rp";
258
+ }
259
+ function isSuiteAccountsCurrentBrowserDeviceCodeConsumerId(value) {
260
+ return typeof value === "string" && SUITE_ACCOUNTS_CURRENT_BROWSER_DEVICE_CODE_CONSUMER_IDS.includes(value);
261
+ }
262
+ function isSuiteAccountsCurrentDeviceClientId(value) {
263
+ return typeof value === "string" && SUITE_ACCOUNTS_CURRENT_DEVICE_CLIENT_IDS.includes(value);
264
+ }
265
+ function suiteAccountsCurrentConsumerRequiresEmailOtp(consumer) {
266
+ return SUITE_ACCOUNTS_CURRENT_EMAIL_OTP_REQUIRED_OIDC_CONSUMER_IDS.includes(consumer);
267
+ }
268
+ function getSuiteAccountsConsumer(consumer) {
269
+ return SUITE_ACCOUNTS_CONSUMERS[consumer];
270
+ }
271
+ function getSuiteAccountsConsumerEnvironment(consumer, environment) {
272
+ if (!isSuiteAccountsRegisteredConsumerId(consumer))
273
+ return null;
274
+ const registration = getSuiteAccountsConsumer(consumer);
275
+ return registration.environments[environment] ?? null;
276
+ }
277
+ function getSuiteAccountsCurrentConsumer(consumer) {
278
+ return SUITE_ACCOUNTS_CURRENT_CONSUMERS[consumer];
279
+ }
280
+ function getSuiteAccountsCurrentConsumerEnvironment(consumer, environment) {
281
+ if (!isSuiteAccountsCurrentConsumerId(consumer))
282
+ return null;
283
+ return getSuiteAccountsCurrentConsumer(consumer).environments[environment] ?? null;
284
+ }
285
+ function isSuiteAccountsActiveConsumerId(value) {
286
+ return SUITE_ACCOUNTS_ACTIVE_CONSUMER_IDS.includes(value);
287
+ }
288
+ function getSuiteAccountsDeployment(environment) {
289
+ return SUITE_ACCOUNTS_DEPLOYMENTS[environment];
290
+ }
291
+
292
+ // src/urls.ts
293
+ var SUITE_ACCOUNTS_OAUTH_RESOURCE = "https://hraness.com/suite";
294
+ var CENTRAL_PATHS = deepFreeze({
295
+ account: "/account",
296
+ home: "/",
297
+ login: "/login"
298
+ });
299
+ function suiteAccountsCentralUrl(environment, destination) {
300
+ return new URL(CENTRAL_PATHS[destination], getSuiteAccountsDeployment(environment).accountsOrigin).href;
301
+ }
302
+ function suiteAccountsBillingReturnUrl(consumer, environment) {
303
+ const consumerEnvironment = getSuiteAccountsConsumerEnvironment(consumer, environment);
304
+ if (consumerEnvironment === null)
305
+ return null;
306
+ return consumerEnvironment.billingReturn.kind === "supported" ? new URL(consumerEnvironment.billingReturn.path, consumerEnvironment.siteUrl).href : null;
307
+ }
308
+ function suiteAccountsOidcClientRegistration(consumer, environment) {
309
+ if (!isSuiteAccountsConsumerId(consumer) || !isSuiteAccountsOAuthConsumerId(consumer))
310
+ return null;
311
+ const consumerEnvironment = getSuiteAccountsConsumerEnvironment(consumer, environment);
312
+ if (consumerEnvironment === null)
313
+ return null;
314
+ return deepFreeze({
315
+ callbackUrl: new URL("/api/suite-auth/callback", consumerEnvironment.siteUrl).href,
316
+ clientId: `hraness:${consumer}:${environment}:v1`
317
+ });
318
+ }
319
+ function suiteAccountsCurrentOidcClientRegistration(consumer, environment) {
320
+ if (!isSuiteAccountsCurrentConsumerId(consumer) || !isSuiteAccountsCurrentOAuthConsumerId(consumer))
321
+ return null;
322
+ const consumerEnvironment = getSuiteAccountsCurrentConsumerEnvironment(consumer, environment);
323
+ if (consumerEnvironment === null)
324
+ return null;
325
+ return deepFreeze({
326
+ callbackUrl: new URL("/api/suite-auth/callback", consumerEnvironment.siteUrl).href,
327
+ clientId: `hraness:${consumer}:${environment}:v1`
328
+ });
329
+ }
330
+ function suiteAccountsOidcClientRequiresEmailOtp(clientId) {
331
+ if (typeof clientId !== "string")
332
+ return false;
333
+ return SUITE_EMAIL_OTP_REQUIRED_OIDC_CONSUMER_IDS.some((consumer) => suiteAccountsOidcClientRegistration(consumer, "production")?.clientId === clientId);
334
+ }
335
+ function suiteAccountsCurrentOidcClientRequiresEmailOtp(clientId) {
336
+ if (typeof clientId !== "string")
337
+ return false;
338
+ return SUITE_ACCOUNTS_CURRENT_EMAIL_OTP_REQUIRED_OIDC_CONSUMER_IDS.some((consumer) => suiteAccountsCurrentOidcClientRegistration(consumer, "production")?.clientId === clientId);
339
+ }
340
+ var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
341
+ function suiteAccountsCurrentDeviceClientRegistration(deviceClient, environment) {
342
+ if (!isSuiteAccountsCurrentDeviceClientId(deviceClient))
343
+ return null;
344
+ const registration = SUITE_ACCOUNTS_CURRENT_DEVICE_CLIENTS[deviceClient];
345
+ if (!registration.environments.includes(environment) || suiteAccountsCurrentOidcClientRegistration(registration.consumer, environment) === null) {
346
+ return null;
347
+ }
348
+ return deepFreeze({
349
+ clientId: `hraness:${deviceClient}:${environment}:v1`,
350
+ consumer: registration.consumer,
351
+ grantTypes: [DEVICE_CODE_GRANT_TYPE],
352
+ redirectUris: [],
353
+ scopes: ["openid", "email", "profile"],
354
+ tokenEndpointAuthMethod: "none"
355
+ });
356
+ }
357
+ function suiteAccountsCurrentDeviceCodeClientAllowed(clientId) {
358
+ if (typeof clientId !== "string")
359
+ return false;
360
+ return SUITE_ACCOUNTS_CURRENT_DEVICE_CLIENT_IDS.some((deviceClient) => suiteAccountsCurrentDeviceClientRegistration(deviceClient, "production")?.clientId === clientId) || SUITE_ACCOUNTS_CURRENT_BROWSER_DEVICE_CODE_CONSUMER_IDS.some((consumer) => isSuiteAccountsCurrentBrowserDeviceCodeConsumerId(consumer) && suiteAccountsCurrentOidcClientRegistration(consumer, "production")?.clientId === clientId);
361
+ }
362
+ function suiteAccountsOidcProviderConfiguration(environment) {
363
+ const issuer = getSuiteAccountsDeployment(environment).accountsOrigin;
364
+ const authBase = new URL("/api/auth/", issuer);
365
+ return deepFreeze({
366
+ authorizationEndpoint: new URL("oauth2/authorize", authBase).href,
367
+ deviceAuthorizationEndpoint: new URL("oauth2/device_authorization", authBase).href,
368
+ deviceTokenEndpoint: new URL("oauth2/device/token", authBase).href,
369
+ discoveryEndpoint: new URL("/.well-known/openid-configuration", issuer).href,
370
+ entitlementReceiptEndpoint: new URL("/suite/entitlements/receipt", issuer).href,
371
+ identityLinkReceiptEndpoint: new URL("/suite/identity-links/receipt", issuer).href,
372
+ issuer,
373
+ jwksEndpoint: new URL("jwks", authBase).href,
374
+ resource: SUITE_ACCOUNTS_OAUTH_RESOURCE,
375
+ revocationEndpoint: new URL("oauth2/revoke", authBase).href,
376
+ tokenEndpoint: new URL("oauth2/token", authBase).href,
377
+ userInfoAudience: new URL("oauth2/userinfo", authBase).href
378
+ });
379
+ }
380
+
381
+ // src/oidc-session-policy.ts
382
+ var SUITE_OIDC_EARLY_REFRESH_WINDOW_MS = 30000;
383
+
384
+ // src/identity/identifiers.ts
385
+ import { err as err2, ok as ok2 } from "@hraness/result";
386
+ var suiteAccountIdPattern = /^acct_[0-9a-f]{32}$/u;
387
+ var suiteInvoiceRefPattern = /^invref_[0-9a-f]{32}$/u;
388
+ var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
389
+ function parseSuiteAccountId(value) {
390
+ return typeof value === "string" && suiteAccountIdPattern.test(value) ? ok2(value) : err2("invalid-suite-account-id");
391
+ }
392
+ function parseSuiteInvoiceRef(value) {
393
+ return typeof value === "string" && suiteInvoiceRefPattern.test(value) ? ok2(value) : err2("invalid-suite-invoice-ref");
394
+ }
395
+ function generateSuiteAccountId(randomUuid = () => crypto.randomUUID()) {
396
+ const uuid = randomUuid();
397
+ if (!uuidPattern.test(uuid)) {
398
+ throw new TypeError("The suite account ID source did not return a UUID.");
399
+ }
400
+ const candidate = `acct_${uuid.replaceAll("-", "").toLowerCase()}`;
401
+ const parsed = parseSuiteAccountId(candidate);
402
+ if (!parsed.ok) {
403
+ throw new TypeError("The suite account ID source produced an invalid value.");
404
+ }
405
+ return parsed.value;
406
+ }
407
+ function generateSuiteInvoiceRef(randomUuid = () => crypto.randomUUID()) {
408
+ const uuid = randomUuid();
409
+ if (!uuidPattern.test(uuid)) {
410
+ throw new TypeError("The suite invoice reference source did not return a UUID.");
411
+ }
412
+ const candidate = `invref_${uuid.replaceAll("-", "").toLowerCase()}`;
413
+ const parsed = parseSuiteInvoiceRef(candidate);
414
+ if (!parsed.ok) {
415
+ throw new TypeError("The suite invoice reference source produced an invalid value.");
416
+ }
417
+ return parsed.value;
418
+ }
419
+
420
+ // src/identity/usernames.ts
421
+ import { err as err3, ok as ok3 } from "@hraness/result";
422
+ var SUITE_USERNAME_MIN_LENGTH = 3;
423
+ var SUITE_USERNAME_MAX_LENGTH = 24;
424
+ var suiteUsernamePattern = /^[a-z0-9](?:[a-z0-9]|[-_](?=[a-z0-9]))*[a-z0-9]$/u;
425
+ var reservedSuiteUsernames = new Set([
426
+ "account",
427
+ "accounts",
428
+ "admin",
429
+ "api",
430
+ "auth",
431
+ "billing",
432
+ "design",
433
+ "docs",
434
+ "help",
435
+ "hraness",
436
+ "login",
437
+ "logout",
438
+ "new",
439
+ "newsletter",
440
+ "party",
441
+ "place",
442
+ "preview",
443
+ "pub",
444
+ "root",
445
+ "settings",
446
+ "social-image",
447
+ "source",
448
+ "sources",
449
+ "support",
450
+ "system",
451
+ "user",
452
+ "users",
453
+ "www"
454
+ ]);
455
+ function validateCanonicalSuiteUsername(value) {
456
+ if (value.length < SUITE_USERNAME_MIN_LENGTH) {
457
+ return err3("suite-username-too-short");
458
+ }
459
+ if (value.length > SUITE_USERNAME_MAX_LENGTH) {
460
+ return err3("suite-username-too-long");
461
+ }
462
+ if (!suiteUsernamePattern.test(value)) {
463
+ return err3("invalid-suite-username");
464
+ }
465
+ return ok3(value);
466
+ }
467
+ function normalizeSuiteUsername(value) {
468
+ if (typeof value !== "string")
469
+ return err3("invalid-suite-username");
470
+ for (let index = 0;index < value.length; index += 1) {
471
+ const code = value.charCodeAt(index);
472
+ if (code > 127 || code < 32 || code === 127) {
473
+ return err3("invalid-suite-username");
474
+ }
475
+ }
476
+ const trimmed = value.trim();
477
+ const parsed = validateCanonicalSuiteUsername(trimmed.toLowerCase());
478
+ if (!parsed.ok)
479
+ return parsed;
480
+ return reservedSuiteUsernames.has(parsed.value) ? err3("suite-username-reserved") : parsed;
481
+ }
482
+ function parseSuiteUsername(value) {
483
+ if (typeof value !== "string")
484
+ return err3("invalid-suite-username");
485
+ const parsed = validateCanonicalSuiteUsername(value);
486
+ return parsed.ok && parsed.value === value ? parsed : err3(parsed.ok ? "invalid-suite-username" : parsed.error);
487
+ }
488
+
489
+ // src/convex-browser-auth.ts
490
+ var SUITE_CONVEX_BROWSER_TOKEN_USE = "suite-convex-browser-v1";
491
+ var SUITE_CONVEX_BROWSER_TOKEN_TTL_MS = 5 * 60000;
492
+ var SUITE_CONVEX_BROWSER_PARENT_REFRESH_WINDOW_MS = SUITE_OIDC_EARLY_REFRESH_WINDOW_MS;
493
+ var SUITE_CONVEX_BROWSER_TOKEN_PATH = "/api/convex-auth/token";
494
+ var SUITE_CONVEX_BROWSER_JWKS_PATH = "/api/convex-auth/jwks";
495
+ var SUITE_CONVEX_BROWSER_ISSUER_PATH = "/api/convex-auth";
496
+ var SUITE_CONVEX_BROWSER_AUDIENCE_PATH = "/convex";
497
+ var SUITE_CONVEX_BROWSER_CONSUMER_IDS = deepFreeze([
498
+ "alt"
499
+ ]);
500
+ function isRecord(value) {
501
+ return typeof value === "object" && value !== null && !Array.isArray(value);
502
+ }
503
+ function enabledConsumer(value) {
504
+ return typeof value === "string" && SUITE_CONVEX_BROWSER_CONSUMER_IDS.includes(value);
505
+ }
506
+ function suiteConvexBrowserConfiguration(consumer, environment) {
507
+ if (!enabledConsumer(consumer)) {
508
+ throw new Error("The suite consumer has no Convex browser-token grant.");
509
+ }
510
+ const consumerEnvironment = getSuiteAccountsCurrentConsumerEnvironment(consumer, environment);
511
+ if (consumerEnvironment === null) {
512
+ throw new Error("The suite consumer is unavailable in this environment.");
513
+ }
514
+ const siteUrl = consumerEnvironment.siteUrl;
515
+ const registration = suiteAccountsCurrentOidcClientRegistration(consumer, environment);
516
+ if (registration === null) {
517
+ throw new Error("The suite consumer has no OIDC client registration.");
518
+ }
519
+ const suiteProvider = suiteAccountsOidcProviderConfiguration(environment);
520
+ return deepFreeze({
521
+ audience: new URL(SUITE_CONVEX_BROWSER_AUDIENCE_PATH, siteUrl).href,
522
+ clientId: registration.clientId,
523
+ consumer,
524
+ environment,
525
+ issuer: new URL(SUITE_CONVEX_BROWSER_ISSUER_PATH, siteUrl).href,
526
+ jwksEndpoint: new URL(SUITE_CONVEX_BROWSER_JWKS_PATH, siteUrl).href,
527
+ siteUrl,
528
+ suiteIssuer: suiteProvider.issuer,
529
+ tokenEndpoint: new URL(SUITE_CONVEX_BROWSER_TOKEN_PATH, siteUrl).href
530
+ });
531
+ }
532
+ function suiteConvexBrowserEnvironmentForOrigin(consumer, value) {
533
+ if (typeof value !== "string" || !enabledConsumer(consumer))
534
+ return null;
535
+ return getSuiteAccountsCurrentConsumerEnvironment(consumer, "production")?.siteUrl === value ? "production" : null;
536
+ }
537
+ function suiteConvexBrowserAuthConfig(consumer, environment) {
538
+ const configuration = suiteConvexBrowserConfiguration(consumer, environment);
539
+ return deepFreeze({
540
+ providers: [
541
+ {
542
+ algorithm: "ES256",
543
+ applicationID: configuration.audience,
544
+ issuer: configuration.issuer,
545
+ jwks: configuration.jwksEndpoint,
546
+ type: "customJwt"
547
+ }
548
+ ]
549
+ });
550
+ }
551
+ function parseSuiteConvexBrowserIdentity(value, configuration) {
552
+ if (!isRecord(value))
553
+ return { error: "invalid-identity", ok: false };
554
+ if (value["issuer"] !== configuration.issuer) {
555
+ return { error: "invalid-issuer", ok: false };
556
+ }
557
+ if (value["suite_client_id"] !== configuration.clientId || value["suite_issuer"] !== configuration.suiteIssuer) {
558
+ return { error: "invalid-client", ok: false };
559
+ }
560
+ if (value["token_use"] !== SUITE_CONVEX_BROWSER_TOKEN_USE) {
561
+ return { error: "invalid-token-use", ok: false };
562
+ }
563
+ if (value["profile_complete"] !== true || value["profile_revision"] !== "username-v1") {
564
+ return { error: "invalid-profile", ok: false };
565
+ }
566
+ const account = parseSuiteAccountId(value["suite_account_id"]);
567
+ const subject = parseSuiteAccountId(value["subject"]);
568
+ if (!account.ok || !subject.ok || account.value !== subject.value) {
569
+ return { error: "invalid-subject", ok: false };
570
+ }
571
+ const username = parseSuiteUsername(value["username"]);
572
+ if (!username.ok)
573
+ return { error: "invalid-profile", ok: false };
574
+ return deepFreeze({
575
+ ok: true,
576
+ value: {
577
+ issuer: configuration.issuer,
578
+ subject: subject.value,
579
+ suiteAccountId: account.value,
580
+ username: username.value
581
+ }
582
+ });
583
+ }
584
+
585
+ // src/convex-browser-auth-server.ts
586
+ import {
587
+ createECDH,
588
+ createPrivateKey,
589
+ createPublicKey
590
+ } from "node:crypto";
591
+ import { SignJWT } from "jose";
592
+ var KEYRING_VERSION = "suite-convex-browser-keyring-v1";
593
+ var TOKEN_RESPONSE_VERSION = "suite-convex-browser-token-response-v1";
594
+ var MAXIMUM_KEY_COUNT = 4;
595
+ var activePrivateKeys = new WeakMap;
596
+ var CONFIGURATION_KEYS = deepFreeze([
597
+ "audience",
598
+ "clientId",
599
+ "consumer",
600
+ "environment",
601
+ "issuer",
602
+ "jwksEndpoint",
603
+ "siteUrl",
604
+ "suiteIssuer",
605
+ "tokenEndpoint"
606
+ ]);
607
+ function authoritativeConfiguration(configuration) {
608
+ const expected = suiteConvexBrowserConfiguration(configuration.consumer, configuration.environment);
609
+ if (Object.keys(configuration).length !== CONFIGURATION_KEYS.length || CONFIGURATION_KEYS.some((key) => configuration[key] !== expected[key])) {
610
+ throw new Error("The Convex browser configuration does not match the registry.");
611
+ }
612
+ return expected;
613
+ }
614
+ function isRecord2(value) {
615
+ return typeof value === "object" && value !== null && !Array.isArray(value);
616
+ }
617
+ function exactKeys(value, required, optional = []) {
618
+ const keys = Object.keys(value);
619
+ const accepted = new Set([...required, ...optional]);
620
+ return required.every((key) => Object.hasOwn(value, key)) && keys.every((key) => accepted.has(key));
621
+ }
622
+ function canonicalCoordinate(value) {
623
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]{43}$/u.test(value)) {
624
+ return false;
625
+ }
626
+ try {
627
+ const bytes = Buffer.from(value, "base64url");
628
+ return bytes.byteLength === 32 && bytes.toString("base64url") === value;
629
+ } catch {
630
+ return false;
631
+ }
632
+ }
633
+ function canonicalKid(value) {
634
+ return typeof value === "string" && /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,63})$/u.test(value);
635
+ }
636
+ function publicJwk(value) {
637
+ if (!exactKeys(value, ["alg", "crv", "kid", "kty", "use", "x", "y"], ["d"]) || value["alg"] !== "ES256" || value["crv"] !== "P-256" || !canonicalKid(value["kid"]) || value["kty"] !== "EC" || value["use"] !== "sig" || !canonicalCoordinate(value["x"]) || !canonicalCoordinate(value["y"]) || value["d"] !== undefined && !canonicalCoordinate(value["d"])) {
638
+ return null;
639
+ }
640
+ return {
641
+ alg: "ES256",
642
+ crv: "P-256",
643
+ kid: value["kid"],
644
+ kty: "EC",
645
+ use: "sig",
646
+ x: value["x"],
647
+ y: value["y"]
648
+ };
649
+ }
650
+ function nodeJwk(key, privateCoordinate) {
651
+ return {
652
+ alg: key.alg,
653
+ crv: key.crv,
654
+ ...privateCoordinate === undefined ? {} : { d: privateCoordinate },
655
+ key_ops: privateCoordinate === undefined ? ["verify"] : ["sign"],
656
+ kid: key.kid,
657
+ kty: key.kty,
658
+ use: key.use,
659
+ x: key.x,
660
+ y: key.y
661
+ };
662
+ }
663
+ function validPublicKey(key) {
664
+ try {
665
+ const imported = createPublicKey({ format: "jwk", key: nodeJwk(key) });
666
+ return imported.asymmetricKeyType === "ec" && imported.asymmetricKeyDetails?.namedCurve === "prime256v1";
667
+ } catch {
668
+ return false;
669
+ }
670
+ }
671
+ function activePrivateKey(key, privateCoordinate) {
672
+ try {
673
+ const ecdh = createECDH("prime256v1");
674
+ ecdh.setPrivateKey(Buffer.from(privateCoordinate, "base64url"));
675
+ const derivedPoint = ecdh.getPublicKey(undefined, "uncompressed");
676
+ if (derivedPoint.byteLength !== 65 || derivedPoint[0] !== 4 || derivedPoint.subarray(1, 33).toString("base64url") !== key.x || derivedPoint.subarray(33, 65).toString("base64url") !== key.y) {
677
+ return null;
678
+ }
679
+ const imported = createPrivateKey({
680
+ format: "jwk",
681
+ key: nodeJwk(key, privateCoordinate)
682
+ });
683
+ if (imported.asymmetricKeyType !== "ec" || imported.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
684
+ return null;
685
+ }
686
+ const derived = createPublicKey(imported).export({ format: "jwk" });
687
+ return derived.kty === "EC" && derived.crv === "P-256" && derived.x === key.x && derived.y === key.y ? imported : null;
688
+ } catch {
689
+ return null;
690
+ }
691
+ }
692
+ function parseSuiteConvexBrowserKeyring(value, configuration) {
693
+ const authoritative = authoritativeConfiguration(configuration);
694
+ if (!isRecord2(value) || !exactKeys(value, ["activeKid", "consumer", "environment", "keys", "version"]) || value["version"] !== KEYRING_VERSION || value["consumer"] !== authoritative.consumer || value["environment"] !== authoritative.environment || !canonicalKid(value["activeKid"]) || !Array.isArray(value["keys"]) || value["keys"].length < 1 || value["keys"].length > MAXIMUM_KEY_COUNT) {
695
+ return null;
696
+ }
697
+ const seen = new Set;
698
+ const parsed = [];
699
+ for (const candidate of value["keys"]) {
700
+ if (!isRecord2(candidate))
701
+ return null;
702
+ const publicKey = publicJwk(candidate);
703
+ if (publicKey === null || seen.has(publicKey.kid) || !validPublicKey(publicKey)) {
704
+ return null;
705
+ }
706
+ seen.add(publicKey.kid);
707
+ parsed.push({
708
+ privateCoordinate: typeof candidate["d"] === "string" ? candidate["d"] : null,
709
+ publicKey
710
+ });
711
+ }
712
+ const active = parsed.find((key) => key.publicKey.kid === value["activeKid"]);
713
+ if (active?.privateCoordinate === null || active === undefined)
714
+ return null;
715
+ const privateKey = activePrivateKey(active.publicKey, active.privateCoordinate);
716
+ if (privateKey === null)
717
+ return null;
718
+ const ordered = parsed.map((key) => key.publicKey).sort((left, right) => {
719
+ if (left.kid === value["activeKid"])
720
+ return -1;
721
+ if (right.kid === value["activeKid"])
722
+ return 1;
723
+ return left.kid.localeCompare(right.kid);
724
+ });
725
+ const keyring = deepFreeze({
726
+ activeKid: value["activeKid"],
727
+ consumer: authoritative.consumer,
728
+ environment: authoritative.environment,
729
+ jwks: { keys: ordered },
730
+ version: KEYRING_VERSION
731
+ });
732
+ activePrivateKeys.set(keyring, privateKey);
733
+ return keyring;
734
+ }
735
+ function safeTimestamp(value) {
736
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
737
+ }
738
+ function createSuiteConvexBrowserTokenSigner(configuration, keyring, now = Date.now) {
739
+ const authoritative = authoritativeConfiguration(configuration);
740
+ if (keyring.consumer !== authoritative.consumer || keyring.environment !== authoritative.environment) {
741
+ throw new Error("The Convex browser keyring environment does not match.");
742
+ }
743
+ const privateKey = activePrivateKeys.get(keyring);
744
+ if (privateKey === undefined) {
745
+ throw new Error("The Convex browser keyring was not parsed by this module.");
746
+ }
747
+ return Object.freeze({
748
+ async sign(session) {
749
+ const nowMs = now();
750
+ const suiteAccountId = parseSuiteAccountId(session.suiteAccountId);
751
+ const username = parseSuiteUsername(session.username);
752
+ if (!safeTimestamp(nowMs) || !safeTimestamp(session.accessTokenExpiresAtMs) || !suiteAccountId.ok || !username.ok) {
753
+ throw new Error("The parent suite session is invalid.");
754
+ }
755
+ if (session.accessTokenExpiresAtMs - nowMs < SUITE_CONVEX_BROWSER_PARENT_REFRESH_WINDOW_MS) {
756
+ return { kind: "refresh_required" };
757
+ }
758
+ const issuedAtSeconds = Math.floor(nowMs / 1000);
759
+ const expiresAtSeconds = Math.min(issuedAtSeconds + Math.floor(SUITE_CONVEX_BROWSER_TOKEN_TTL_MS / 1000), Math.floor(session.accessTokenExpiresAtMs / 1000));
760
+ if (expiresAtSeconds <= issuedAtSeconds) {
761
+ return { kind: "refresh_required" };
762
+ }
763
+ const token = await new SignJWT({
764
+ profile_complete: true,
765
+ profile_revision: "username-v1",
766
+ suite_account_id: suiteAccountId.value,
767
+ suite_client_id: authoritative.clientId,
768
+ suite_issuer: authoritative.suiteIssuer,
769
+ token_use: SUITE_CONVEX_BROWSER_TOKEN_USE,
770
+ username: username.value
771
+ }).setProtectedHeader({ alg: "ES256", kid: keyring.activeKid, typ: "JWT" }).setIssuer(authoritative.issuer).setAudience(authoritative.audience).setSubject(suiteAccountId.value).setIssuedAt(issuedAtSeconds).setNotBefore(issuedAtSeconds).setExpirationTime(expiresAtSeconds).sign(privateKey);
772
+ return {
773
+ expiresAtMs: expiresAtSeconds * 1000,
774
+ kind: "token",
775
+ token,
776
+ version: TOKEN_RESPONSE_VERSION
777
+ };
778
+ }
779
+ });
780
+ }
781
+ function noStoreJson(value, status = 200) {
782
+ return Response.json(value, {
783
+ headers: {
784
+ "cache-control": "no-store",
785
+ "content-security-policy": "default-src 'none'",
786
+ pragma: "no-cache",
787
+ "x-content-type-options": "nosniff"
788
+ },
789
+ status
790
+ });
791
+ }
792
+ function exactEndpointRequest(request, endpoint, method) {
793
+ let incoming;
794
+ let expected;
795
+ try {
796
+ incoming = new URL(request.url);
797
+ expected = new URL(endpoint);
798
+ } catch {
799
+ return false;
800
+ }
801
+ return request.method === method && incoming.origin === expected.origin && incoming.pathname === expected.pathname && incoming.search === "" && incoming.username === "" && incoming.password === "" && request.body === null;
802
+ }
803
+ function exactTokenRequest(request, configuration) {
804
+ if (!exactEndpointRequest(request, configuration.tokenEndpoint, "POST")) {
805
+ return false;
806
+ }
807
+ const contentLength = request.headers.get("content-length");
808
+ const fetchDestination = request.headers.get("sec-fetch-dest");
809
+ const fetchMode = request.headers.get("sec-fetch-mode");
810
+ const fetchSite = request.headers.get("sec-fetch-site");
811
+ return request.headers.get("origin") === configuration.siteUrl && (contentLength === null || contentLength === "0") && request.headers.get("content-type") === null && (fetchDestination === null || fetchDestination === "empty") && (fetchMode === null || fetchMode === "cors" || fetchMode === "same-origin") && (fetchSite === null || fetchSite === "same-origin") && request.headers.get("sec-fetch-user") === null;
812
+ }
813
+ function createSuiteConvexBrowserAuthHandlers(options) {
814
+ const configuration = authoritativeConfiguration(options.configuration);
815
+ const keyring = options.keyring;
816
+ const serverSession = options.serverSession;
817
+ const signer = createSuiteConvexBrowserTokenSigner(configuration, keyring, options.now);
818
+ return Object.freeze({
819
+ jwks: (request) => Promise.resolve(exactEndpointRequest(request, configuration.jwksEndpoint, "GET") ? noStoreJson(keyring.jwks) : noStoreJson({ kind: "request_rejected" }, 403)),
820
+ token: async (request) => {
821
+ if (!exactTokenRequest(request, configuration)) {
822
+ return noStoreJson({ kind: "request_rejected" }, 403);
823
+ }
824
+ const session = await serverSession(request);
825
+ if (session === null) {
826
+ return noStoreJson({ kind: "signed_out" }, 401);
827
+ }
828
+ let result;
829
+ try {
830
+ result = await signer.sign({
831
+ accessTokenExpiresAtMs: session.accessTokenExpiresAtMs,
832
+ suiteAccountId: session.suiteAccountId,
833
+ username: session.username
834
+ });
835
+ } catch {
836
+ return noStoreJson({ kind: "signed_out" }, 401);
837
+ }
838
+ return result.kind === "refresh_required" ? noStoreJson(result, 409) : noStoreJson(result);
839
+ }
840
+ });
841
+ }
842
+ export {
843
+ parseSuiteConvexBrowserKeyring,
844
+ createSuiteConvexBrowserTokenSigner,
845
+ createSuiteConvexBrowserAuthHandlers
846
+ };