@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.
- package/LICENSE +21 -0
- package/README.md +496 -0
- package/compiler-foundation.css +1 -0
- package/dist/auth-client.js +16 -0
- package/dist/auth-proxy.js +757 -0
- package/dist/bearer-verifier.js +1119 -0
- package/dist/browser-session.js +208 -0
- package/dist/client-configuration.js +470 -0
- package/dist/convex-browser-auth-browser.js +923 -0
- package/dist/convex-browser-auth-server.js +846 -0
- package/dist/convex-browser-auth.js +597 -0
- package/dist/entitlements.js +502 -0
- package/dist/identity/functions.js +44 -0
- package/dist/identity/index.js +1716 -0
- package/dist/identity/return-targets.js +33 -0
- package/dist/index.js +1133 -0
- package/dist/oidc-device-code.js +253 -0
- package/dist/oidc-rp.js +2259 -0
- package/dist/oidc-session-policy.js +5 -0
- package/dist/oidc-surface-server.js +2332 -0
- package/dist/profile-form.js +1934 -0
- package/dist/profile.js +17 -0
- package/dist/public-config.js +462 -0
- package/dist/react.js +137 -0
- package/dist/receipt-verifier.js +619 -0
- package/dist/registry.js +327 -0
- package/dist/stylex-manifest.json +1 -0
- package/dist/stylex.css +435 -0
- package/dist/urls.js +391 -0
- package/package.json +263 -0
- package/portfolio-inventory.json +37 -0
- package/src/auth-client.ts +37 -0
- package/src/auth-proxy.ts +473 -0
- package/src/bearer-verifier.ts +478 -0
- package/src/browser-session.ts +260 -0
- package/src/client-configuration.ts +172 -0
- package/src/convex-browser-auth-browser.ts +238 -0
- package/src/convex-browser-auth-server.ts +492 -0
- package/src/convex-browser-auth.ts +208 -0
- package/src/convex-url.ts +74 -0
- package/src/entitlements.ts +317 -0
- package/src/identity/catalog.ts +227 -0
- package/src/identity/consumers.ts +53 -0
- package/src/identity/functions.ts +70 -0
- package/src/identity/identifiers.ts +82 -0
- package/src/identity/index.ts +10 -0
- package/src/identity/links.ts +280 -0
- package/src/identity/membership.ts +156 -0
- package/src/identity/principals.ts +301 -0
- package/src/identity/profiles-v2.ts +330 -0
- package/src/identity/profiles.ts +524 -0
- package/src/identity/return-targets.ts +20 -0
- package/src/identity/usernames.ts +101 -0
- package/src/identity/views.ts +289 -0
- package/src/immutable.ts +25 -0
- package/src/index.ts +7 -0
- package/src/oidc-continuation.ts +59 -0
- package/src/oidc-device-code.ts +401 -0
- package/src/oidc-rp.ts +2217 -0
- package/src/oidc-session-policy.ts +6 -0
- package/src/oidc-surface-server.ts +186 -0
- package/src/profile-form.css +1 -0
- package/src/profile-form.stylex.ts +226 -0
- package/src/profile-form.tsx +314 -0
- package/src/profile.ts +33 -0
- package/src/public-config.ts +294 -0
- package/src/public-profile-form-result.ts +82 -0
- package/src/public-profile-form.tsx +319 -0
- package/src/public-profile-links.ts +100 -0
- package/src/react.tsx +196 -0
- package/src/receipt-verifier.ts +273 -0
- package/src/registry.ts +600 -0
- package/src/urls.ts +234 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
// src/identity/links.ts
|
|
2
|
+
import { ok as ok5 } 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
|
+
import { err, ok } from "@hraness/result";
|
|
27
|
+
var SUITE_CATALOG_REVISION = "hraness-suite-v4";
|
|
28
|
+
var PREVIOUS_SUITE_CATALOG_REVISION = "cclrte-suite-v3";
|
|
29
|
+
var LEGACY_SUITE_CATALOG_REVISION = "cclrte-suite-v2";
|
|
30
|
+
var ARCHIVED_SUITE_CATALOG_REVISION = "cclrte-suite-v1";
|
|
31
|
+
var SUITE_CATALOG_REVISIONS = deepFreeze([
|
|
32
|
+
ARCHIVED_SUITE_CATALOG_REVISION,
|
|
33
|
+
LEGACY_SUITE_CATALOG_REVISION,
|
|
34
|
+
PREVIOUS_SUITE_CATALOG_REVISION,
|
|
35
|
+
SUITE_CATALOG_REVISION
|
|
36
|
+
]);
|
|
37
|
+
var SUITE_PLAN_IDS = deepFreeze(["community", "pro"]);
|
|
38
|
+
var SUITE_HISTORICAL_PLAN_IDS = deepFreeze([
|
|
39
|
+
"individual",
|
|
40
|
+
"business"
|
|
41
|
+
]);
|
|
42
|
+
var SUITE_CURRENT_FEATURE_IDS = deepFreeze([
|
|
43
|
+
"suite.paid",
|
|
44
|
+
"suite.community",
|
|
45
|
+
"suite.pro"
|
|
46
|
+
]);
|
|
47
|
+
var SUITE_PREVIOUS_FEATURE_IDS = deepFreeze([
|
|
48
|
+
"suite.paid",
|
|
49
|
+
"suite.believer"
|
|
50
|
+
]);
|
|
51
|
+
var SUITE_LEGACY_FEATURE_IDS = deepFreeze([
|
|
52
|
+
"suite.paid",
|
|
53
|
+
"suite.business"
|
|
54
|
+
]);
|
|
55
|
+
var SUITE_FEATURE_IDS = deepFreeze([
|
|
56
|
+
"suite.paid",
|
|
57
|
+
"suite.community",
|
|
58
|
+
"suite.pro",
|
|
59
|
+
"suite.believer",
|
|
60
|
+
"suite.business"
|
|
61
|
+
]);
|
|
62
|
+
var CURRENT_PLAN_FEATURES = deepFreeze({
|
|
63
|
+
community: ["suite.paid", "suite.community"],
|
|
64
|
+
pro: ["suite.paid", "suite.community", "suite.pro"]
|
|
65
|
+
});
|
|
66
|
+
var PREVIOUS_PLAN_FEATURES = deepFreeze({
|
|
67
|
+
business: ["suite.paid", "suite.believer"],
|
|
68
|
+
individual: ["suite.paid"]
|
|
69
|
+
});
|
|
70
|
+
var LEGACY_PLAN_FEATURES = deepFreeze({
|
|
71
|
+
business: ["suite.paid", "suite.business"],
|
|
72
|
+
individual: ["suite.paid"]
|
|
73
|
+
});
|
|
74
|
+
function parseSuitePlanId(value, revision = SUITE_CATALOG_REVISION) {
|
|
75
|
+
const ids = revision === SUITE_CATALOG_REVISION ? SUITE_PLAN_IDS : SUITE_HISTORICAL_PLAN_IDS;
|
|
76
|
+
return typeof value === "string" && ids.includes(value) ? ok(value) : err("invalid-plan");
|
|
77
|
+
}
|
|
78
|
+
function parseAnySuitePlanId(value) {
|
|
79
|
+
return typeof value === "string" && [...SUITE_PLAN_IDS, ...SUITE_HISTORICAL_PLAN_IDS].includes(value) ? ok(value) : err("invalid-plan");
|
|
80
|
+
}
|
|
81
|
+
function parseCurrentSuiteFeatureId(value) {
|
|
82
|
+
return value === "suite.paid" || value === "suite.community" || value === "suite.pro" ? ok(value) : err("invalid-feature");
|
|
83
|
+
}
|
|
84
|
+
function parseSuiteFeatureId(value) {
|
|
85
|
+
return typeof value === "string" && SUITE_FEATURE_IDS.includes(value) ? ok(value) : err("invalid-feature");
|
|
86
|
+
}
|
|
87
|
+
function parseSuiteCatalogRevision(value) {
|
|
88
|
+
return typeof value === "string" && SUITE_CATALOG_REVISIONS.includes(value) ? ok(value) : err("invalid-catalog-revision");
|
|
89
|
+
}
|
|
90
|
+
function featuresForSuitePlan(plan, revision = SUITE_CATALOG_REVISION) {
|
|
91
|
+
const table = revision === SUITE_CATALOG_REVISION ? CURRENT_PLAN_FEATURES : revision === ARCHIVED_SUITE_CATALOG_REVISION ? LEGACY_PLAN_FEATURES : PREVIOUS_PLAN_FEATURES;
|
|
92
|
+
const features = table[plan];
|
|
93
|
+
return features === undefined ? [] : [...features];
|
|
94
|
+
}
|
|
95
|
+
function grantedSuiteFeatureSets(revision) {
|
|
96
|
+
if (revision === SUITE_CATALOG_REVISION) {
|
|
97
|
+
return deepFreeze([
|
|
98
|
+
[],
|
|
99
|
+
CURRENT_PLAN_FEATURES.community,
|
|
100
|
+
CURRENT_PLAN_FEATURES.pro
|
|
101
|
+
]);
|
|
102
|
+
}
|
|
103
|
+
const table = revision === ARCHIVED_SUITE_CATALOG_REVISION ? LEGACY_PLAN_FEATURES : PREVIOUS_PLAN_FEATURES;
|
|
104
|
+
return deepFreeze([
|
|
105
|
+
[],
|
|
106
|
+
table.individual,
|
|
107
|
+
table.business
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
function suitePlanIncludesFeature(plan, feature) {
|
|
111
|
+
return plan !== null && featuresForSuitePlan(plan).includes(feature);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/identity/identifiers.ts
|
|
115
|
+
import { err as err2, ok as ok2 } from "@hraness/result";
|
|
116
|
+
var suiteAccountIdPattern = /^acct_[0-9a-f]{32}$/u;
|
|
117
|
+
var suiteInvoiceRefPattern = /^invref_[0-9a-f]{32}$/u;
|
|
118
|
+
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;
|
|
119
|
+
function parseSuiteAccountId(value) {
|
|
120
|
+
return typeof value === "string" && suiteAccountIdPattern.test(value) ? ok2(value) : err2("invalid-suite-account-id");
|
|
121
|
+
}
|
|
122
|
+
function parseSuiteInvoiceRef(value) {
|
|
123
|
+
return typeof value === "string" && suiteInvoiceRefPattern.test(value) ? ok2(value) : err2("invalid-suite-invoice-ref");
|
|
124
|
+
}
|
|
125
|
+
function generateSuiteAccountId(randomUuid = () => crypto.randomUUID()) {
|
|
126
|
+
const uuid = randomUuid();
|
|
127
|
+
if (!uuidPattern.test(uuid)) {
|
|
128
|
+
throw new TypeError("The suite account ID source did not return a UUID.");
|
|
129
|
+
}
|
|
130
|
+
const candidate = `acct_${uuid.replaceAll("-", "").toLowerCase()}`;
|
|
131
|
+
const parsed = parseSuiteAccountId(candidate);
|
|
132
|
+
if (!parsed.ok) {
|
|
133
|
+
throw new TypeError("The suite account ID source produced an invalid value.");
|
|
134
|
+
}
|
|
135
|
+
return parsed.value;
|
|
136
|
+
}
|
|
137
|
+
function generateSuiteInvoiceRef(randomUuid = () => crypto.randomUUID()) {
|
|
138
|
+
const uuid = randomUuid();
|
|
139
|
+
if (!uuidPattern.test(uuid)) {
|
|
140
|
+
throw new TypeError("The suite invoice reference source did not return a UUID.");
|
|
141
|
+
}
|
|
142
|
+
const candidate = `invref_${uuid.replaceAll("-", "").toLowerCase()}`;
|
|
143
|
+
const parsed = parseSuiteInvoiceRef(candidate);
|
|
144
|
+
if (!parsed.ok) {
|
|
145
|
+
throw new TypeError("The suite invoice reference source produced an invalid value.");
|
|
146
|
+
}
|
|
147
|
+
return parsed.value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/identity/principals.ts
|
|
151
|
+
import { err as err4, isRecord, ok as ok4 } from "@hraness/result";
|
|
152
|
+
|
|
153
|
+
// src/identity/usernames.ts
|
|
154
|
+
import { err as err3, ok as ok3 } from "@hraness/result";
|
|
155
|
+
var SUITE_USERNAME_MIN_LENGTH = 3;
|
|
156
|
+
var SUITE_USERNAME_MAX_LENGTH = 24;
|
|
157
|
+
var suiteUsernamePattern = /^[a-z0-9](?:[a-z0-9]|[-_](?=[a-z0-9]))*[a-z0-9]$/u;
|
|
158
|
+
var reservedSuiteUsernames = new Set([
|
|
159
|
+
"account",
|
|
160
|
+
"accounts",
|
|
161
|
+
"admin",
|
|
162
|
+
"api",
|
|
163
|
+
"auth",
|
|
164
|
+
"billing",
|
|
165
|
+
"design",
|
|
166
|
+
"docs",
|
|
167
|
+
"help",
|
|
168
|
+
"hraness",
|
|
169
|
+
"login",
|
|
170
|
+
"logout",
|
|
171
|
+
"new",
|
|
172
|
+
"newsletter",
|
|
173
|
+
"party",
|
|
174
|
+
"place",
|
|
175
|
+
"preview",
|
|
176
|
+
"pub",
|
|
177
|
+
"root",
|
|
178
|
+
"settings",
|
|
179
|
+
"social-image",
|
|
180
|
+
"source",
|
|
181
|
+
"sources",
|
|
182
|
+
"support",
|
|
183
|
+
"system",
|
|
184
|
+
"user",
|
|
185
|
+
"users",
|
|
186
|
+
"www"
|
|
187
|
+
]);
|
|
188
|
+
function validateCanonicalSuiteUsername(value) {
|
|
189
|
+
if (value.length < SUITE_USERNAME_MIN_LENGTH) {
|
|
190
|
+
return err3("suite-username-too-short");
|
|
191
|
+
}
|
|
192
|
+
if (value.length > SUITE_USERNAME_MAX_LENGTH) {
|
|
193
|
+
return err3("suite-username-too-long");
|
|
194
|
+
}
|
|
195
|
+
if (!suiteUsernamePattern.test(value)) {
|
|
196
|
+
return err3("invalid-suite-username");
|
|
197
|
+
}
|
|
198
|
+
return ok3(value);
|
|
199
|
+
}
|
|
200
|
+
function normalizeSuiteUsername(value) {
|
|
201
|
+
if (typeof value !== "string")
|
|
202
|
+
return err3("invalid-suite-username");
|
|
203
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
204
|
+
const code = value.charCodeAt(index);
|
|
205
|
+
if (code > 127 || code < 32 || code === 127) {
|
|
206
|
+
return err3("invalid-suite-username");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const trimmed = value.trim();
|
|
210
|
+
const parsed = validateCanonicalSuiteUsername(trimmed.toLowerCase());
|
|
211
|
+
if (!parsed.ok)
|
|
212
|
+
return parsed;
|
|
213
|
+
return reservedSuiteUsernames.has(parsed.value) ? err3("suite-username-reserved") : parsed;
|
|
214
|
+
}
|
|
215
|
+
function parseSuiteUsername(value) {
|
|
216
|
+
if (typeof value !== "string")
|
|
217
|
+
return err3("invalid-suite-username");
|
|
218
|
+
const parsed = validateCanonicalSuiteUsername(value);
|
|
219
|
+
return parsed.ok && parsed.value === value ? parsed : err3(parsed.ok ? "invalid-suite-username" : parsed.error);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/identity/principals.ts
|
|
223
|
+
var SUITE_PRODUCTS = deepFreeze([
|
|
224
|
+
"soundfish",
|
|
225
|
+
"hra",
|
|
226
|
+
"peopleblade",
|
|
227
|
+
"ghostget",
|
|
228
|
+
"crclte",
|
|
229
|
+
"pub"
|
|
230
|
+
]);
|
|
231
|
+
var LEGACY_SUITE_PRODUCT_IDS = deepFreeze([
|
|
232
|
+
"oprte",
|
|
233
|
+
"kitchen"
|
|
234
|
+
]);
|
|
235
|
+
var SUITE_ENVIRONMENTS = deepFreeze([
|
|
236
|
+
"development",
|
|
237
|
+
"staging",
|
|
238
|
+
"production"
|
|
239
|
+
]);
|
|
240
|
+
var SUITE_ISSUABLE_ENVIRONMENTS = deepFreeze([
|
|
241
|
+
"development",
|
|
242
|
+
"production"
|
|
243
|
+
]);
|
|
244
|
+
var localIssuerHosts = new Set(["127.0.0.1", "[::1]", "localhost"]);
|
|
245
|
+
function containsAsciiControl(value) {
|
|
246
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
247
|
+
const code = value.charCodeAt(index);
|
|
248
|
+
if (code <= 31 || code === 127)
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
function parseIdentityIssuer(value) {
|
|
254
|
+
if (typeof value !== "string" || value.length > 2048 || value.trim() !== value) {
|
|
255
|
+
return err4("invalid-issuer");
|
|
256
|
+
}
|
|
257
|
+
let parsed;
|
|
258
|
+
try {
|
|
259
|
+
parsed = new URL(value);
|
|
260
|
+
} catch {
|
|
261
|
+
return err4("invalid-issuer");
|
|
262
|
+
}
|
|
263
|
+
const local = localIssuerHosts.has(parsed.hostname);
|
|
264
|
+
if (parsed.protocol !== "https:" && !(local && parsed.protocol === "http:") || parsed.username !== "" || parsed.password !== "" || parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "") {
|
|
265
|
+
return err4("invalid-issuer");
|
|
266
|
+
}
|
|
267
|
+
return ok4(parsed.origin);
|
|
268
|
+
}
|
|
269
|
+
function parseIdentitySubject(value) {
|
|
270
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 255 || value.trim() !== value || containsAsciiControl(value)) {
|
|
271
|
+
return err4("invalid-subject");
|
|
272
|
+
}
|
|
273
|
+
return ok4(value);
|
|
274
|
+
}
|
|
275
|
+
function parseIssuerSubject(value) {
|
|
276
|
+
if (!isRecord(value))
|
|
277
|
+
return err4("invalid-subject");
|
|
278
|
+
const issuer = parseIdentityIssuer(value["issuer"]);
|
|
279
|
+
if (!issuer.ok)
|
|
280
|
+
return issuer;
|
|
281
|
+
const subject = parseIdentitySubject(value["subject"]);
|
|
282
|
+
return subject.ok ? ok4({ issuer: issuer.value, subject: subject.value }) : subject;
|
|
283
|
+
}
|
|
284
|
+
function parseSuiteProduct(value) {
|
|
285
|
+
switch (value) {
|
|
286
|
+
case "soundfish":
|
|
287
|
+
case "hra":
|
|
288
|
+
case "peopleblade":
|
|
289
|
+
case "ghostget":
|
|
290
|
+
case "crclte":
|
|
291
|
+
case "pub":
|
|
292
|
+
return ok4(value);
|
|
293
|
+
case "oprte":
|
|
294
|
+
case "kitchen":
|
|
295
|
+
return ok4("hra");
|
|
296
|
+
default:
|
|
297
|
+
return err4("invalid-product");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function parseSuiteEnvironment(value) {
|
|
301
|
+
return typeof value === "string" && SUITE_ENVIRONMENTS.includes(value) ? ok4(value) : err4("invalid-environment");
|
|
302
|
+
}
|
|
303
|
+
function isSuiteIssuableEnvironment(value) {
|
|
304
|
+
return value === "development" || value === "production";
|
|
305
|
+
}
|
|
306
|
+
function parseLegacyPrincipalLink(value) {
|
|
307
|
+
if (!isRecord(value))
|
|
308
|
+
return err4("invalid-legacy-link");
|
|
309
|
+
const product = parseSuiteProduct(value["product"]);
|
|
310
|
+
if (!product.ok)
|
|
311
|
+
return product;
|
|
312
|
+
const environment = parseSuiteEnvironment(value["environment"]);
|
|
313
|
+
if (!environment.ok)
|
|
314
|
+
return environment;
|
|
315
|
+
const legacySubject = parseIdentitySubject(value["legacySubject"]);
|
|
316
|
+
if (!legacySubject.ok)
|
|
317
|
+
return legacySubject;
|
|
318
|
+
return ok4({
|
|
319
|
+
environment: environment.value,
|
|
320
|
+
legacySubject: legacySubject.value,
|
|
321
|
+
product: product.value
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function parseAudience(value) {
|
|
325
|
+
const values = typeof value === "string" ? [value] : value;
|
|
326
|
+
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) {
|
|
327
|
+
return err4("invalid-audience");
|
|
328
|
+
}
|
|
329
|
+
return ok4(values);
|
|
330
|
+
}
|
|
331
|
+
function parseTimestamp(value) {
|
|
332
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
333
|
+
}
|
|
334
|
+
function parseSuiteJwtClaims(value) {
|
|
335
|
+
if (!isRecord(value))
|
|
336
|
+
return err4("invalid-jwt-claims");
|
|
337
|
+
const principal = parseIssuerSubject({
|
|
338
|
+
issuer: value["iss"],
|
|
339
|
+
subject: value["sub"]
|
|
340
|
+
});
|
|
341
|
+
if (!principal.ok)
|
|
342
|
+
return principal;
|
|
343
|
+
const suiteAccountId = parseSuiteAccountId(value["suite_account_id"]);
|
|
344
|
+
if (!suiteAccountId.ok)
|
|
345
|
+
return suiteAccountId;
|
|
346
|
+
const audience = parseAudience(value["aud"]);
|
|
347
|
+
if (!audience.ok)
|
|
348
|
+
return audience;
|
|
349
|
+
const issuedAtSeconds = parseTimestamp(value["iat"]);
|
|
350
|
+
const expiresAtSeconds = parseTimestamp(value["exp"]);
|
|
351
|
+
const notBeforeSeconds = value["nbf"] === undefined ? undefined : parseTimestamp(value["nbf"]);
|
|
352
|
+
const legacyProfile = value["profile_revision"] === undefined && value["profile_complete"] === undefined && value["username"] === undefined;
|
|
353
|
+
const profileRevision = legacyProfile ? null : value["profile_revision"] === "username-v1" ? "username-v1" : undefined;
|
|
354
|
+
const profileComplete = legacyProfile ? false : typeof value["profile_complete"] === "boolean" ? value["profile_complete"] : undefined;
|
|
355
|
+
const username = legacyProfile || value["username"] === null ? null : parseSuiteUsername(value["username"]);
|
|
356
|
+
if (issuedAtSeconds === null || expiresAtSeconds === null || expiresAtSeconds <= issuedAtSeconds || notBeforeSeconds === null || notBeforeSeconds !== undefined && notBeforeSeconds > expiresAtSeconds || profileRevision === undefined || profileComplete === undefined || username !== null && !username.ok || profileComplete !== (username !== null)) {
|
|
357
|
+
return err4("invalid-jwt-claims");
|
|
358
|
+
}
|
|
359
|
+
return ok4({
|
|
360
|
+
audience: audience.value,
|
|
361
|
+
expiresAtSeconds,
|
|
362
|
+
issuedAtSeconds,
|
|
363
|
+
...notBeforeSeconds === undefined ? {} : { notBeforeSeconds },
|
|
364
|
+
principal: principal.value,
|
|
365
|
+
profileComplete,
|
|
366
|
+
profileRevision,
|
|
367
|
+
suiteAccountId: suiteAccountId.value,
|
|
368
|
+
username: username?.value ?? null
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/identity/links.ts
|
|
373
|
+
var IDENTITY_LINK_PROOF_VERSION = "suite-product-link-proof-v1";
|
|
374
|
+
var IDENTITY_LINK_RECEIPT_VERSION = "suite-link-receipt-v1";
|
|
375
|
+
var SUITE_ENTITLEMENTS_CLAIM_VERSION = "suite-entitlements-v1";
|
|
376
|
+
var SUITE_ENTITLEMENT_RECEIPT_VERSION = "suite-entitlement-receipt-v1";
|
|
377
|
+
var IDENTITY_LINK_MAX_TTL_MS = 5 * 60000;
|
|
378
|
+
var SUITE_ENTITLEMENT_RECEIPT_MAX_TTL_MS = 5 * 60000;
|
|
379
|
+
var IDENTITY_LINK_CLOCK_SKEW_MS = 30000;
|
|
380
|
+
var SUITE_LINK_PRODUCTS = deepFreeze([
|
|
381
|
+
"soundfish",
|
|
382
|
+
"hra",
|
|
383
|
+
"peopleblade",
|
|
384
|
+
"ghostget",
|
|
385
|
+
"crclte",
|
|
386
|
+
"pub"
|
|
387
|
+
]);
|
|
388
|
+
var LEGACY_SUITE_LINK_PRODUCTS = deepFreeze([
|
|
389
|
+
"oprte",
|
|
390
|
+
"kitchen"
|
|
391
|
+
]);
|
|
392
|
+
function parseSuiteLinkProduct(value) {
|
|
393
|
+
const parsed = parseSuiteProduct(value);
|
|
394
|
+
if (!parsed.ok)
|
|
395
|
+
return parsed;
|
|
396
|
+
switch (parsed.value) {
|
|
397
|
+
case "soundfish":
|
|
398
|
+
case "hra":
|
|
399
|
+
case "peopleblade":
|
|
400
|
+
case "ghostget":
|
|
401
|
+
case "crclte":
|
|
402
|
+
case "pub":
|
|
403
|
+
return ok5(parsed.value);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function safeInteger(value) {
|
|
407
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
408
|
+
}
|
|
409
|
+
function validateProductLinkProof(input, now) {
|
|
410
|
+
const product = parseSuiteLinkProduct(input.product);
|
|
411
|
+
const environment = parseSuiteEnvironment(input.environment);
|
|
412
|
+
const localSubject = parseIdentitySubject(input.localSubject);
|
|
413
|
+
if (!safeInteger(now) || !product.ok || !environment.ok || !localSubject.ok || !/^[A-Za-z0-9_-]{22,128}$/u.test(input.challengeId) || !/^[a-z0-9][a-z0-9._-]{0,31}$/u.test(input.keyVersion) || !safeInteger(input.issuedAtMs) || !safeInteger(input.expiresAtMs) || input.expiresAtMs <= input.issuedAtMs || input.expiresAtMs - input.issuedAtMs > IDENTITY_LINK_MAX_TTL_MS) {
|
|
414
|
+
return "invalid";
|
|
415
|
+
}
|
|
416
|
+
if (input.issuedAtMs > now + IDENTITY_LINK_CLOCK_SKEW_MS) {
|
|
417
|
+
return "not-yet-valid";
|
|
418
|
+
}
|
|
419
|
+
return input.expiresAtMs <= now ? "expired" : null;
|
|
420
|
+
}
|
|
421
|
+
function productLinkProofMessage(input) {
|
|
422
|
+
return JSON.stringify([
|
|
423
|
+
IDENTITY_LINK_PROOF_VERSION,
|
|
424
|
+
input.product,
|
|
425
|
+
input.environment,
|
|
426
|
+
input.localSubject,
|
|
427
|
+
input.challengeId,
|
|
428
|
+
input.issuedAtMs,
|
|
429
|
+
input.expiresAtMs,
|
|
430
|
+
input.keyVersion
|
|
431
|
+
]);
|
|
432
|
+
}
|
|
433
|
+
function suiteLinkReceiptMessage(input) {
|
|
434
|
+
return JSON.stringify([
|
|
435
|
+
IDENTITY_LINK_RECEIPT_VERSION,
|
|
436
|
+
input.product,
|
|
437
|
+
input.environment,
|
|
438
|
+
input.localSubject,
|
|
439
|
+
input.suiteAccountId,
|
|
440
|
+
input.challengeId,
|
|
441
|
+
input.issuedAtMs,
|
|
442
|
+
input.expiresAtMs,
|
|
443
|
+
input.keyVersion
|
|
444
|
+
]);
|
|
445
|
+
}
|
|
446
|
+
function validateSuiteLinkReceipt(input, now) {
|
|
447
|
+
const proofIssue = validateProductLinkProof(input, now);
|
|
448
|
+
if (proofIssue !== null || input.version !== IDENTITY_LINK_RECEIPT_VERSION || !parseSuiteAccountId(input.suiteAccountId).ok || !/^[A-Za-z0-9_-]{43}$/u.test(input.signature)) {
|
|
449
|
+
return proofIssue ?? "invalid";
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
function exactGrantedFeatures(revision, values) {
|
|
454
|
+
if (values.length > SUITE_FEATURE_IDS.length)
|
|
455
|
+
return false;
|
|
456
|
+
const parsed = [];
|
|
457
|
+
for (const value of values) {
|
|
458
|
+
const feature = parseSuiteFeatureId(value);
|
|
459
|
+
if (!feature.ok || parsed.includes(feature.value))
|
|
460
|
+
return false;
|
|
461
|
+
parsed.push(feature.value);
|
|
462
|
+
}
|
|
463
|
+
return grantedSuiteFeatureSets(revision).some((set) => set.length === parsed.length && set.every((feature, index) => feature === parsed[index]));
|
|
464
|
+
}
|
|
465
|
+
function validateSuiteEntitlementsClaim(input) {
|
|
466
|
+
const catalogRevision = parseSuiteCatalogRevision(input.catalogRevision);
|
|
467
|
+
return input.version === SUITE_ENTITLEMENTS_CLAIM_VERSION && catalogRevision.ok && safeInteger(input.observedAtMs) && safeInteger(input.expiresAtMs) && input.expiresAtMs > input.observedAtMs && safeInteger(input.projectionRevision) && Array.isArray(input.features) && exactGrantedFeatures(catalogRevision.value, input.features);
|
|
468
|
+
}
|
|
469
|
+
function suiteEntitlementReceiptMessage(input) {
|
|
470
|
+
return JSON.stringify([
|
|
471
|
+
SUITE_ENTITLEMENT_RECEIPT_VERSION,
|
|
472
|
+
input.product,
|
|
473
|
+
input.environment,
|
|
474
|
+
input.suiteAccountId,
|
|
475
|
+
input.keyVersion,
|
|
476
|
+
input.issuedAtMs,
|
|
477
|
+
input.expiresAtMs,
|
|
478
|
+
input.entitlements.version,
|
|
479
|
+
input.entitlements.catalogRevision,
|
|
480
|
+
input.entitlements.observedAtMs,
|
|
481
|
+
input.entitlements.expiresAtMs,
|
|
482
|
+
input.entitlements.projectionRevision,
|
|
483
|
+
input.entitlements.features
|
|
484
|
+
]);
|
|
485
|
+
}
|
|
486
|
+
function validateSuiteEntitlementReceipt(input, now) {
|
|
487
|
+
const product = parseSuiteLinkProduct(input.product);
|
|
488
|
+
const environment = parseSuiteEnvironment(input.environment);
|
|
489
|
+
if (!safeInteger(now) || input.version !== SUITE_ENTITLEMENT_RECEIPT_VERSION || !product.ok || !environment.ok || !parseSuiteAccountId(input.suiteAccountId).ok || !/^[a-z0-9][a-z0-9._-]{0,31}$/u.test(input.keyVersion) || !safeInteger(input.issuedAtMs) || !safeInteger(input.expiresAtMs) || input.expiresAtMs <= input.issuedAtMs || input.expiresAtMs - input.issuedAtMs > SUITE_ENTITLEMENT_RECEIPT_MAX_TTL_MS || !validateSuiteEntitlementsClaim(input.entitlements) || input.expiresAtMs > input.entitlements.expiresAtMs || !/^[A-Za-z0-9_-]{43}$/u.test(input.signature)) {
|
|
490
|
+
return "invalid";
|
|
491
|
+
}
|
|
492
|
+
if (input.issuedAtMs > now + IDENTITY_LINK_CLOCK_SKEW_MS || input.entitlements.observedAtMs > now + IDENTITY_LINK_CLOCK_SKEW_MS) {
|
|
493
|
+
return "not-yet-valid";
|
|
494
|
+
}
|
|
495
|
+
return input.expiresAtMs <= now ? "expired" : null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/receipt-verifier.ts
|
|
499
|
+
function isRecord2(value) {
|
|
500
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
501
|
+
}
|
|
502
|
+
function parseSuiteReceiptKeyring(value) {
|
|
503
|
+
let decoded = value;
|
|
504
|
+
if (typeof value === "string") {
|
|
505
|
+
if (value.length > 32768)
|
|
506
|
+
return null;
|
|
507
|
+
try {
|
|
508
|
+
decoded = JSON.parse(value);
|
|
509
|
+
} catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (!isRecord2(decoded) || decoded["version"] !== 1 || !Array.isArray(decoded["keys"]) || decoded["keys"].length < 1 || decoded["keys"].length > 20) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
const keys = [];
|
|
517
|
+
const identities = new Set;
|
|
518
|
+
for (const rawKey of decoded["keys"]) {
|
|
519
|
+
if (!isRecord2(rawKey))
|
|
520
|
+
return null;
|
|
521
|
+
const { environment, keyVersion, product, secret } = rawKey;
|
|
522
|
+
const parsedEnvironment = parseSuiteEnvironment(environment);
|
|
523
|
+
const parsedProduct = parseSuiteLinkProduct(product);
|
|
524
|
+
if (!parsedEnvironment.ok || !parsedProduct.ok || !/^[a-z0-9][a-z0-9._-]{0,31}$/u.test(String(keyVersion)) || typeof secret !== "string" || new TextEncoder().encode(secret).byteLength < 32 || secret.length > 1024) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
const identity = `${parsedProduct.value}:${parsedEnvironment.value}:${String(keyVersion)}`;
|
|
528
|
+
if (identities.has(identity))
|
|
529
|
+
return null;
|
|
530
|
+
identities.add(identity);
|
|
531
|
+
keys.push({
|
|
532
|
+
environment: parsedEnvironment.value,
|
|
533
|
+
keyVersion: String(keyVersion),
|
|
534
|
+
product: parsedProduct.value,
|
|
535
|
+
secret
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return deepFreeze({ keys, version: 1 });
|
|
539
|
+
}
|
|
540
|
+
function selectSuiteReceiptConfiguration(value, product, activeKeyVersion) {
|
|
541
|
+
const canonicalProduct = parseSuiteLinkProduct(product);
|
|
542
|
+
if (!canonicalProduct.ok || typeof activeKeyVersion !== "string" || !/^[a-z0-9][a-z0-9._-]{0,31}$/u.test(activeKeyVersion)) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
const keyring = parseSuiteReceiptKeyring(value);
|
|
546
|
+
if (keyring === null)
|
|
547
|
+
return null;
|
|
548
|
+
const active = keyring.keys.filter((key2) => key2.product === canonicalProduct.value && key2.keyVersion === activeKeyVersion && isSuiteIssuableEnvironment(key2.environment));
|
|
549
|
+
if (active.length !== 1)
|
|
550
|
+
return null;
|
|
551
|
+
const key = active[0];
|
|
552
|
+
const verificationKeys = keyring.keys.filter((candidate) => candidate.product === canonicalProduct.value && candidate.environment === key.environment);
|
|
553
|
+
return deepFreeze({
|
|
554
|
+
key,
|
|
555
|
+
keyring: { keys: verificationKeys, version: 1 }
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
function keyFor(keyring, product, environment, keyVersion) {
|
|
559
|
+
const canonicalProduct = parseSuiteLinkProduct(product);
|
|
560
|
+
if (!canonicalProduct.ok)
|
|
561
|
+
return null;
|
|
562
|
+
return keyring.keys.find((key) => key.product === canonicalProduct.value && key.environment === environment && key.keyVersion === keyVersion) ?? null;
|
|
563
|
+
}
|
|
564
|
+
function decodeSignature(value) {
|
|
565
|
+
if (!/^[A-Za-z0-9_-]{43}$/u.test(value))
|
|
566
|
+
return null;
|
|
567
|
+
try {
|
|
568
|
+
const binary = atob(value.replaceAll("-", "+").replaceAll("_", "/") + "=");
|
|
569
|
+
const result = new Uint8Array(new ArrayBuffer(binary.length));
|
|
570
|
+
for (let index = 0;index < binary.length; index += 1) {
|
|
571
|
+
result[index] = binary.charCodeAt(index);
|
|
572
|
+
}
|
|
573
|
+
return result.byteLength === 32 ? result : null;
|
|
574
|
+
} catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async function hmac(secret, message) {
|
|
579
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign"]);
|
|
580
|
+
return await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
|
|
581
|
+
}
|
|
582
|
+
async function verifySignature(secret, message, signature) {
|
|
583
|
+
const received = decodeSignature(signature);
|
|
584
|
+
if (received === null)
|
|
585
|
+
return false;
|
|
586
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["verify"]);
|
|
587
|
+
return await crypto.subtle.verify("HMAC", key, received, new TextEncoder().encode(message));
|
|
588
|
+
}
|
|
589
|
+
function encodeBase64Url(bytes) {
|
|
590
|
+
let binary = "";
|
|
591
|
+
for (const byte of bytes)
|
|
592
|
+
binary += String.fromCharCode(byte);
|
|
593
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
|
|
594
|
+
}
|
|
595
|
+
async function signSuiteProductLinkProof(proof, keyring, nowMs) {
|
|
596
|
+
if (validateProductLinkProof(proof, nowMs) !== null || !isSuiteIssuableEnvironment(proof.environment))
|
|
597
|
+
return null;
|
|
598
|
+
const key = keyFor(keyring, proof.product, proof.environment, proof.keyVersion);
|
|
599
|
+
return key === null ? null : encodeBase64Url(new Uint8Array(await hmac(key.secret, productLinkProofMessage(proof))));
|
|
600
|
+
}
|
|
601
|
+
async function verifySuiteLinkReceiptSignature(receipt, keyring, nowMs) {
|
|
602
|
+
if (validateSuiteLinkReceipt(receipt, nowMs) !== null)
|
|
603
|
+
return false;
|
|
604
|
+
const key = keyFor(keyring, receipt.product, receipt.environment, receipt.keyVersion);
|
|
605
|
+
return key !== null && await verifySignature(key.secret, suiteLinkReceiptMessage(receipt), receipt.signature);
|
|
606
|
+
}
|
|
607
|
+
async function verifySuiteEntitlementReceiptSignature(receipt, keyring, nowMs) {
|
|
608
|
+
if (validateSuiteEntitlementReceipt(receipt, nowMs) !== null)
|
|
609
|
+
return false;
|
|
610
|
+
const key = keyFor(keyring, receipt.product, receipt.environment, receipt.keyVersion);
|
|
611
|
+
return key !== null && await verifySignature(key.secret, suiteEntitlementReceiptMessage(receipt), receipt.signature);
|
|
612
|
+
}
|
|
613
|
+
export {
|
|
614
|
+
verifySuiteLinkReceiptSignature,
|
|
615
|
+
verifySuiteEntitlementReceiptSignature,
|
|
616
|
+
signSuiteProductLinkProof,
|
|
617
|
+
selectSuiteReceiptConfiguration,
|
|
618
|
+
parseSuiteReceiptKeyring
|
|
619
|
+
};
|