@c1m-public/platform-auth 0.9.0 → 0.9.2
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/dist/access/index.d.ts +1 -153
- package/dist/access/index.js +307 -2
- package/dist/access/next.d.ts +2 -3
- package/dist/access/next.js +337 -7
- package/dist/index-BnchfjP_.d.ts +235 -0
- package/dist/index-CbsCWtku.d.ts +154 -0
- package/dist/index.d.ts +1 -230
- package/dist/index.js +534 -3
- package/dist/next.d.ts +2 -3
- package/dist/next.js +537 -8
- package/package.json +1 -1
- package/dist/chunk-FBC2NUH7.js +0 -83
- package/dist/chunk-Q7YI7RMG.js +0 -404
- package/dist/chunk-U67NBJND.js +0 -267
- package/dist/chunk-ZWSJVE5S.js +0 -52
- package/dist/internal-D6RafCm2.d.ts +0 -6
package/dist/chunk-Q7YI7RMG.js
DELETED
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
import { parseAuthResponse, hasTokenAudience, decodeTokenPayload, isTokenExpired, isTokenExpiringSoon } from './chunk-FBC2NUH7.js';
|
|
2
|
-
import { DEFAULT_FETCH_TIMEOUT_MS, forwardedRequestHeaders, timedFetch, resolveValue, resolveAll } from './chunk-ZWSJVE5S.js';
|
|
3
|
-
|
|
4
|
-
// src/return-path.ts
|
|
5
|
-
function safeInternalPath(raw) {
|
|
6
|
-
if (raw == null) return null;
|
|
7
|
-
const candidate = raw.trim();
|
|
8
|
-
if (candidate.length === 0) return null;
|
|
9
|
-
if (!candidate.startsWith("/")) return null;
|
|
10
|
-
if (candidate.startsWith("//") || candidate.includes("://")) return null;
|
|
11
|
-
if (/%(?:2f|5c)/i.test(candidate)) return null;
|
|
12
|
-
if (/[\x00-\x1f\x7f]/.test(candidate)) return null;
|
|
13
|
-
try {
|
|
14
|
-
const parsed = new URL(candidate, "https://internal.invalid");
|
|
15
|
-
if (parsed.origin !== "https://internal.invalid") return null;
|
|
16
|
-
const roundtrip = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
17
|
-
if (roundtrip !== candidate) return null;
|
|
18
|
-
return candidate;
|
|
19
|
-
} catch {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// src/cookies.ts
|
|
25
|
-
function readCookie(request, name) {
|
|
26
|
-
const header = request.headers.get("cookie");
|
|
27
|
-
if (!header) return null;
|
|
28
|
-
for (const part of header.split(";")) {
|
|
29
|
-
const separator = part.indexOf("=");
|
|
30
|
-
if (separator < 0) continue;
|
|
31
|
-
if (part.slice(0, separator).trim() !== name) continue;
|
|
32
|
-
const raw = part.slice(separator + 1).trim();
|
|
33
|
-
try {
|
|
34
|
-
return decodeURIComponent(raw);
|
|
35
|
-
} catch {
|
|
36
|
-
return raw;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
function serializeCookie(name, value, options = {}) {
|
|
42
|
-
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
43
|
-
parts.push(`Path=${options.path ?? "/"}`);
|
|
44
|
-
if (options.maxAge !== void 0) parts.push(`Max-Age=${Math.max(0, Math.floor(options.maxAge))}`);
|
|
45
|
-
if (options.httpOnly ?? true) parts.push("HttpOnly");
|
|
46
|
-
if (options.secure ?? name.startsWith("__Host-")) parts.push("Secure");
|
|
47
|
-
const sameSite = options.sameSite ?? "lax";
|
|
48
|
-
parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);
|
|
49
|
-
return parts.join("; ");
|
|
50
|
-
}
|
|
51
|
-
function setCookie(response, name, value, options) {
|
|
52
|
-
response.headers.append("Set-Cookie", serializeCookie(name, value, options));
|
|
53
|
-
return response;
|
|
54
|
-
}
|
|
55
|
-
function deleteCookie(response, name, options) {
|
|
56
|
-
response.headers.append("Set-Cookie", serializeCookie(name, "", { ...options, maxAge: 0 }));
|
|
57
|
-
return response;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// src/device-cookie.ts
|
|
61
|
-
var DEFAULT_DEVICE_MAX_AGE = 365 * 24 * 60 * 60;
|
|
62
|
-
function createDeviceIdentity(config) {
|
|
63
|
-
const maxAge = config.maxAgeSeconds ?? DEFAULT_DEVICE_MAX_AGE;
|
|
64
|
-
return {
|
|
65
|
-
read(request) {
|
|
66
|
-
const existing = readCookie(request, config.cookieName)?.trim();
|
|
67
|
-
return existing ? { value: existing, created: false } : { value: crypto.randomUUID(), created: true };
|
|
68
|
-
},
|
|
69
|
-
persist(response, identity) {
|
|
70
|
-
if (!identity.created) return response;
|
|
71
|
-
return setCookie(response, config.cookieName, identity.value, {
|
|
72
|
-
httpOnly: true,
|
|
73
|
-
secure: config.secure ?? true,
|
|
74
|
-
sameSite: "lax",
|
|
75
|
-
path: "/",
|
|
76
|
-
maxAge
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// src/handoff-errors.ts
|
|
83
|
-
var HANDOFF_ERROR_MESSAGES = {
|
|
84
|
-
// backend — handoff broker
|
|
85
|
-
handoff_unknown_app: "That application is not registered for sign-in handoff.",
|
|
86
|
-
handoff_not_configured: "Sign-in handoff is not configured for this application yet.",
|
|
87
|
-
handoff_source_not_allowed: "Your current session cannot open that application. Sign in there directly.",
|
|
88
|
-
handoff_invalid: "That sign-in handoff expired or was already used. Start again from where you came from.",
|
|
89
|
-
handoff_device_required: "The handoff could not identify this browser. Please try again.",
|
|
90
|
-
handoff_request_invalid: "The sign-in handoff request was malformed. Please try again.",
|
|
91
|
-
handoff_failed: "Sign-in handoff failed. Sign in with an email code instead.",
|
|
92
|
-
// backend — target admission (same codes the target's own login returns)
|
|
93
|
-
knowledge_library_access_required: "Your account has not been granted Knowledge Library access. Ask a platform admin to enable it.",
|
|
94
|
-
admin_center_access_required: "Your account has not been granted Admin Center access.",
|
|
95
|
-
platform_admin_surface_required: "Platform operator accounts sign in through the Platform Admin Center.",
|
|
96
|
-
account_disabled: "This account is disabled.",
|
|
97
|
-
// BFF — target callback
|
|
98
|
-
handoff_state_invalid: "Sign-in attempt expired \u2014 please try again.",
|
|
99
|
-
handoff_exchange_failed: "Sign-in could not be completed. Please try again.",
|
|
100
|
-
// BFF — source start
|
|
101
|
-
handoff_start_failed: "Could not start the sign-in handoff. Please try again.",
|
|
102
|
-
handoff_redirect_invalid: "The sign-in handoff returned an unexpected destination and was stopped.",
|
|
103
|
-
// BFF — surface bounces
|
|
104
|
-
admin_surface: "Platform operators sign in here; SaaSDash sent you to the Admin Center."
|
|
105
|
-
};
|
|
106
|
-
var HANDOFF_ERROR_FALLBACK = "Sign-in failed \u2014 please try again.";
|
|
107
|
-
var SSO_ERROR_MESSAGES = {
|
|
108
|
-
sso_cancelled: "Sign-in was cancelled.",
|
|
109
|
-
sso_denied: "Sign-in was refused by the provider.",
|
|
110
|
-
sso_state_invalid: "Sign-in attempt expired \u2014 please try again.",
|
|
111
|
-
sso_start_failed: "Could not start sign-in with that provider. Please try again.",
|
|
112
|
-
sso_complete_failed: "Sign-in could not be completed. Please try again.",
|
|
113
|
-
sso_unknown_operator: "No operator account matches that identity. Ask an admin for an invite.",
|
|
114
|
-
sso_provider_not_found: "That sign-in provider is not available.",
|
|
115
|
-
sso_not_configured: "That sign-in provider is not fully configured."
|
|
116
|
-
};
|
|
117
|
-
var AUTH_ERROR_MESSAGES = {
|
|
118
|
-
...HANDOFF_ERROR_MESSAGES,
|
|
119
|
-
...SSO_ERROR_MESSAGES
|
|
120
|
-
};
|
|
121
|
-
function handoffErrorMessage(code) {
|
|
122
|
-
if (!code) return HANDOFF_ERROR_FALLBACK;
|
|
123
|
-
return HANDOFF_ERROR_MESSAGES[code] ?? HANDOFF_ERROR_FALLBACK;
|
|
124
|
-
}
|
|
125
|
-
function isHandoffErrorCode(code) {
|
|
126
|
-
return !!code && Object.prototype.hasOwnProperty.call(HANDOFF_ERROR_MESSAGES, code);
|
|
127
|
-
}
|
|
128
|
-
function authErrorMessage(code, overrides) {
|
|
129
|
-
if (!code) return HANDOFF_ERROR_FALLBACK;
|
|
130
|
-
return overrides?.[code] ?? AUTH_ERROR_MESSAGES[code] ?? HANDOFF_ERROR_FALLBACK;
|
|
131
|
-
}
|
|
132
|
-
function isAuthErrorCode(code, overrides) {
|
|
133
|
-
if (!code) return false;
|
|
134
|
-
return Object.prototype.hasOwnProperty.call(AUTH_ERROR_MESSAGES, code) || !!overrides && Object.prototype.hasOwnProperty.call(overrides, code);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// src/handoff.ts
|
|
138
|
-
var HANDOFF_COOKIE_TTL_SECONDS = 120;
|
|
139
|
-
var CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
140
|
-
function sourceStartPath(app) {
|
|
141
|
-
return `/api/auth/handoffs/${encodeURIComponent(app)}/start`;
|
|
142
|
-
}
|
|
143
|
-
function brokerPaths(app) {
|
|
144
|
-
const key = encodeURIComponent(app);
|
|
145
|
-
return {
|
|
146
|
-
start: `/api/v1/platform/auth/handoffs/${key}/start`,
|
|
147
|
-
exchange: `/api/v1/platform/auth/handoffs/${key}/exchange`
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
function originOf(value) {
|
|
151
|
-
try {
|
|
152
|
-
return new URL(value).origin;
|
|
153
|
-
} catch {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
function base64Url(bytes) {
|
|
158
|
-
let binary = "";
|
|
159
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
160
|
-
return globalThis.btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
161
|
-
}
|
|
162
|
-
function randomToken(bytes) {
|
|
163
|
-
const raw = new Uint8Array(bytes);
|
|
164
|
-
crypto.getRandomValues(raw);
|
|
165
|
-
return base64Url(raw);
|
|
166
|
-
}
|
|
167
|
-
async function s256(verifier) {
|
|
168
|
-
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
169
|
-
return base64Url(new Uint8Array(digest));
|
|
170
|
-
}
|
|
171
|
-
function validState(state) {
|
|
172
|
-
return state.length >= 16 && state.length <= 256 && !/[\r\n]/.test(state);
|
|
173
|
-
}
|
|
174
|
-
function redirect(url, status = 302) {
|
|
175
|
-
return new Response(null, { status, headers: { Location: url.toString(), "Cache-Control": "no-store" } });
|
|
176
|
-
}
|
|
177
|
-
function loginErrorRedirect(origin, loginPath, code) {
|
|
178
|
-
const target = new URL(loginPath, origin);
|
|
179
|
-
target.searchParams.set("error", code);
|
|
180
|
-
return redirect(target);
|
|
181
|
-
}
|
|
182
|
-
function createHandoffTarget(config) {
|
|
183
|
-
const secure = config.secureCookies ?? true;
|
|
184
|
-
const prefix = `${secure ? "__Host-" : ""}${config.cookiePrefix}_handoff_`;
|
|
185
|
-
const cookies = { state: `${prefix}state`, verifier: `${prefix}verifier`, from: `${prefix}from` };
|
|
186
|
-
const cookieOptions = { httpOnly: true, secure, sameSite: "lax", path: "/", maxAge: HANDOFF_COOKIE_TTL_SECONDS };
|
|
187
|
-
const timeout = config.timeoutMs ?? config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
188
|
-
const clearHandoffCookies = (response) => {
|
|
189
|
-
for (const name of Object.values(cookies)) deleteCookie(response, name, { path: "/", secure });
|
|
190
|
-
return response;
|
|
191
|
-
};
|
|
192
|
-
const fail = (origin, code) => clearHandoffCookies(loginErrorRedirect(origin, config.loginPath, code));
|
|
193
|
-
return {
|
|
194
|
-
cookies,
|
|
195
|
-
/** GET `?source=&from=` */
|
|
196
|
-
async start(request) {
|
|
197
|
-
const url = new URL(request.url);
|
|
198
|
-
const sourceKey = url.searchParams.get("source")?.trim() || config.defaultSource;
|
|
199
|
-
const source = config.sources[sourceKey];
|
|
200
|
-
if (!source) return fail(url.origin, "handoff_request_invalid");
|
|
201
|
-
const sourceOrigin = originOf(resolveValue(source));
|
|
202
|
-
if (!sourceOrigin) return fail(url.origin, "handoff_not_configured");
|
|
203
|
-
const verifier = randomToken(48);
|
|
204
|
-
const state = randomToken(32);
|
|
205
|
-
const start = new URL(sourceStartPath(config.app), sourceOrigin);
|
|
206
|
-
start.searchParams.set("code_challenge", await s256(verifier));
|
|
207
|
-
start.searchParams.set("state", state);
|
|
208
|
-
const response = redirect(start);
|
|
209
|
-
setCookie(response, cookies.verifier, verifier, cookieOptions);
|
|
210
|
-
setCookie(response, cookies.state, state, cookieOptions);
|
|
211
|
-
const from = safeInternalPath(url.searchParams.get("from"));
|
|
212
|
-
if (from) setCookie(response, cookies.from, from, cookieOptions);
|
|
213
|
-
return response;
|
|
214
|
-
},
|
|
215
|
-
/** GET `?code=&state=` */
|
|
216
|
-
async callback(request) {
|
|
217
|
-
const url = new URL(request.url);
|
|
218
|
-
const code = url.searchParams.get("code") ?? "";
|
|
219
|
-
const state = url.searchParams.get("state") ?? "";
|
|
220
|
-
const expectedState = readCookie(request, cookies.state) ?? "";
|
|
221
|
-
const verifier = readCookie(request, cookies.verifier) ?? "";
|
|
222
|
-
if (!code || !state || !expectedState || state !== expectedState || !verifier) {
|
|
223
|
-
return fail(url.origin, "handoff_state_invalid");
|
|
224
|
-
}
|
|
225
|
-
const device = config.device.read(request);
|
|
226
|
-
const respond = (response) => config.device.persist(response, device);
|
|
227
|
-
try {
|
|
228
|
-
const headers = forwardedRequestHeaders(request, {
|
|
229
|
-
"Content-Type": "application/json",
|
|
230
|
-
"X-Client-Type": config.clientType,
|
|
231
|
-
"X-Client-Device-ID": device.value
|
|
232
|
-
});
|
|
233
|
-
const response = await timedFetch(`${resolveValue(config.apiUrl)}${brokerPaths(config.app).exchange}`, { init: { method: "POST", headers, body: JSON.stringify({ code, code_verifier: verifier }) }, timeoutMs: timeout, fetch: config.fetch });
|
|
234
|
-
const payload = await response.json().catch(() => ({}));
|
|
235
|
-
if (!response.ok) {
|
|
236
|
-
const backendCode = payload?.code;
|
|
237
|
-
return respond(fail(url.origin, typeof backendCode === "string" ? backendCode : "handoff_exchange_failed"));
|
|
238
|
-
}
|
|
239
|
-
const session = parseAuthResponse(payload);
|
|
240
|
-
if (!session || !hasTokenAudience(session.access_token, config.audience)) {
|
|
241
|
-
return respond(fail(url.origin, "handoff_exchange_failed"));
|
|
242
|
-
}
|
|
243
|
-
const from = safeInternalPath(readCookie(request, cookies.from)) ?? config.defaultReturnPath;
|
|
244
|
-
const established = await config.establishSession(session, redirect(new URL(from, url.origin)), request) ?? redirect(new URL(from, url.origin));
|
|
245
|
-
return respond(clearHandoffCookies(established));
|
|
246
|
-
} catch {
|
|
247
|
-
return respond(fail(url.origin, "handoff_exchange_failed"));
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
var DEFAULT_TARGET_CALLBACK = "/api/auth/platform-sso/callback";
|
|
253
|
-
var DEFAULT_TARGET_LOGIN = "/login";
|
|
254
|
-
function createHandoffSource(config) {
|
|
255
|
-
const timeout = config.timeoutMs ?? config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
256
|
-
return {
|
|
257
|
-
/** GET `/api/auth/handoffs/{app}/start?code_challenge=&state=` */
|
|
258
|
-
async start(request, app) {
|
|
259
|
-
const target = config.targets[app];
|
|
260
|
-
if (!target) return new Response("unknown handoff application", { status: 404, headers: { "Cache-Control": "no-store" } });
|
|
261
|
-
const targetOrigin = originOf(resolveValue(target.origin));
|
|
262
|
-
const targetLogin = target.loginPath ?? DEFAULT_TARGET_LOGIN;
|
|
263
|
-
if (!targetOrigin) return new Response("handoff target not configured", { status: 503, headers: { "Cache-Control": "no-store" } });
|
|
264
|
-
const targetError = (code) => loginErrorRedirect(targetOrigin, targetLogin, code);
|
|
265
|
-
const url = new URL(request.url);
|
|
266
|
-
const challenge = url.searchParams.get("code_challenge")?.trim() ?? "";
|
|
267
|
-
const state = url.searchParams.get("state")?.trim() ?? "";
|
|
268
|
-
if (!CHALLENGE_PATTERN.test(challenge) || !validState(state)) return targetError("handoff_request_invalid");
|
|
269
|
-
const sourceLogin = () => {
|
|
270
|
-
const login = new URL(config.loginPath, url.origin);
|
|
271
|
-
login.searchParams.set("from", `${url.pathname}${url.search}`);
|
|
272
|
-
return redirect(login);
|
|
273
|
-
};
|
|
274
|
-
const token = await config.getAccessToken(request);
|
|
275
|
-
if (!token) return sourceLogin();
|
|
276
|
-
try {
|
|
277
|
-
const headers = forwardedRequestHeaders(request, {
|
|
278
|
-
"Content-Type": "application/json",
|
|
279
|
-
"X-Client-Type": config.clientType,
|
|
280
|
-
Authorization: `Bearer ${token}`
|
|
281
|
-
});
|
|
282
|
-
const response = await timedFetch(`${resolveValue(config.apiUrl)}${brokerPaths(app).start}`, { init: { method: "POST", headers, body: JSON.stringify({ code_challenge: challenge, state }) }, timeoutMs: timeout, fetch: config.fetch });
|
|
283
|
-
if (response.status === 401) return sourceLogin();
|
|
284
|
-
const payload = await response.json().catch(() => ({}));
|
|
285
|
-
if (!response.ok) {
|
|
286
|
-
const backendCode = payload?.code;
|
|
287
|
-
return targetError(typeof backendCode === "string" ? backendCode : "handoff_start_failed");
|
|
288
|
-
}
|
|
289
|
-
const redirectUrl = payload?.redirect_url;
|
|
290
|
-
if (typeof redirectUrl !== "string") return targetError("handoff_start_failed");
|
|
291
|
-
let destination;
|
|
292
|
-
try {
|
|
293
|
-
destination = new URL(redirectUrl);
|
|
294
|
-
} catch {
|
|
295
|
-
return targetError("handoff_redirect_invalid");
|
|
296
|
-
}
|
|
297
|
-
if (destination.origin !== targetOrigin || destination.pathname !== (target.callbackPath ?? DEFAULT_TARGET_CALLBACK)) {
|
|
298
|
-
return targetError("handoff_redirect_invalid");
|
|
299
|
-
}
|
|
300
|
-
return redirect(destination);
|
|
301
|
-
} catch {
|
|
302
|
-
return targetError("handoff_start_failed");
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// src/session-refresh.ts
|
|
309
|
-
var DEFAULT_TTL_MS = 5e3;
|
|
310
|
-
var DEFAULT_MAX = 1e3;
|
|
311
|
-
function parseRefreshEnvelope(value) {
|
|
312
|
-
if (!value || typeof value !== "object") return null;
|
|
313
|
-
const outer = value;
|
|
314
|
-
const payload = outer.data && typeof outer.data === "object" ? outer.data : value;
|
|
315
|
-
if (typeof payload.access_token !== "string" || !payload.access_token) return null;
|
|
316
|
-
return {
|
|
317
|
-
accessToken: payload.access_token,
|
|
318
|
-
refreshToken: typeof payload.refresh_token === "string" && payload.refresh_token ? payload.refresh_token : void 0
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
function createSessionRefresher(config) {
|
|
322
|
-
const ttl = config.singleflightTtlMs ?? DEFAULT_TTL_MS;
|
|
323
|
-
const max = config.maxSingleflights ?? DEFAULT_MAX;
|
|
324
|
-
const timeoutMs = config.timeoutMs ?? config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
325
|
-
const inflight = /* @__PURE__ */ new Map();
|
|
326
|
-
async function perform(refreshToken) {
|
|
327
|
-
try {
|
|
328
|
-
const pending = resolveAll(config.apiUrl, config.headers);
|
|
329
|
-
const [origin, extra] = pending instanceof Promise ? await pending : pending;
|
|
330
|
-
const headers = new Headers(extra);
|
|
331
|
-
headers.set("Content-Type", "application/json");
|
|
332
|
-
headers.set("X-Client-Type", config.clientType);
|
|
333
|
-
const response = await timedFetch(`${origin}${config.refreshPath}`, {
|
|
334
|
-
init: {
|
|
335
|
-
method: "POST",
|
|
336
|
-
headers,
|
|
337
|
-
body: JSON.stringify({ refresh_token: refreshToken })
|
|
338
|
-
},
|
|
339
|
-
timeoutMs,
|
|
340
|
-
fetch: config.fetch
|
|
341
|
-
});
|
|
342
|
-
if (!response.ok) return null;
|
|
343
|
-
const refreshed = parseRefreshEnvelope(await response.json());
|
|
344
|
-
return refreshed && hasTokenAudience(refreshed.accessToken, config.audience) ? refreshed : null;
|
|
345
|
-
} catch {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
function prune(now) {
|
|
350
|
-
for (const [token, entry] of inflight) {
|
|
351
|
-
if (entry.expiresAt <= now) inflight.delete(token);
|
|
352
|
-
}
|
|
353
|
-
while (inflight.size >= max) {
|
|
354
|
-
const oldest = inflight.keys().next().value;
|
|
355
|
-
if (typeof oldest !== "string") break;
|
|
356
|
-
inflight.delete(oldest);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
return {
|
|
360
|
-
refresh(refreshToken) {
|
|
361
|
-
const now = Date.now();
|
|
362
|
-
prune(now);
|
|
363
|
-
const existing = inflight.get(refreshToken);
|
|
364
|
-
if (existing && existing.expiresAt > now) return existing.promise;
|
|
365
|
-
const promise = perform(refreshToken).then((result) => {
|
|
366
|
-
if (!result) inflight.delete(refreshToken);
|
|
367
|
-
return result;
|
|
368
|
-
});
|
|
369
|
-
inflight.set(refreshToken, { expiresAt: now + ttl, promise });
|
|
370
|
-
return promise;
|
|
371
|
-
},
|
|
372
|
-
size() {
|
|
373
|
-
return inflight.size;
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// src/session-gate.ts
|
|
379
|
-
function hasSessionShape(token) {
|
|
380
|
-
if (!token) return false;
|
|
381
|
-
const payload = decodeTokenPayload(token);
|
|
382
|
-
return Boolean(payload?.user_id || payload?.actor_id || payload?.customer_id || payload?.sub);
|
|
383
|
-
}
|
|
384
|
-
async function evaluateSessionGate(input) {
|
|
385
|
-
const { accessToken, refreshToken, audience, refresher, isPublic, isLoginPath, isApiPath } = input;
|
|
386
|
-
const live = !!accessToken && hasSessionShape(accessToken) && hasTokenAudience(accessToken, audience) && !isTokenExpired(accessToken);
|
|
387
|
-
if (live) {
|
|
388
|
-
if (isPublic && isLoginPath) return { kind: "redirect_home" };
|
|
389
|
-
if (refreshToken && isTokenExpiringSoon(accessToken, input.expiringSoonBufferSeconds)) {
|
|
390
|
-
const refreshed = await refresher.refresh(refreshToken);
|
|
391
|
-
if (refreshed) return { kind: "pass", refreshed };
|
|
392
|
-
}
|
|
393
|
-
return { kind: "pass" };
|
|
394
|
-
}
|
|
395
|
-
if (refreshToken) {
|
|
396
|
-
const refreshed = await refresher.refresh(refreshToken);
|
|
397
|
-
if (refreshed) return isPublic ? { kind: "redirect_home", refreshed } : { kind: "pass", refreshed };
|
|
398
|
-
}
|
|
399
|
-
if (isPublic) return { kind: "anonymous_public" };
|
|
400
|
-
if (isApiPath) return { kind: "unauthorized_api" };
|
|
401
|
-
return { kind: "redirect_login" };
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
export { AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, SSO_ERROR_MESSAGES, authErrorMessage, brokerPaths, createDeviceIdentity, createHandoffSource, createHandoffTarget, createSessionRefresher, deleteCookie, evaluateSessionGate, handoffErrorMessage, hasSessionShape, isAuthErrorCode, isHandoffErrorCode, parseRefreshEnvelope, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath };
|
package/dist/chunk-U67NBJND.js
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import { resolveValue, forwardedRequestHeaders, timedFetch } from './chunk-ZWSJVE5S.js';
|
|
2
|
-
|
|
3
|
-
// src/access/generated/keys.ts
|
|
4
|
-
var PLATFORM_PERMISSIONS = [
|
|
5
|
-
"affiliates.finance.manage",
|
|
6
|
-
"affiliates.partners.manage",
|
|
7
|
-
"affiliates.programmes.manage",
|
|
8
|
-
"affiliates.view",
|
|
9
|
-
"linkservice.domains.manage",
|
|
10
|
-
"linkservice.links.manage",
|
|
11
|
-
"linkservice.qr.manage",
|
|
12
|
-
"linkservice.view",
|
|
13
|
-
"platform.admin.view",
|
|
14
|
-
"platform.audit.view",
|
|
15
|
-
"platform.auth.manage",
|
|
16
|
-
"platform.billing.manage",
|
|
17
|
-
"platform.channels.manage",
|
|
18
|
-
"platform.channels.view",
|
|
19
|
-
"platform.config.manage",
|
|
20
|
-
"platform.docs.manage",
|
|
21
|
-
"platform.docs.view",
|
|
22
|
-
"platform.observability.manage",
|
|
23
|
-
"platform.observability.view",
|
|
24
|
-
"platform.offboarding.view",
|
|
25
|
-
"platform.ops.manage",
|
|
26
|
-
"platform.support.impersonate",
|
|
27
|
-
"platform.support.sessions",
|
|
28
|
-
"platform.tenants.archive",
|
|
29
|
-
"platform.tenants.manage",
|
|
30
|
-
"platform.tenants.restore",
|
|
31
|
-
"platform.users.deactivate",
|
|
32
|
-
"platform.users.delete",
|
|
33
|
-
"platform.users.invite",
|
|
34
|
-
"platform.users.manage_access",
|
|
35
|
-
"platform.users.reactivate",
|
|
36
|
-
"platform.users.view"
|
|
37
|
-
];
|
|
38
|
-
var DASH_PERMISSIONS = [
|
|
39
|
-
"dash.apps.manage",
|
|
40
|
-
"dash.billing.manage",
|
|
41
|
-
"dash.channels.manage",
|
|
42
|
-
"dash.customers.manage",
|
|
43
|
-
"dash.domains.manage",
|
|
44
|
-
"dash.members.manage",
|
|
45
|
-
"dash.ownership.transfer",
|
|
46
|
-
"dash.plans.manage",
|
|
47
|
-
"dash.publish",
|
|
48
|
-
"dash.settings.manage",
|
|
49
|
-
"dash.workspaces.admin",
|
|
50
|
-
"dash.workspaces.manage"
|
|
51
|
-
];
|
|
52
|
-
var DASH_ROLES = [
|
|
53
|
-
"dash.admin",
|
|
54
|
-
"dash.owner"
|
|
55
|
-
];
|
|
56
|
-
var LEADBROKER_PERMISSIONS = [
|
|
57
|
-
"leadbroker.finance.manage",
|
|
58
|
-
"leadbroker.leads.dispatch",
|
|
59
|
-
"leadbroker.leads.review",
|
|
60
|
-
"leadbroker.leads.view",
|
|
61
|
-
"leadbroker.policy.manage"
|
|
62
|
-
];
|
|
63
|
-
var WORKSPACE_PERMISSIONS = [
|
|
64
|
-
"workspace.channels.manage",
|
|
65
|
-
"workspace.channels.secrets",
|
|
66
|
-
"workspace.channels.view",
|
|
67
|
-
"workspace.crm.manage",
|
|
68
|
-
"workspace.crm.view",
|
|
69
|
-
"workspace.customers.manage",
|
|
70
|
-
"workspace.customers.view",
|
|
71
|
-
"workspace.orders.manage",
|
|
72
|
-
"workspace.orders.view",
|
|
73
|
-
"workspace.payments.record",
|
|
74
|
-
"workspace.site.edit",
|
|
75
|
-
"workspace.site.view",
|
|
76
|
-
"workspace.staff.manage"
|
|
77
|
-
];
|
|
78
|
-
var WORKSPACE_ROLES = [
|
|
79
|
-
"workspace.editor",
|
|
80
|
-
"workspace.frontdesk",
|
|
81
|
-
"workspace.manager"
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
// src/access/generated/stages.ts
|
|
85
|
-
var PIPELINE_STAGES = ["created", "awaiting_payment", "provisioning", "attention", "active"];
|
|
86
|
-
function isPipelineStage(value) {
|
|
87
|
-
return typeof value === "string" && PIPELINE_STAGES.includes(value);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// src/access/errors.ts
|
|
91
|
-
var AccessSnapshotError = class extends Error {
|
|
92
|
-
code;
|
|
93
|
-
status;
|
|
94
|
-
constructor(code, message, status) {
|
|
95
|
-
super(message);
|
|
96
|
-
this.name = "AccessSnapshotError";
|
|
97
|
-
this.code = code;
|
|
98
|
-
this.status = status;
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
function isAccessSnapshotError(error, code) {
|
|
102
|
-
return error instanceof AccessSnapshotError && (code === void 0 || error.code === code);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// src/access/snapshot.ts
|
|
106
|
-
var SCOPE_TYPES = /* @__PURE__ */ new Set(["platform", "saas_app", "workspace"]);
|
|
107
|
-
function emptySnapshot(subjectId = "") {
|
|
108
|
-
return {
|
|
109
|
-
subject_id: subjectId,
|
|
110
|
-
access_version: 0,
|
|
111
|
-
is_owner: false,
|
|
112
|
-
active: false,
|
|
113
|
-
platform_role: "",
|
|
114
|
-
permissions: [],
|
|
115
|
-
dash_roles: {},
|
|
116
|
-
workspace_permissions: {},
|
|
117
|
-
workspace_roles: {}
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
function isRecord(value) {
|
|
121
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
122
|
-
}
|
|
123
|
-
function fail(field, why) {
|
|
124
|
-
throw new AccessSnapshotError("invalid_snapshot", `access snapshot: ${field} ${why}`);
|
|
125
|
-
}
|
|
126
|
-
function parseAccessSnapshot(input) {
|
|
127
|
-
if (!isRecord(input)) fail("payload", "is not an object");
|
|
128
|
-
const { subject_id, access_version, is_owner, active, platform_role, permissions, dash_roles, workspace_permissions, workspace_roles } = input;
|
|
129
|
-
if (typeof subject_id !== "string" || subject_id === "") fail("subject_id", "must be a non-empty string");
|
|
130
|
-
if (typeof access_version !== "number" || !Number.isInteger(access_version) || access_version < 0) fail("access_version", "must be a non-negative integer");
|
|
131
|
-
if (typeof is_owner !== "boolean") fail("is_owner", "must be a boolean");
|
|
132
|
-
if (typeof active !== "boolean") fail("active", "must be a boolean");
|
|
133
|
-
if (platform_role !== void 0 && platform_role !== null && typeof platform_role !== "string") fail("platform_role", "must be a string");
|
|
134
|
-
if (!Array.isArray(permissions)) fail("permissions", "must be an array");
|
|
135
|
-
const entries = permissions.map((raw, i) => {
|
|
136
|
-
if (!isRecord(raw)) fail(`permissions[${i}]`, "is not an object");
|
|
137
|
-
const { key, scope_type, scope_id } = raw;
|
|
138
|
-
if (typeof key !== "string" || key === "") fail(`permissions[${i}].key`, "must be a non-empty string");
|
|
139
|
-
if (typeof scope_type !== "string" || !SCOPE_TYPES.has(scope_type)) fail(`permissions[${i}].scope_type`, "must be platform|saas_app|workspace");
|
|
140
|
-
if (scope_id !== void 0 && scope_id !== null && typeof scope_id !== "string") fail(`permissions[${i}].scope_id`, "must be a string");
|
|
141
|
-
return { key, scope_type, scope_id: typeof scope_id === "string" ? scope_id : "" };
|
|
142
|
-
});
|
|
143
|
-
const dashRoles = {};
|
|
144
|
-
if (dash_roles !== void 0 && dash_roles !== null) {
|
|
145
|
-
if (!isRecord(dash_roles)) fail("dash_roles", "must be an object");
|
|
146
|
-
for (const [dash, role] of Object.entries(dash_roles)) {
|
|
147
|
-
if (typeof role !== "string") fail(`dash_roles[${dash}]`, "must be a string");
|
|
148
|
-
dashRoles[dash] = role;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const wsPerms = {};
|
|
152
|
-
if (workspace_permissions !== void 0 && workspace_permissions !== null) {
|
|
153
|
-
if (!isRecord(workspace_permissions)) fail("workspace_permissions", "must be an object");
|
|
154
|
-
for (const [ws, keys] of Object.entries(workspace_permissions)) {
|
|
155
|
-
if (!Array.isArray(keys) || keys.some((k) => typeof k !== "string")) fail(`workspace_permissions[${ws}]`, "must be a string array");
|
|
156
|
-
wsPerms[ws] = [...keys];
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const wsRoles = {};
|
|
160
|
-
if (workspace_roles !== void 0 && workspace_roles !== null) {
|
|
161
|
-
if (!isRecord(workspace_roles)) fail("workspace_roles", "must be an object");
|
|
162
|
-
for (const [ws, role] of Object.entries(workspace_roles)) {
|
|
163
|
-
if (typeof role !== "string") fail(`workspace_roles[${ws}]`, "must be a string");
|
|
164
|
-
wsRoles[ws] = role;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return {
|
|
168
|
-
subject_id,
|
|
169
|
-
access_version,
|
|
170
|
-
is_owner,
|
|
171
|
-
active,
|
|
172
|
-
platform_role: typeof platform_role === "string" ? platform_role : "",
|
|
173
|
-
permissions: entries,
|
|
174
|
-
dash_roles: dashRoles,
|
|
175
|
-
workspace_permissions: wsPerms,
|
|
176
|
-
workspace_roles: wsRoles
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// src/access/predicates.ts
|
|
181
|
-
function entryCoversApp(entry, saasApp) {
|
|
182
|
-
switch (entry.scope_type) {
|
|
183
|
-
case "platform":
|
|
184
|
-
return true;
|
|
185
|
-
case "saas_app":
|
|
186
|
-
if (saasApp === void 0) return true;
|
|
187
|
-
return entry.scope_id === "*" || entry.scope_id.toLowerCase() === saasApp.toLowerCase();
|
|
188
|
-
default:
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
function hasPermission(snapshot, key, opts = {}) {
|
|
193
|
-
if (!snapshot.active) return false;
|
|
194
|
-
return snapshot.permissions.some((entry) => entry.key === key && entryCoversApp(entry, opts.saasApp));
|
|
195
|
-
}
|
|
196
|
-
function hasAnyPermission(snapshot, keys, opts = {}) {
|
|
197
|
-
return keys.some((key) => hasPermission(snapshot, key, opts));
|
|
198
|
-
}
|
|
199
|
-
function hasAllPermissions(snapshot, keys, opts = {}) {
|
|
200
|
-
return keys.every((key) => hasPermission(snapshot, key, opts));
|
|
201
|
-
}
|
|
202
|
-
function hasWorkspacePermission(snapshot, workspaceId, key) {
|
|
203
|
-
if (!snapshot.active) return false;
|
|
204
|
-
if (snapshot.is_owner) return true;
|
|
205
|
-
return (snapshot.workspace_permissions[workspaceId] ?? []).includes(key);
|
|
206
|
-
}
|
|
207
|
-
function managedDashes(snapshot) {
|
|
208
|
-
if (!snapshot.active) return [];
|
|
209
|
-
return Object.entries(snapshot.dash_roles).map(([saasApp, role]) => ({ saasApp, role })).sort((a, b) => a.saasApp.localeCompare(b.saasApp));
|
|
210
|
-
}
|
|
211
|
-
function dashRole(snapshot, saasApp) {
|
|
212
|
-
if (!snapshot.active) return "";
|
|
213
|
-
return snapshot.dash_roles[saasApp] ?? "";
|
|
214
|
-
}
|
|
215
|
-
function isPlatformOwner(snapshot) {
|
|
216
|
-
return snapshot.active && snapshot.is_owner;
|
|
217
|
-
}
|
|
218
|
-
function appScopes(snapshot, key) {
|
|
219
|
-
if (!snapshot.active) return { all: false, apps: [] };
|
|
220
|
-
const apps = /* @__PURE__ */ new Set();
|
|
221
|
-
for (const entry of snapshot.permissions) {
|
|
222
|
-
if (entry.key !== key) continue;
|
|
223
|
-
if (entry.scope_type === "platform") return { all: true, apps: [] };
|
|
224
|
-
if (entry.scope_type === "saas_app") {
|
|
225
|
-
if (entry.scope_id === "*") return { all: true, apps: [] };
|
|
226
|
-
apps.add(entry.scope_id.toLowerCase());
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return { all: false, apps: [...apps].sort() };
|
|
230
|
-
}
|
|
231
|
-
function permissionsFor(snapshot, opts = {}) {
|
|
232
|
-
if (!snapshot.active) return [];
|
|
233
|
-
const keys = /* @__PURE__ */ new Set();
|
|
234
|
-
for (const entry of snapshot.permissions) if (entryCoversApp(entry, opts.saasApp)) keys.add(entry.key);
|
|
235
|
-
return [...keys].sort();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// src/access/fetch.ts
|
|
239
|
-
var DEFAULT_ACCESS_ENDPOINT_PATH = "/api/v1/platform/access";
|
|
240
|
-
async function fetchAccessSnapshot(opts) {
|
|
241
|
-
const url = `${resolveValue(opts.apiUrl).replace(/\/$/, "")}${opts.endpointPath ?? DEFAULT_ACCESS_ENDPOINT_PATH}`;
|
|
242
|
-
const headers = opts.request ? forwardedRequestHeaders(opts.request, opts.headers) : new Headers(opts.headers);
|
|
243
|
-
headers.set("Authorization", `Bearer ${opts.accessToken}`);
|
|
244
|
-
headers.set("Accept", "application/json");
|
|
245
|
-
let response;
|
|
246
|
-
try {
|
|
247
|
-
response = await timedFetch(url, {
|
|
248
|
-
init: { method: "GET", headers },
|
|
249
|
-
timeoutMs: opts.fetchTimeoutMs,
|
|
250
|
-
fetchImpl: opts.fetchImpl
|
|
251
|
-
});
|
|
252
|
-
} catch (error) {
|
|
253
|
-
throw new AccessSnapshotError("unavailable", `access snapshot request failed: ${error.message}`);
|
|
254
|
-
}
|
|
255
|
-
if (response.status === 401) throw new AccessSnapshotError("unauthenticated", "session expired", 401);
|
|
256
|
-
if (response.status === 403) throw new AccessSnapshotError("forbidden", "access snapshot refused", 403);
|
|
257
|
-
if (!response.ok) throw new AccessSnapshotError("unavailable", `access snapshot returned ${response.status}`, response.status);
|
|
258
|
-
let body;
|
|
259
|
-
try {
|
|
260
|
-
body = await response.json();
|
|
261
|
-
} catch {
|
|
262
|
-
throw new AccessSnapshotError("invalid_snapshot", "access snapshot body is not JSON", response.status);
|
|
263
|
-
}
|
|
264
|
-
return parseAccessSnapshot(body);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export { AccessSnapshotError, DASH_PERMISSIONS, DASH_ROLES, DEFAULT_ACCESS_ENDPOINT_PATH, LEADBROKER_PERMISSIONS, PIPELINE_STAGES, PLATFORM_PERMISSIONS, WORKSPACE_PERMISSIONS, WORKSPACE_ROLES, appScopes, dashRole, emptySnapshot, entryCoversApp, fetchAccessSnapshot, hasAllPermissions, hasAnyPermission, hasPermission, hasWorkspacePermission, isAccessSnapshotError, isPipelineStage, isPlatformOwner, managedDashes, parseAccessSnapshot, permissionsFor };
|