@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,502 @@
1
+ // src/identity/catalog.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/catalog.ts
26
+ var SUITE_CATALOG_REVISION = "hraness-suite-v4";
27
+ var PREVIOUS_SUITE_CATALOG_REVISION = "cclrte-suite-v3";
28
+ var LEGACY_SUITE_CATALOG_REVISION = "cclrte-suite-v2";
29
+ var ARCHIVED_SUITE_CATALOG_REVISION = "cclrte-suite-v1";
30
+ var SUITE_CATALOG_REVISIONS = deepFreeze([
31
+ ARCHIVED_SUITE_CATALOG_REVISION,
32
+ LEGACY_SUITE_CATALOG_REVISION,
33
+ PREVIOUS_SUITE_CATALOG_REVISION,
34
+ SUITE_CATALOG_REVISION
35
+ ]);
36
+ var SUITE_PLAN_IDS = deepFreeze(["community", "pro"]);
37
+ var SUITE_HISTORICAL_PLAN_IDS = deepFreeze([
38
+ "individual",
39
+ "business"
40
+ ]);
41
+ var SUITE_CURRENT_FEATURE_IDS = deepFreeze([
42
+ "suite.paid",
43
+ "suite.community",
44
+ "suite.pro"
45
+ ]);
46
+ var SUITE_PREVIOUS_FEATURE_IDS = deepFreeze([
47
+ "suite.paid",
48
+ "suite.believer"
49
+ ]);
50
+ var SUITE_LEGACY_FEATURE_IDS = deepFreeze([
51
+ "suite.paid",
52
+ "suite.business"
53
+ ]);
54
+ var SUITE_FEATURE_IDS = deepFreeze([
55
+ "suite.paid",
56
+ "suite.community",
57
+ "suite.pro",
58
+ "suite.believer",
59
+ "suite.business"
60
+ ]);
61
+ var CURRENT_PLAN_FEATURES = deepFreeze({
62
+ community: ["suite.paid", "suite.community"],
63
+ pro: ["suite.paid", "suite.community", "suite.pro"]
64
+ });
65
+ var PREVIOUS_PLAN_FEATURES = deepFreeze({
66
+ business: ["suite.paid", "suite.believer"],
67
+ individual: ["suite.paid"]
68
+ });
69
+ var LEGACY_PLAN_FEATURES = deepFreeze({
70
+ business: ["suite.paid", "suite.business"],
71
+ individual: ["suite.paid"]
72
+ });
73
+ function parseSuitePlanId(value, revision = SUITE_CATALOG_REVISION) {
74
+ const ids = revision === SUITE_CATALOG_REVISION ? SUITE_PLAN_IDS : SUITE_HISTORICAL_PLAN_IDS;
75
+ return typeof value === "string" && ids.includes(value) ? ok(value) : err("invalid-plan");
76
+ }
77
+ function parseAnySuitePlanId(value) {
78
+ return typeof value === "string" && [...SUITE_PLAN_IDS, ...SUITE_HISTORICAL_PLAN_IDS].includes(value) ? ok(value) : err("invalid-plan");
79
+ }
80
+ function parseCurrentSuiteFeatureId(value) {
81
+ return value === "suite.paid" || value === "suite.community" || value === "suite.pro" ? ok(value) : err("invalid-feature");
82
+ }
83
+ function parseSuiteFeatureId(value) {
84
+ return typeof value === "string" && SUITE_FEATURE_IDS.includes(value) ? ok(value) : err("invalid-feature");
85
+ }
86
+ function parseSuiteCatalogRevision(value) {
87
+ return typeof value === "string" && SUITE_CATALOG_REVISIONS.includes(value) ? ok(value) : err("invalid-catalog-revision");
88
+ }
89
+ function featuresForSuitePlan(plan, revision = SUITE_CATALOG_REVISION) {
90
+ const table = revision === SUITE_CATALOG_REVISION ? CURRENT_PLAN_FEATURES : revision === ARCHIVED_SUITE_CATALOG_REVISION ? LEGACY_PLAN_FEATURES : PREVIOUS_PLAN_FEATURES;
91
+ const features = table[plan];
92
+ return features === undefined ? [] : [...features];
93
+ }
94
+ function grantedSuiteFeatureSets(revision) {
95
+ if (revision === SUITE_CATALOG_REVISION) {
96
+ return deepFreeze([
97
+ [],
98
+ CURRENT_PLAN_FEATURES.community,
99
+ CURRENT_PLAN_FEATURES.pro
100
+ ]);
101
+ }
102
+ const table = revision === ARCHIVED_SUITE_CATALOG_REVISION ? LEGACY_PLAN_FEATURES : PREVIOUS_PLAN_FEATURES;
103
+ return deepFreeze([
104
+ [],
105
+ table.individual,
106
+ table.business
107
+ ]);
108
+ }
109
+ function suitePlanIncludesFeature(plan, feature) {
110
+ return plan !== null && featuresForSuitePlan(plan).includes(feature);
111
+ }
112
+
113
+ // src/identity/principals.ts
114
+ import { err as err4, isRecord, ok as ok4 } from "@hraness/result";
115
+
116
+ // src/identity/identifiers.ts
117
+ import { err as err2, ok as ok2 } from "@hraness/result";
118
+ var suiteAccountIdPattern = /^acct_[0-9a-f]{32}$/u;
119
+ var suiteInvoiceRefPattern = /^invref_[0-9a-f]{32}$/u;
120
+ 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;
121
+ function parseSuiteAccountId(value) {
122
+ return typeof value === "string" && suiteAccountIdPattern.test(value) ? ok2(value) : err2("invalid-suite-account-id");
123
+ }
124
+ function parseSuiteInvoiceRef(value) {
125
+ return typeof value === "string" && suiteInvoiceRefPattern.test(value) ? ok2(value) : err2("invalid-suite-invoice-ref");
126
+ }
127
+ function generateSuiteAccountId(randomUuid = () => crypto.randomUUID()) {
128
+ const uuid = randomUuid();
129
+ if (!uuidPattern.test(uuid)) {
130
+ throw new TypeError("The suite account ID source did not return a UUID.");
131
+ }
132
+ const candidate = `acct_${uuid.replaceAll("-", "").toLowerCase()}`;
133
+ const parsed = parseSuiteAccountId(candidate);
134
+ if (!parsed.ok) {
135
+ throw new TypeError("The suite account ID source produced an invalid value.");
136
+ }
137
+ return parsed.value;
138
+ }
139
+ function generateSuiteInvoiceRef(randomUuid = () => crypto.randomUUID()) {
140
+ const uuid = randomUuid();
141
+ if (!uuidPattern.test(uuid)) {
142
+ throw new TypeError("The suite invoice reference source did not return a UUID.");
143
+ }
144
+ const candidate = `invref_${uuid.replaceAll("-", "").toLowerCase()}`;
145
+ const parsed = parseSuiteInvoiceRef(candidate);
146
+ if (!parsed.ok) {
147
+ throw new TypeError("The suite invoice reference source produced an invalid value.");
148
+ }
149
+ return parsed.value;
150
+ }
151
+
152
+ // src/identity/usernames.ts
153
+ import { err as err3, ok as ok3 } from "@hraness/result";
154
+ var SUITE_USERNAME_MIN_LENGTH = 3;
155
+ var SUITE_USERNAME_MAX_LENGTH = 24;
156
+ var suiteUsernamePattern = /^[a-z0-9](?:[a-z0-9]|[-_](?=[a-z0-9]))*[a-z0-9]$/u;
157
+ var reservedSuiteUsernames = new Set([
158
+ "account",
159
+ "accounts",
160
+ "admin",
161
+ "api",
162
+ "auth",
163
+ "billing",
164
+ "design",
165
+ "docs",
166
+ "help",
167
+ "hraness",
168
+ "login",
169
+ "logout",
170
+ "new",
171
+ "newsletter",
172
+ "party",
173
+ "place",
174
+ "preview",
175
+ "pub",
176
+ "root",
177
+ "settings",
178
+ "social-image",
179
+ "source",
180
+ "sources",
181
+ "support",
182
+ "system",
183
+ "user",
184
+ "users",
185
+ "www"
186
+ ]);
187
+ function validateCanonicalSuiteUsername(value) {
188
+ if (value.length < SUITE_USERNAME_MIN_LENGTH) {
189
+ return err3("suite-username-too-short");
190
+ }
191
+ if (value.length > SUITE_USERNAME_MAX_LENGTH) {
192
+ return err3("suite-username-too-long");
193
+ }
194
+ if (!suiteUsernamePattern.test(value)) {
195
+ return err3("invalid-suite-username");
196
+ }
197
+ return ok3(value);
198
+ }
199
+ function normalizeSuiteUsername(value) {
200
+ if (typeof value !== "string")
201
+ return err3("invalid-suite-username");
202
+ for (let index = 0;index < value.length; index += 1) {
203
+ const code = value.charCodeAt(index);
204
+ if (code > 127 || code < 32 || code === 127) {
205
+ return err3("invalid-suite-username");
206
+ }
207
+ }
208
+ const trimmed = value.trim();
209
+ const parsed = validateCanonicalSuiteUsername(trimmed.toLowerCase());
210
+ if (!parsed.ok)
211
+ return parsed;
212
+ return reservedSuiteUsernames.has(parsed.value) ? err3("suite-username-reserved") : parsed;
213
+ }
214
+ function parseSuiteUsername(value) {
215
+ if (typeof value !== "string")
216
+ return err3("invalid-suite-username");
217
+ const parsed = validateCanonicalSuiteUsername(value);
218
+ return parsed.ok && parsed.value === value ? parsed : err3(parsed.ok ? "invalid-suite-username" : parsed.error);
219
+ }
220
+
221
+ // src/identity/principals.ts
222
+ var SUITE_PRODUCTS = deepFreeze([
223
+ "soundfish",
224
+ "hra",
225
+ "peopleblade",
226
+ "ghostget",
227
+ "crclte",
228
+ "pub"
229
+ ]);
230
+ var LEGACY_SUITE_PRODUCT_IDS = deepFreeze([
231
+ "oprte",
232
+ "kitchen"
233
+ ]);
234
+ var SUITE_ENVIRONMENTS = deepFreeze([
235
+ "development",
236
+ "staging",
237
+ "production"
238
+ ]);
239
+ var SUITE_ISSUABLE_ENVIRONMENTS = deepFreeze([
240
+ "development",
241
+ "production"
242
+ ]);
243
+ var localIssuerHosts = new Set(["127.0.0.1", "[::1]", "localhost"]);
244
+ function containsAsciiControl(value) {
245
+ for (let index = 0;index < value.length; index += 1) {
246
+ const code = value.charCodeAt(index);
247
+ if (code <= 31 || code === 127)
248
+ return true;
249
+ }
250
+ return false;
251
+ }
252
+ function parseIdentityIssuer(value) {
253
+ if (typeof value !== "string" || value.length > 2048 || value.trim() !== value) {
254
+ return err4("invalid-issuer");
255
+ }
256
+ let parsed;
257
+ try {
258
+ parsed = new URL(value);
259
+ } catch {
260
+ return err4("invalid-issuer");
261
+ }
262
+ const local = localIssuerHosts.has(parsed.hostname);
263
+ if (parsed.protocol !== "https:" && !(local && parsed.protocol === "http:") || parsed.username !== "" || parsed.password !== "" || parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "") {
264
+ return err4("invalid-issuer");
265
+ }
266
+ return ok4(parsed.origin);
267
+ }
268
+ function parseIdentitySubject(value) {
269
+ if (typeof value !== "string" || value.length < 1 || value.length > 255 || value.trim() !== value || containsAsciiControl(value)) {
270
+ return err4("invalid-subject");
271
+ }
272
+ return ok4(value);
273
+ }
274
+ function parseIssuerSubject(value) {
275
+ if (!isRecord(value))
276
+ return err4("invalid-subject");
277
+ const issuer = parseIdentityIssuer(value["issuer"]);
278
+ if (!issuer.ok)
279
+ return issuer;
280
+ const subject = parseIdentitySubject(value["subject"]);
281
+ return subject.ok ? ok4({ issuer: issuer.value, subject: subject.value }) : subject;
282
+ }
283
+ function parseSuiteProduct(value) {
284
+ switch (value) {
285
+ case "soundfish":
286
+ case "hra":
287
+ case "peopleblade":
288
+ case "ghostget":
289
+ case "crclte":
290
+ case "pub":
291
+ return ok4(value);
292
+ case "oprte":
293
+ case "kitchen":
294
+ return ok4("hra");
295
+ default:
296
+ return err4("invalid-product");
297
+ }
298
+ }
299
+ function parseSuiteEnvironment(value) {
300
+ return typeof value === "string" && SUITE_ENVIRONMENTS.includes(value) ? ok4(value) : err4("invalid-environment");
301
+ }
302
+ function isSuiteIssuableEnvironment(value) {
303
+ return value === "development" || value === "production";
304
+ }
305
+ function parseLegacyPrincipalLink(value) {
306
+ if (!isRecord(value))
307
+ return err4("invalid-legacy-link");
308
+ const product = parseSuiteProduct(value["product"]);
309
+ if (!product.ok)
310
+ return product;
311
+ const environment = parseSuiteEnvironment(value["environment"]);
312
+ if (!environment.ok)
313
+ return environment;
314
+ const legacySubject = parseIdentitySubject(value["legacySubject"]);
315
+ if (!legacySubject.ok)
316
+ return legacySubject;
317
+ return ok4({
318
+ environment: environment.value,
319
+ legacySubject: legacySubject.value,
320
+ product: product.value
321
+ });
322
+ }
323
+ function parseAudience(value) {
324
+ const values = typeof value === "string" ? [value] : value;
325
+ if (!Array.isArray(values) || values.length < 1 || values.length > 8 || values.some((entry) => typeof entry !== "string" || entry.length < 1 || entry.length > 255 || entry.trim() !== entry || containsAsciiControl(entry)) || new Set(values).size !== values.length) {
326
+ return err4("invalid-audience");
327
+ }
328
+ return ok4(values);
329
+ }
330
+ function parseTimestamp(value) {
331
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
332
+ }
333
+ function parseSuiteJwtClaims(value) {
334
+ if (!isRecord(value))
335
+ return err4("invalid-jwt-claims");
336
+ const principal = parseIssuerSubject({
337
+ issuer: value["iss"],
338
+ subject: value["sub"]
339
+ });
340
+ if (!principal.ok)
341
+ return principal;
342
+ const suiteAccountId = parseSuiteAccountId(value["suite_account_id"]);
343
+ if (!suiteAccountId.ok)
344
+ return suiteAccountId;
345
+ const audience = parseAudience(value["aud"]);
346
+ if (!audience.ok)
347
+ return audience;
348
+ const issuedAtSeconds = parseTimestamp(value["iat"]);
349
+ const expiresAtSeconds = parseTimestamp(value["exp"]);
350
+ const notBeforeSeconds = value["nbf"] === undefined ? undefined : parseTimestamp(value["nbf"]);
351
+ const legacyProfile = value["profile_revision"] === undefined && value["profile_complete"] === undefined && value["username"] === undefined;
352
+ const profileRevision = legacyProfile ? null : value["profile_revision"] === "username-v1" ? "username-v1" : undefined;
353
+ const profileComplete = legacyProfile ? false : typeof value["profile_complete"] === "boolean" ? value["profile_complete"] : undefined;
354
+ const username = legacyProfile || value["username"] === null ? null : parseSuiteUsername(value["username"]);
355
+ if (issuedAtSeconds === null || expiresAtSeconds === null || expiresAtSeconds <= issuedAtSeconds || notBeforeSeconds === null || notBeforeSeconds !== undefined && notBeforeSeconds > expiresAtSeconds || profileRevision === undefined || profileComplete === undefined || username !== null && !username.ok || profileComplete !== (username !== null)) {
356
+ return err4("invalid-jwt-claims");
357
+ }
358
+ return ok4({
359
+ audience: audience.value,
360
+ expiresAtSeconds,
361
+ issuedAtSeconds,
362
+ ...notBeforeSeconds === undefined ? {} : { notBeforeSeconds },
363
+ principal: principal.value,
364
+ profileComplete,
365
+ profileRevision,
366
+ suiteAccountId: suiteAccountId.value,
367
+ username: username?.value ?? null
368
+ });
369
+ }
370
+
371
+ // src/entitlements.ts
372
+ var SUITE_ENTITLEMENTS_CLAIM_VERSION = "suite-entitlements-v1";
373
+ var SUITE_ENTITLEMENT_MAX_PROVIDER_AGE_MS = 26 * 60 * 60000;
374
+ function isRecord2(value) {
375
+ return typeof value === "object" && value !== null && !Array.isArray(value);
376
+ }
377
+ function nonNegativeInteger(value) {
378
+ return Number.isSafeInteger(value) && typeof value === "number" && value >= 0;
379
+ }
380
+ function canonicalFeatures(revision, features) {
381
+ return grantedSuiteFeatureSets(revision).some((set) => set.length === features.length && set.every((feature, index) => feature === features[index]));
382
+ }
383
+ function validReceiptProjection(projection) {
384
+ const revision = parseSuiteCatalogRevision(projection.catalogRevision);
385
+ return revision.ok && projection.suiteAccountId.length >= 1 && projection.suiteAccountId.length <= 128 && canonicalFeatures(revision.value, projection.features) && nonNegativeInteger(projection.observedAtMs) && nonNegativeInteger(projection.expiresAtMs) && projection.expiresAtMs > projection.observedAtMs && nonNegativeInteger(projection.projectionRevision) && nonNegativeInteger(projection.receiptIssuedAtMs) && projection.receiptIssuedAtMs < projection.expiresAtMs && (projection.receiptDigest === undefined || /^[a-f0-9]{64}$/u.test(projection.receiptDigest));
386
+ }
387
+ function sameFeatures(left, right) {
388
+ return left.length === right.length && left.every((feature, index) => feature === right[index]);
389
+ }
390
+ function decideSuiteEntitlementReceiptProjection(current, incoming) {
391
+ if (!validReceiptProjection(incoming))
392
+ return "conflict";
393
+ if (current === null)
394
+ return "insert";
395
+ if (!validReceiptProjection(current) || current.suiteAccountId !== incoming.suiteAccountId || incoming.projectionRevision < current.projectionRevision || incoming.observedAtMs < current.observedAtMs || incoming.receiptIssuedAtMs < current.receiptIssuedAtMs) {
396
+ return "conflict";
397
+ }
398
+ if (incoming.projectionRevision > current.projectionRevision || incoming.receiptIssuedAtMs > current.receiptIssuedAtMs) {
399
+ return "replace";
400
+ }
401
+ return incoming.catalogRevision === current.catalogRevision && incoming.expiresAtMs === current.expiresAtMs && incoming.observedAtMs === current.observedAtMs && sameFeatures(incoming.features, current.features) && incoming.receiptDigest === current.receiptDigest ? "replay" : "conflict";
402
+ }
403
+ function parseEntitlements(value) {
404
+ if (!isRecord2(value))
405
+ return null;
406
+ const catalogRevision = parseSuiteCatalogRevision(value["catalogRevision"]);
407
+ if (value["version"] !== SUITE_ENTITLEMENTS_CLAIM_VERSION || !catalogRevision.ok || !nonNegativeInteger(value["observedAtMs"]) || !nonNegativeInteger(value["expiresAtMs"]) || !nonNegativeInteger(value["projectionRevision"]) || !Array.isArray(value["features"]) || value["features"].length > SUITE_FEATURE_IDS.length) {
408
+ return null;
409
+ }
410
+ const features = [];
411
+ for (const valueFeature of value["features"]) {
412
+ const feature = parseSuiteFeatureId(valueFeature);
413
+ if (!feature.ok || features.includes(feature.value))
414
+ return null;
415
+ features.push(feature.value);
416
+ }
417
+ if (value["expiresAtMs"] <= value["observedAtMs"] || !canonicalFeatures(catalogRevision.value, features))
418
+ return null;
419
+ return {
420
+ catalogRevision: catalogRevision.value,
421
+ expiresAtMs: value["expiresAtMs"],
422
+ features,
423
+ observedAtMs: value["observedAtMs"],
424
+ projectionRevision: value["projectionRevision"],
425
+ version: SUITE_ENTITLEMENTS_CLAIM_VERSION
426
+ };
427
+ }
428
+ function validExpectedString(value) {
429
+ let hasControl = false;
430
+ for (let index = 0;index < value.length; index += 1) {
431
+ const code = value.charCodeAt(index);
432
+ if (code <= 31 || code === 127) {
433
+ hasControl = true;
434
+ break;
435
+ }
436
+ }
437
+ return value.length >= 1 && value.length <= 2048 && value.trim() === value && !hasControl;
438
+ }
439
+ async function verifySuiteEntitlementToken(compactToken, options) {
440
+ if (compactToken.length < 1 || compactToken.length > 16384 || !validExpectedString(options.expectedAudience) || !validExpectedString(options.expectedIssuer)) {
441
+ return { kind: "invalid", reason: "claims" };
442
+ }
443
+ let payload;
444
+ try {
445
+ payload = await options.verify(compactToken);
446
+ } catch {
447
+ return { kind: "invalid", reason: "signature" };
448
+ }
449
+ const parsed = parseSuiteJwtClaims(payload);
450
+ if (!parsed.ok)
451
+ return { kind: "invalid", reason: "claims" };
452
+ const claims = parsed.value;
453
+ if (claims.principal.issuer !== options.expectedIssuer) {
454
+ return { kind: "invalid", reason: "issuer" };
455
+ }
456
+ if (!claims.audience.includes(options.expectedAudience)) {
457
+ return { kind: "invalid", reason: "audience" };
458
+ }
459
+ const nowMs = options.nowMs ?? Date.now();
460
+ const clockSkewMs = options.clockSkewMs ?? 30000;
461
+ const maxTokenLifetimeMs = options.maxTokenLifetimeMs ?? 20 * 60000;
462
+ const maxProjectionAgeMs = options.maxProjectionAgeMs ?? SUITE_ENTITLEMENT_MAX_PROVIDER_AGE_MS;
463
+ if (!nonNegativeInteger(nowMs) || !nonNegativeInteger(clockSkewMs) || !nonNegativeInteger(maxTokenLifetimeMs) || !nonNegativeInteger(maxProjectionAgeMs)) {
464
+ return { kind: "invalid", reason: "time" };
465
+ }
466
+ const issuedAtMs = claims.issuedAtSeconds * 1000;
467
+ const expiresAtMs = claims.expiresAtSeconds * 1000;
468
+ const notBeforeMs = claims.notBeforeSeconds === undefined ? issuedAtMs : claims.notBeforeSeconds * 1000;
469
+ if (expiresAtMs - issuedAtMs > maxTokenLifetimeMs || issuedAtMs > nowMs + clockSkewMs || notBeforeMs > nowMs + clockSkewMs || expiresAtMs <= nowMs - clockSkewMs) {
470
+ return { kind: "invalid", reason: "time" };
471
+ }
472
+ if (!isRecord2(payload))
473
+ return { kind: "invalid", reason: "claims" };
474
+ const rawEntitlements = payload["suite_entitlements"];
475
+ if (rawEntitlements === undefined) {
476
+ return {
477
+ claims,
478
+ entitlements: { claim: null, features: [], kind: "legacy" },
479
+ kind: "verified"
480
+ };
481
+ }
482
+ const claim = parseEntitlements(rawEntitlements);
483
+ if (claim === null || claim.expiresAtMs > expiresAtMs) {
484
+ return { kind: "invalid", reason: "entitlements" };
485
+ }
486
+ const fresh = claim.observedAtMs <= nowMs + clockSkewMs && claim.expiresAtMs > nowMs - clockSkewMs && nowMs - claim.observedAtMs <= maxProjectionAgeMs;
487
+ return {
488
+ claims,
489
+ entitlements: fresh ? { claim, features: claim.features, kind: "fresh" } : { claim, features: [], kind: "stale" },
490
+ kind: "verified"
491
+ };
492
+ }
493
+ function suiteTokenGrantsFeature(result, feature) {
494
+ return result.kind === "verified" && result.entitlements.kind === "fresh" && result.entitlements.features.includes(feature);
495
+ }
496
+ export {
497
+ verifySuiteEntitlementToken,
498
+ suiteTokenGrantsFeature,
499
+ decideSuiteEntitlementReceiptProjection,
500
+ SUITE_ENTITLEMENT_MAX_PROVIDER_AGE_MS,
501
+ SUITE_ENTITLEMENTS_CLAIM_VERSION
502
+ };
@@ -0,0 +1,44 @@
1
+ // src/identity/functions.ts
2
+ import { makeFunctionReference } from "convex/server";
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/functions.ts
26
+ var accountsCurrent = makeFunctionReference("accounts:current");
27
+ var billingStartCheckout = makeFunctionReference("billing:startCheckout");
28
+ var billingCreatePortalSession = makeFunctionReference("billing:createPortalSession");
29
+ var billingOpenInvoice = makeFunctionReference("billing:openInvoice");
30
+ var suiteAccountsApi = deepFreeze({
31
+ accounts: { current: accountsCurrent },
32
+ billing: {
33
+ createPortalSession: billingCreatePortalSession,
34
+ openInvoice: billingOpenInvoice,
35
+ startCheckout: billingStartCheckout
36
+ }
37
+ });
38
+ export {
39
+ suiteAccountsApi,
40
+ billingStartCheckout,
41
+ billingOpenInvoice,
42
+ billingCreatePortalSession,
43
+ accountsCurrent
44
+ };