@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,923 @@
|
|
|
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/oidc-continuation.ts
|
|
586
|
+
function htmlAttribute(value) {
|
|
587
|
+
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
588
|
+
}
|
|
589
|
+
function inlineJson(value) {
|
|
590
|
+
return JSON.stringify(value).replaceAll("&", "\\u0026").replaceAll("<", "\\u003c").replaceAll(">", "\\u003e").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
591
|
+
}
|
|
592
|
+
var SUITE_OIDC_JUST_SIGNED_IN_STORAGE_KEY = "hraness:suite:signed-in:v1";
|
|
593
|
+
function createOidcContinuationResponse(returnTo, nonce, cookies) {
|
|
594
|
+
const headers = new Headers({
|
|
595
|
+
"cache-control": "no-store",
|
|
596
|
+
"content-security-policy": [
|
|
597
|
+
"default-src 'none'",
|
|
598
|
+
"base-uri 'none'",
|
|
599
|
+
"form-action 'none'",
|
|
600
|
+
"frame-ancestors 'none'",
|
|
601
|
+
`script-src 'nonce-${nonce}'`
|
|
602
|
+
].join("; "),
|
|
603
|
+
"content-type": "text/html; charset=utf-8",
|
|
604
|
+
pragma: "no-cache",
|
|
605
|
+
"referrer-policy": "no-referrer",
|
|
606
|
+
"x-content-type-options": "nosniff",
|
|
607
|
+
"x-frame-options": "DENY"
|
|
608
|
+
});
|
|
609
|
+
for (const cookie of cookies)
|
|
610
|
+
headers.append("set-cookie", cookie);
|
|
611
|
+
const body = [
|
|
612
|
+
"<!doctype html>",
|
|
613
|
+
'<html lang="en">',
|
|
614
|
+
"<head>",
|
|
615
|
+
'<meta charset="utf-8">',
|
|
616
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
617
|
+
"<title>Signed in</title>",
|
|
618
|
+
"</head>",
|
|
619
|
+
"<body>",
|
|
620
|
+
"<p>Signed in.</p>",
|
|
621
|
+
`<p><a href="${htmlAttribute(returnTo)}">Continue</a></p>`,
|
|
622
|
+
`<script nonce="${nonce}">try{sessionStorage.setItem(${inlineJson(SUITE_OIDC_JUST_SIGNED_IN_STORAGE_KEY)},"1")}catch{}location.replace(${inlineJson(returnTo)});</script>`,
|
|
623
|
+
"</body>",
|
|
624
|
+
"</html>"
|
|
625
|
+
].join("");
|
|
626
|
+
return new Response(body, { headers, status: 200 });
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/browser-session.ts
|
|
630
|
+
var MAXIMUM_SESSION_RESPONSE_BYTES = 32768;
|
|
631
|
+
var SESSION_PATH = "/api/suite-auth/session";
|
|
632
|
+
var REFRESH_PATH = "/api/suite-auth/refresh";
|
|
633
|
+
var SIGN_OUT_PATH = "/api/suite-auth/sign-out";
|
|
634
|
+
var SUITE_OIDC_REFRESH_LOCK_NAME = "jungle-suite-accounts:oidc-refresh:v1";
|
|
635
|
+
var SUITE_OIDC_SESSION_CHANNEL_NAME = "jungle-suite-accounts:oidc-session:v1";
|
|
636
|
+
var SIGNED_OUT_EVENT = Object.freeze({
|
|
637
|
+
kind: "signed_out",
|
|
638
|
+
version: "suite-oidc-session-event-v1"
|
|
639
|
+
});
|
|
640
|
+
var localRefresh = null;
|
|
641
|
+
function isExactRefreshRequired(value) {
|
|
642
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 1 && Reflect.get(value, "kind") === "refresh_required";
|
|
643
|
+
}
|
|
644
|
+
async function readBoundedJson(response) {
|
|
645
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
646
|
+
if (contentType !== "application/json" || response.body === null) {
|
|
647
|
+
throw new Error("The suite session response was not JSON.");
|
|
648
|
+
}
|
|
649
|
+
const declaredLength = response.headers.get("content-length");
|
|
650
|
+
if (declaredLength !== null && (!/^(?:0|[1-9]\d*)$/u.test(declaredLength) || Number(declaredLength) > MAXIMUM_SESSION_RESPONSE_BYTES)) {
|
|
651
|
+
await response.body.cancel().catch(() => {
|
|
652
|
+
return;
|
|
653
|
+
});
|
|
654
|
+
throw new Error("The suite session response was too large.");
|
|
655
|
+
}
|
|
656
|
+
const reader = response.body.getReader();
|
|
657
|
+
const chunks = [];
|
|
658
|
+
let total = 0;
|
|
659
|
+
try {
|
|
660
|
+
while (true) {
|
|
661
|
+
const result = await reader.read();
|
|
662
|
+
if (result.done)
|
|
663
|
+
break;
|
|
664
|
+
total += result.value.byteLength;
|
|
665
|
+
if (total > MAXIMUM_SESSION_RESPONSE_BYTES) {
|
|
666
|
+
await reader.cancel().catch(() => {
|
|
667
|
+
return;
|
|
668
|
+
});
|
|
669
|
+
throw new Error("The suite session response was too large.");
|
|
670
|
+
}
|
|
671
|
+
chunks.push(result.value);
|
|
672
|
+
}
|
|
673
|
+
} finally {
|
|
674
|
+
reader.releaseLock();
|
|
675
|
+
}
|
|
676
|
+
const bytes = new Uint8Array(total);
|
|
677
|
+
let offset = 0;
|
|
678
|
+
for (const chunk of chunks) {
|
|
679
|
+
bytes.set(chunk, offset);
|
|
680
|
+
offset += chunk.byteLength;
|
|
681
|
+
}
|
|
682
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
683
|
+
}
|
|
684
|
+
async function requestJson(fetcher, path, method) {
|
|
685
|
+
const response = await fetcher(path, {
|
|
686
|
+
cache: "no-store",
|
|
687
|
+
credentials: "same-origin",
|
|
688
|
+
headers: { accept: "application/json" },
|
|
689
|
+
method,
|
|
690
|
+
redirect: "error"
|
|
691
|
+
});
|
|
692
|
+
const value = await readBoundedJson(response);
|
|
693
|
+
if (!response.ok) {
|
|
694
|
+
throw new Error("The suite session request failed.");
|
|
695
|
+
}
|
|
696
|
+
return value;
|
|
697
|
+
}
|
|
698
|
+
function exactSignedOut(value) {
|
|
699
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 1 && Reflect.get(value, "kind") === "signed_out";
|
|
700
|
+
}
|
|
701
|
+
function broadcastSuiteOidcBrowserSignOut() {
|
|
702
|
+
if (typeof BroadcastChannel === "undefined")
|
|
703
|
+
return;
|
|
704
|
+
const channel = new BroadcastChannel(SUITE_OIDC_SESSION_CHANNEL_NAME);
|
|
705
|
+
try {
|
|
706
|
+
channel.postMessage(SIGNED_OUT_EVENT);
|
|
707
|
+
} finally {
|
|
708
|
+
channel.close();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function subscribeSuiteOidcBrowserSignOut(listener) {
|
|
712
|
+
if (typeof BroadcastChannel === "undefined")
|
|
713
|
+
return () => {
|
|
714
|
+
return;
|
|
715
|
+
};
|
|
716
|
+
const channel = new BroadcastChannel(SUITE_OIDC_SESSION_CHANNEL_NAME);
|
|
717
|
+
const onMessage = (event) => {
|
|
718
|
+
const value = event.data;
|
|
719
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 2 && Reflect.get(value, "kind") === SIGNED_OUT_EVENT.kind && Reflect.get(value, "version") === SIGNED_OUT_EVENT.version) {
|
|
720
|
+
listener();
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
channel.addEventListener("message", onMessage);
|
|
724
|
+
return () => {
|
|
725
|
+
channel.removeEventListener("message", onMessage);
|
|
726
|
+
channel.close();
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
async function withLocalRefreshLock(task) {
|
|
730
|
+
if (localRefresh !== null)
|
|
731
|
+
return await localRefresh;
|
|
732
|
+
const pending = task();
|
|
733
|
+
localRefresh = pending;
|
|
734
|
+
try {
|
|
735
|
+
return await pending;
|
|
736
|
+
} finally {
|
|
737
|
+
if (localRefresh === pending)
|
|
738
|
+
localRefresh = null;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function withRefreshLock(task, injectedLock) {
|
|
742
|
+
if (injectedLock !== undefined) {
|
|
743
|
+
return injectedLock === null ? await withLocalRefreshLock(task) : await injectedLock(SUITE_OIDC_REFRESH_LOCK_NAME, task);
|
|
744
|
+
}
|
|
745
|
+
if (typeof navigator !== "undefined" && navigator.locks !== undefined) {
|
|
746
|
+
return await navigator.locks.request(SUITE_OIDC_REFRESH_LOCK_NAME, { mode: "exclusive" }, task);
|
|
747
|
+
}
|
|
748
|
+
return await withLocalRefreshLock(task);
|
|
749
|
+
}
|
|
750
|
+
async function loadSuiteOidcBrowserSession(dependencies = {}) {
|
|
751
|
+
const fetcher = dependencies.fetch ?? globalThis.fetch;
|
|
752
|
+
const withExclusiveLock = dependencies.withExclusiveLock;
|
|
753
|
+
const current = await requestJson(fetcher, SESSION_PATH, "GET");
|
|
754
|
+
if (!isExactRefreshRequired(current))
|
|
755
|
+
return current;
|
|
756
|
+
return await withRefreshLock(async () => {
|
|
757
|
+
const latest = await requestJson(fetcher, SESSION_PATH, "GET");
|
|
758
|
+
return isExactRefreshRequired(latest) ? await requestJson(fetcher, REFRESH_PATH, "POST") : latest;
|
|
759
|
+
}, withExclusiveLock);
|
|
760
|
+
}
|
|
761
|
+
async function signOutSuiteOidcBrowserSession(dependencies = {}) {
|
|
762
|
+
const fetcher = dependencies.fetch ?? globalThis.fetch;
|
|
763
|
+
const notifySignedOut = dependencies.notifySignedOut ?? broadcastSuiteOidcBrowserSignOut;
|
|
764
|
+
const withExclusiveLock = dependencies.withExclusiveLock;
|
|
765
|
+
await withRefreshLock(async () => {
|
|
766
|
+
const value = await requestJson(fetcher, SIGN_OUT_PATH, "POST");
|
|
767
|
+
if (!exactSignedOut(value)) {
|
|
768
|
+
throw new Error("The suite sign-out response was invalid.");
|
|
769
|
+
}
|
|
770
|
+
}, withExclusiveLock);
|
|
771
|
+
notifySignedOut();
|
|
772
|
+
}
|
|
773
|
+
function consumeSuiteOidcJustSignedIn() {
|
|
774
|
+
try {
|
|
775
|
+
if (typeof sessionStorage === "undefined" || sessionStorage.getItem(SUITE_OIDC_JUST_SIGNED_IN_STORAGE_KEY) === null) {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
sessionStorage.removeItem(SUITE_OIDC_JUST_SIGNED_IN_STORAGE_KEY);
|
|
779
|
+
return true;
|
|
780
|
+
} catch {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/convex-browser-auth-browser.ts
|
|
786
|
+
var MAXIMUM_TOKEN_RESPONSE_BYTES = 20 * 1024;
|
|
787
|
+
var TOKEN_RESPONSE_VERSION = "suite-convex-browser-token-response-v1";
|
|
788
|
+
function isRecord2(value) {
|
|
789
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
790
|
+
}
|
|
791
|
+
function exactKeys(value, keys) {
|
|
792
|
+
const actual = Object.keys(value);
|
|
793
|
+
return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
794
|
+
}
|
|
795
|
+
function compactJwt(value) {
|
|
796
|
+
return typeof value === "string" && value.length >= 32 && value.length <= 16384 && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/u.test(value);
|
|
797
|
+
}
|
|
798
|
+
async function readBoundedJson2(response) {
|
|
799
|
+
const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
800
|
+
const cacheControl = response.headers.get("cache-control")?.toLowerCase();
|
|
801
|
+
if (contentType !== "application/json" || cacheControl === undefined || !cacheControl.split(",").some((value) => value.trim() === "no-store") || response.body === null) {
|
|
802
|
+
throw new Error("The Convex token response was unsafe.");
|
|
803
|
+
}
|
|
804
|
+
const declaredLength = response.headers.get("content-length");
|
|
805
|
+
if (declaredLength !== null && (!/^(?:0|[1-9]\d*)$/u.test(declaredLength) || Number(declaredLength) > MAXIMUM_TOKEN_RESPONSE_BYTES)) {
|
|
806
|
+
await response.body.cancel().catch(() => {
|
|
807
|
+
return;
|
|
808
|
+
});
|
|
809
|
+
throw new Error("The Convex token response was too large.");
|
|
810
|
+
}
|
|
811
|
+
const reader = response.body.getReader();
|
|
812
|
+
const chunks = [];
|
|
813
|
+
let length = 0;
|
|
814
|
+
try {
|
|
815
|
+
while (true) {
|
|
816
|
+
const result = await reader.read();
|
|
817
|
+
if (result.done)
|
|
818
|
+
break;
|
|
819
|
+
length += result.value.byteLength;
|
|
820
|
+
if (length > MAXIMUM_TOKEN_RESPONSE_BYTES) {
|
|
821
|
+
await reader.cancel().catch(() => {
|
|
822
|
+
return;
|
|
823
|
+
});
|
|
824
|
+
throw new Error("The Convex token response was too large.");
|
|
825
|
+
}
|
|
826
|
+
chunks.push(result.value);
|
|
827
|
+
}
|
|
828
|
+
} finally {
|
|
829
|
+
reader.releaseLock();
|
|
830
|
+
}
|
|
831
|
+
const bytes = new Uint8Array(length);
|
|
832
|
+
let offset = 0;
|
|
833
|
+
for (const chunk of chunks) {
|
|
834
|
+
bytes.set(chunk, offset);
|
|
835
|
+
offset += chunk.byteLength;
|
|
836
|
+
}
|
|
837
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
838
|
+
}
|
|
839
|
+
function parseEndpointResult(value, status, nowMs) {
|
|
840
|
+
if (!isRecord2(value)) {
|
|
841
|
+
throw new Error("The Convex token response was invalid.");
|
|
842
|
+
}
|
|
843
|
+
if (status === 401 && exactKeys(value, ["kind"]) && value["kind"] === "signed_out") {
|
|
844
|
+
return { kind: "signed_out" };
|
|
845
|
+
}
|
|
846
|
+
if (status === 409 && exactKeys(value, ["kind"]) && value["kind"] === "refresh_required") {
|
|
847
|
+
return { kind: "refresh_required" };
|
|
848
|
+
}
|
|
849
|
+
if (status !== 200 || !exactKeys(value, ["expiresAtMs", "kind", "token", "version"]) || value["kind"] !== "token" || value["version"] !== TOKEN_RESPONSE_VERSION || !compactJwt(value["token"]) || typeof value["expiresAtMs"] !== "number" || !Number.isSafeInteger(value["expiresAtMs"]) || value["expiresAtMs"] <= nowMs || value["expiresAtMs"] > nowMs + 5 * 60000) {
|
|
850
|
+
throw new Error("The Convex token response was invalid.");
|
|
851
|
+
}
|
|
852
|
+
return {
|
|
853
|
+
expiresAtMs: value["expiresAtMs"],
|
|
854
|
+
kind: "token",
|
|
855
|
+
token: value["token"]
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
async function requestToken(fetcher, now) {
|
|
859
|
+
const response = await fetcher(SUITE_CONVEX_BROWSER_TOKEN_PATH, {
|
|
860
|
+
cache: "no-store",
|
|
861
|
+
credentials: "same-origin",
|
|
862
|
+
headers: { accept: "application/json" },
|
|
863
|
+
method: "POST",
|
|
864
|
+
redirect: "error"
|
|
865
|
+
});
|
|
866
|
+
return parseEndpointResult(await readBoundedJson2(response), response.status, now());
|
|
867
|
+
}
|
|
868
|
+
function createSuiteConvexBrowserTokenLoader(dependencies = {}) {
|
|
869
|
+
const fetcher = dependencies.fetch ?? globalThis.fetch;
|
|
870
|
+
const now = dependencies.now ?? Date.now;
|
|
871
|
+
const withExclusiveLock = dependencies.withExclusiveLock;
|
|
872
|
+
const refreshSuiteSession = dependencies.refreshSuiteSession ?? (() => loadSuiteOidcBrowserSession({
|
|
873
|
+
fetch: fetcher,
|
|
874
|
+
...withExclusiveLock === undefined ? {} : { withExclusiveLock }
|
|
875
|
+
}));
|
|
876
|
+
let cached = null;
|
|
877
|
+
let pending = null;
|
|
878
|
+
let custodyGeneration = 0;
|
|
879
|
+
async function exchange(generation) {
|
|
880
|
+
const first = await requestToken(fetcher, now);
|
|
881
|
+
const result = first.kind === "refresh_required" ? await (async () => {
|
|
882
|
+
await refreshSuiteSession();
|
|
883
|
+
return await requestToken(fetcher, now);
|
|
884
|
+
})() : first;
|
|
885
|
+
if (result.kind === "refresh_required") {
|
|
886
|
+
throw new Error("The suite session still requires refresh.");
|
|
887
|
+
}
|
|
888
|
+
if (result.kind === "signed_out") {
|
|
889
|
+
if (custodyGeneration === generation)
|
|
890
|
+
cached = null;
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
if (custodyGeneration !== generation)
|
|
894
|
+
return null;
|
|
895
|
+
cached = { expiresAtMs: result.expiresAtMs, token: result.token };
|
|
896
|
+
return result.token;
|
|
897
|
+
}
|
|
898
|
+
return Object.freeze({
|
|
899
|
+
clear() {
|
|
900
|
+
custodyGeneration += 1;
|
|
901
|
+
cached = null;
|
|
902
|
+
},
|
|
903
|
+
async fetchAccessToken() {
|
|
904
|
+
const nowMs = now();
|
|
905
|
+
if (cached !== null && cached.expiresAtMs - nowMs >= SUITE_CONVEX_BROWSER_PARENT_REFRESH_WINDOW_MS) {
|
|
906
|
+
return cached.token;
|
|
907
|
+
}
|
|
908
|
+
if (pending !== null)
|
|
909
|
+
return await pending;
|
|
910
|
+
const request = exchange(custodyGeneration);
|
|
911
|
+
pending = request;
|
|
912
|
+
try {
|
|
913
|
+
return await request;
|
|
914
|
+
} finally {
|
|
915
|
+
if (pending === request)
|
|
916
|
+
pending = null;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
export {
|
|
922
|
+
createSuiteConvexBrowserTokenLoader
|
|
923
|
+
};
|