@c1m-public/platform-auth 0.15.1 → 0.17.0
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 -1
- package/dist/access/index.js +4 -0
- package/dist/access/next.d.ts +2 -2
- package/dist/access/next.js +4 -0
- package/dist/{index-aKe58m6n.d.ts → index-Blx3FNX2.d.ts} +43 -2
- package/dist/{index-BEiQTDX9.d.ts → index-BoZch6LF.d.ts} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +49 -18
- package/dist/next.d.ts +2 -2
- package/dist/next.js +49 -18
- package/dist/service-accounts.js +27 -25
- package/package.json +3 -2
package/dist/access/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, A as AccessSnapshot, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, P as PermissionCheckOptions, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-
|
|
1
|
+
export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, A as AccessSnapshot, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, P as PermissionCheckOptions, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-BoZch6LF.js';
|
package/dist/access/index.js
CHANGED
|
@@ -17,7 +17,11 @@ var PLATFORM_PERMISSIONS = [
|
|
|
17
17
|
"platform.channels.manage",
|
|
18
18
|
"platform.channels.view",
|
|
19
19
|
"platform.config.manage",
|
|
20
|
+
"platform.dashes.archive",
|
|
21
|
+
"platform.dashes.manage",
|
|
20
22
|
"platform.dashes.purge",
|
|
23
|
+
"platform.dashes.restore",
|
|
24
|
+
"platform.dashes.view",
|
|
21
25
|
"platform.docs.manage",
|
|
22
26
|
"platform.docs.view",
|
|
23
27
|
"platform.mcp.access",
|
package/dist/access/next.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as AccessSnapshot, P as PermissionCheckOptions, R as Resolvable } from '../index-
|
|
2
|
-
export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-
|
|
1
|
+
import { A as AccessSnapshot, P as PermissionCheckOptions, R as Resolvable } from '../index-BoZch6LF.js';
|
|
2
|
+
export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-BoZch6LF.js';
|
|
3
3
|
|
|
4
4
|
/** Default cache tag for one subject's snapshot: `access:<subjectId>`. */
|
|
5
5
|
declare function accessCacheTag(subjectId: string): string;
|
package/dist/access/next.js
CHANGED
|
@@ -275,7 +275,11 @@ var PLATFORM_PERMISSIONS = [
|
|
|
275
275
|
"platform.channels.manage",
|
|
276
276
|
"platform.channels.view",
|
|
277
277
|
"platform.config.manage",
|
|
278
|
+
"platform.dashes.archive",
|
|
279
|
+
"platform.dashes.manage",
|
|
278
280
|
"platform.dashes.purge",
|
|
281
|
+
"platform.dashes.restore",
|
|
282
|
+
"platform.dashes.view",
|
|
279
283
|
"platform.docs.manage",
|
|
280
284
|
"platform.docs.view",
|
|
281
285
|
"platform.mcp.access",
|
|
@@ -178,6 +178,29 @@ interface RefreshedSession {
|
|
|
178
178
|
accessToken: string;
|
|
179
179
|
refreshToken?: string;
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Why a refresh did not produce a session.
|
|
183
|
+
*
|
|
184
|
+
* `rejected` and `unavailable` must never be merged. A caller acts on them
|
|
185
|
+
* oppositely: a rejected token means clear the cookies and send the user to
|
|
186
|
+
* log in; an unreachable backend means keep the cookies and say "try again".
|
|
187
|
+
* Collapsing them is what made a backend outage sign operators out of
|
|
188
|
+
* SaaSDash — and because the cookies were cleared, they stayed out after the
|
|
189
|
+
* backend came back. `refresh()` cannot express this (a truthy failure object
|
|
190
|
+
* would break every `if (refreshed)` caller), so the distinction lives here.
|
|
191
|
+
*/
|
|
192
|
+
type RefreshOutcome = {
|
|
193
|
+
status: "refreshed";
|
|
194
|
+
session: RefreshedSession;
|
|
195
|
+
}
|
|
196
|
+
/** The backend answered: this token is no good. Definitive. */
|
|
197
|
+
| {
|
|
198
|
+
status: "rejected";
|
|
199
|
+
}
|
|
200
|
+
/** We could not ask, or could not trust the answer. Says nothing about the token. */
|
|
201
|
+
| {
|
|
202
|
+
status: "unavailable";
|
|
203
|
+
};
|
|
181
204
|
interface SessionRefresherConfig {
|
|
182
205
|
/** Backend origin, no /api/v1. May resolve asynchronously (a tenant-scoped origin). */
|
|
183
206
|
apiUrl: AsyncResolvable<string>;
|
|
@@ -202,7 +225,17 @@ interface SessionRefresherConfig {
|
|
|
202
225
|
*/
|
|
203
226
|
declare function parseRefreshEnvelope(value: unknown): RefreshedSession | null;
|
|
204
227
|
declare function createSessionRefresher(config: SessionRefresherConfig): {
|
|
228
|
+
/**
|
|
229
|
+
* The session, or null for any failure.
|
|
230
|
+
*
|
|
231
|
+
* Kept for callers that cannot act on the difference. Anything deciding
|
|
232
|
+
* whether to CLEAR COOKIES must use {@link attemptRefresh} instead — null
|
|
233
|
+
* here means "rejected or unreachable", and treating that as rejected
|
|
234
|
+
* signs a user out because the backend restarted.
|
|
235
|
+
*/
|
|
205
236
|
refresh(refreshToken: string): Promise<RefreshedSession | null>;
|
|
237
|
+
/** The refresh outcome, keeping a rejected token apart from an unreachable backend. */
|
|
238
|
+
attemptRefresh: (refreshToken: string) => Promise<RefreshOutcome>;
|
|
206
239
|
size(): number;
|
|
207
240
|
};
|
|
208
241
|
type SessionRefresher = ReturnType<typeof createSessionRefresher>;
|
|
@@ -220,12 +253,20 @@ type SessionGateDecision = {
|
|
|
220
253
|
kind: "unauthorized_api";
|
|
221
254
|
} | {
|
|
222
255
|
kind: "redirect_login";
|
|
256
|
+
} | {
|
|
257
|
+
kind: "unavailable";
|
|
223
258
|
};
|
|
224
259
|
interface SessionGateInput {
|
|
225
260
|
accessToken?: string | null;
|
|
226
261
|
refreshToken?: string | null;
|
|
227
262
|
audience: string;
|
|
228
|
-
|
|
263
|
+
/**
|
|
264
|
+
* `attemptRefresh` is what distinguishes a rejected token from an
|
|
265
|
+
* unreachable backend. A refresher supplying only `refresh` still works and
|
|
266
|
+
* keeps the old behaviour — an outage reads as "signed out" — so pass a
|
|
267
|
+
* refresher that has it wherever a session is at stake.
|
|
268
|
+
*/
|
|
269
|
+
refresher: Pick<SessionRefresher, "refresh"> & Partial<Pick<SessionRefresher, "attemptRefresh">>;
|
|
229
270
|
isPublic: boolean;
|
|
230
271
|
isLoginPath: boolean;
|
|
231
272
|
isApiPath: boolean;
|
|
@@ -233,4 +274,4 @@ interface SessionGateInput {
|
|
|
233
274
|
}
|
|
234
275
|
declare function evaluateSessionGate(input: SessionGateInput): Promise<SessionGateDecision>;
|
|
235
276
|
|
|
236
|
-
export { type AsyncResolvable as A,
|
|
277
|
+
export { type AsyncResolvable as A, decodeDefaultSession as B, type CookieOptions as C, type DeviceIdentity as D, decodeTokenPayload as E, deleteCookie as F, evaluateSessionGate as G, HANDOFF_ERROR_FALLBACK as H, forwardedRequestHeaders as I, handoffErrorMessage as J, hasSessionShape as K, hasTokenAudience as L, isAuthErrorCode as M, isHandoffErrorCode as N, isTokenExpired as O, isTokenExpiringSoon as P, parseAuthResponse as Q, type Resolvable as R, type SessionData as S, type TokenPayload as T, parseRefreshEnvelope as U, readCookie as V, safeInternalPath as W, serializeCookie as X, setCookie as Y, sourceStartPath as Z, tokenMaxAgeSeconds as _, AUTH_ERROR_MESSAGES as a, type AuthResponse as b, type DeviceIdentityConfig as c, type DeviceIdentityStore as d, HANDOFF_ERROR_MESSAGES as e, type HandoffSource as f, type HandoffSourceConfig as g, type HandoffSourceTarget as h, type HandoffTarget as i, type HandoffTargetConfig as j, type RefreshOutcome as k, type RefreshedSession as l, SSO_ERROR_MESSAGES as m, type SessionGateDecision as n, type SessionGateInput as o, type SessionRefresher as p, type SessionRefresherConfig as q, type SessionUser as r, applyForwardedClientHeaders as s, authErrorMessage as t, brokerPaths as u, createDeviceIdentity as v, createHandoffSource as w, createHandoffTarget as x, createSessionRefresher as y, decodeBase64Url as z };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Platform-operator permission keys. */
|
|
2
|
-
declare const PLATFORM_PERMISSIONS: readonly ["affiliates.enrollments.manage", "affiliates.finance.manage", "affiliates.partners.manage", "affiliates.programmes.manage", "affiliates.view", "linkservice.domains.manage", "linkservice.links.manage", "linkservice.qr.manage", "linkservice.view", "platform.admin.view", "platform.audit.view", "platform.auth.manage", "platform.billing.manage", "platform.billing.view", "platform.channels.manage", "platform.channels.view", "platform.config.manage", "platform.dashes.purge", "platform.docs.manage", "platform.docs.view", "platform.mcp.access", "platform.observability.manage", "platform.observability.view", "platform.offboarding.view", "platform.ops.manage", "platform.ops.view", "platform.service_accounts.manage", "platform.service_accounts.view", "platform.support.impersonate", "platform.support.sessions", "platform.tenants.archive", "platform.tenants.manage", "platform.tenants.restore", "platform.tenants.view", "platform.users.deactivate", "platform.users.delete", "platform.users.invite", "platform.users.manage_access", "platform.users.reactivate", "platform.users.view"];
|
|
2
|
+
declare const PLATFORM_PERMISSIONS: readonly ["affiliates.enrollments.manage", "affiliates.finance.manage", "affiliates.partners.manage", "affiliates.programmes.manage", "affiliates.view", "linkservice.domains.manage", "linkservice.links.manage", "linkservice.qr.manage", "linkservice.view", "platform.admin.view", "platform.audit.view", "platform.auth.manage", "platform.billing.manage", "platform.billing.view", "platform.channels.manage", "platform.channels.view", "platform.config.manage", "platform.dashes.archive", "platform.dashes.manage", "platform.dashes.purge", "platform.dashes.restore", "platform.dashes.view", "platform.docs.manage", "platform.docs.view", "platform.mcp.access", "platform.observability.manage", "platform.observability.view", "platform.offboarding.view", "platform.ops.manage", "platform.ops.view", "platform.service_accounts.manage", "platform.service_accounts.view", "platform.support.impersonate", "platform.support.sessions", "platform.tenants.archive", "platform.tenants.manage", "platform.tenants.restore", "platform.tenants.view", "platform.users.deactivate", "platform.users.delete", "platform.users.invite", "platform.users.manage_access", "platform.users.reactivate", "platform.users.view"];
|
|
3
3
|
type PlatformPermissionKey = (typeof PLATFORM_PERMISSIONS)[number];
|
|
4
4
|
/** Dash-owner permission keys (granted at dash scope, mirrored from the membership role). */
|
|
5
5
|
declare const DASH_PERMISSIONS: readonly ["dash.activity.view", "dash.apps.manage", "dash.apps.view", "dash.billing.manage", "dash.billing.view", "dash.channels.manage", "dash.customers.manage", "dash.customers.view", "dash.domains.manage", "dash.domains.view", "dash.leads.export", "dash.leads.view", "dash.learning.records.view", "dash.learning.view", "dash.marketing.view", "dash.mcp.access", "dash.members.manage", "dash.members.view", "dash.ops_library.view", "dash.overview.view", "dash.ownership.transfer", "dash.plans.manage", "dash.plans.view", "dash.publish", "dash.settings.manage", "dash.settings.view", "dash.shop.view", "dash.workspaces.admin", "dash.workspaces.manage", "dash.workspaces.view"];
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { a as AUTH_ERROR_MESSAGES, b as AuthResponse, C as CookieOptions, D as DeviceIdentity, c as DeviceIdentityConfig, d as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, e as HANDOFF_ERROR_MESSAGES, f as HandoffSource, g as HandoffSourceConfig, h as HandoffSourceTarget, i as HandoffTarget, j as HandoffTargetConfig, k as
|
|
1
|
+
export { a as AUTH_ERROR_MESSAGES, b as AuthResponse, C as CookieOptions, D as DeviceIdentity, c as DeviceIdentityConfig, d as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, e as HANDOFF_ERROR_MESSAGES, f as HandoffSource, g as HandoffSourceConfig, h as HandoffSourceTarget, i as HandoffTarget, j as HandoffTargetConfig, k as RefreshOutcome, l as RefreshedSession, m as SSO_ERROR_MESSAGES, S as SessionData, n as SessionGateDecision, o as SessionGateInput, p as SessionRefresher, q as SessionRefresherConfig, r as SessionUser, T as TokenPayload, s as applyForwardedClientHeaders, t as authErrorMessage, u as brokerPaths, v as createDeviceIdentity, w as createHandoffSource, x as createHandoffTarget, y as createSessionRefresher, z as decodeBase64Url, B as decodeDefaultSession, E as decodeTokenPayload, F as deleteCookie, G as evaluateSessionGate, I as forwardedRequestHeaders, J as handoffErrorMessage, K as hasSessionShape, L as hasTokenAudience, M as isAuthErrorCode, N as isHandoffErrorCode, O as isTokenExpired, P as isTokenExpiringSoon, Q as parseAuthResponse, U as parseRefreshEnvelope, V as readCookie, W as safeInternalPath, X as serializeCookie, Y as setCookie, Z as sourceStartPath, _ as tokenMaxAgeSeconds } from './index-Blx3FNX2.js';
|
package/dist/index.js
CHANGED
|
@@ -462,6 +462,7 @@ function createHandoffSource(config) {
|
|
|
462
462
|
}
|
|
463
463
|
|
|
464
464
|
// src/session-refresh.ts
|
|
465
|
+
var REJECTION_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
|
|
465
466
|
var DEFAULT_TTL_MS = 5e3;
|
|
466
467
|
var DEFAULT_MAX = 1e3;
|
|
467
468
|
function parseRefreshEnvelope(value) {
|
|
@@ -495,11 +496,22 @@ function createSessionRefresher(config) {
|
|
|
495
496
|
timeoutMs,
|
|
496
497
|
fetch: config.fetch
|
|
497
498
|
});
|
|
498
|
-
if (!response.ok)
|
|
499
|
-
|
|
500
|
-
|
|
499
|
+
if (!response.ok) {
|
|
500
|
+
return REJECTION_STATUSES.has(response.status) ? { status: "rejected" } : { status: "unavailable" };
|
|
501
|
+
}
|
|
502
|
+
let parsed;
|
|
503
|
+
try {
|
|
504
|
+
parsed = await response.json();
|
|
505
|
+
} catch {
|
|
506
|
+
return { status: "unavailable" };
|
|
507
|
+
}
|
|
508
|
+
const refreshed = parseRefreshEnvelope(parsed);
|
|
509
|
+
if (!refreshed || !hasTokenAudience(refreshed.accessToken, config.audience)) {
|
|
510
|
+
return { status: "rejected" };
|
|
511
|
+
}
|
|
512
|
+
return { status: "refreshed", session: refreshed };
|
|
501
513
|
} catch {
|
|
502
|
-
return
|
|
514
|
+
return { status: "unavailable" };
|
|
503
515
|
}
|
|
504
516
|
}
|
|
505
517
|
function prune(now) {
|
|
@@ -512,19 +524,32 @@ function createSessionRefresher(config) {
|
|
|
512
524
|
inflight.delete(oldest);
|
|
513
525
|
}
|
|
514
526
|
}
|
|
527
|
+
function attempt(refreshToken) {
|
|
528
|
+
const now = Date.now();
|
|
529
|
+
prune(now);
|
|
530
|
+
const existing = inflight.get(refreshToken);
|
|
531
|
+
if (existing && existing.expiresAt > now) return existing.promise;
|
|
532
|
+
const promise = perform(refreshToken).then((outcome) => {
|
|
533
|
+
if (outcome.status !== "refreshed") inflight.delete(refreshToken);
|
|
534
|
+
return outcome;
|
|
535
|
+
});
|
|
536
|
+
inflight.set(refreshToken, { expiresAt: now + ttl, promise });
|
|
537
|
+
return promise;
|
|
538
|
+
}
|
|
515
539
|
return {
|
|
540
|
+
/**
|
|
541
|
+
* The session, or null for any failure.
|
|
542
|
+
*
|
|
543
|
+
* Kept for callers that cannot act on the difference. Anything deciding
|
|
544
|
+
* whether to CLEAR COOKIES must use {@link attemptRefresh} instead — null
|
|
545
|
+
* here means "rejected or unreachable", and treating that as rejected
|
|
546
|
+
* signs a user out because the backend restarted.
|
|
547
|
+
*/
|
|
516
548
|
refresh(refreshToken) {
|
|
517
|
-
|
|
518
|
-
prune(now);
|
|
519
|
-
const existing = inflight.get(refreshToken);
|
|
520
|
-
if (existing && existing.expiresAt > now) return existing.promise;
|
|
521
|
-
const promise = perform(refreshToken).then((result) => {
|
|
522
|
-
if (!result) inflight.delete(refreshToken);
|
|
523
|
-
return result;
|
|
524
|
-
});
|
|
525
|
-
inflight.set(refreshToken, { expiresAt: now + ttl, promise });
|
|
526
|
-
return promise;
|
|
549
|
+
return attempt(refreshToken).then((outcome) => outcome.status === "refreshed" ? outcome.session : null);
|
|
527
550
|
},
|
|
551
|
+
/** The refresh outcome, keeping a rejected token apart from an unreachable backend. */
|
|
552
|
+
attemptRefresh: attempt,
|
|
528
553
|
size() {
|
|
529
554
|
return inflight.size;
|
|
530
555
|
}
|
|
@@ -540,17 +565,23 @@ function hasSessionShape(token) {
|
|
|
540
565
|
async function evaluateSessionGate(input) {
|
|
541
566
|
const { accessToken, refreshToken, audience, refresher, isPublic, isLoginPath, isApiPath } = input;
|
|
542
567
|
const live = !!accessToken && hasSessionShape(accessToken) && hasTokenAudience(accessToken, audience) && !isTokenExpired(accessToken);
|
|
568
|
+
const attempt = async (token) => refresher.attemptRefresh ? await refresher.attemptRefresh(token) : await refresher.refresh(token).then(
|
|
569
|
+
(session) => session ? { status: "refreshed", session } : { status: "rejected" }
|
|
570
|
+
);
|
|
543
571
|
if (live) {
|
|
544
572
|
if (isPublic && isLoginPath) return { kind: "redirect_home" };
|
|
545
573
|
if (refreshToken && isTokenExpiringSoon(accessToken, input.expiringSoonBufferSeconds)) {
|
|
546
|
-
const
|
|
547
|
-
if (refreshed) return { kind: "pass", refreshed };
|
|
574
|
+
const outcome = await attempt(refreshToken);
|
|
575
|
+
if (outcome.status === "refreshed") return { kind: "pass", refreshed: outcome.session };
|
|
548
576
|
}
|
|
549
577
|
return { kind: "pass" };
|
|
550
578
|
}
|
|
551
579
|
if (refreshToken) {
|
|
552
|
-
const
|
|
553
|
-
if (
|
|
580
|
+
const outcome = await attempt(refreshToken);
|
|
581
|
+
if (outcome.status === "refreshed") {
|
|
582
|
+
return isPublic ? { kind: "redirect_home", refreshed: outcome.session } : { kind: "pass", refreshed: outcome.session };
|
|
583
|
+
}
|
|
584
|
+
if (outcome.status === "unavailable" && !isPublic) return { kind: "unavailable" };
|
|
554
585
|
}
|
|
555
586
|
if (isPublic) return { kind: "anonymous_public" };
|
|
556
587
|
if (isApiPath) return { kind: "unauthorized_api" };
|
package/dist/next.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as Resolvable, A as AsyncResolvable, S as SessionData, C as CookieOptions } from './index-
|
|
2
|
-
export { a as AUTH_ERROR_MESSAGES, b as AuthResponse, D as DeviceIdentity, c as DeviceIdentityConfig, d as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, e as HANDOFF_ERROR_MESSAGES, f as HandoffSource, g as HandoffSourceConfig, h as HandoffSourceTarget, i as HandoffTarget, j as HandoffTargetConfig, k as
|
|
1
|
+
import { R as Resolvable, A as AsyncResolvable, S as SessionData, C as CookieOptions } from './index-Blx3FNX2.js';
|
|
2
|
+
export { a as AUTH_ERROR_MESSAGES, b as AuthResponse, D as DeviceIdentity, c as DeviceIdentityConfig, d as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, e as HANDOFF_ERROR_MESSAGES, f as HandoffSource, g as HandoffSourceConfig, h as HandoffSourceTarget, i as HandoffTarget, j as HandoffTargetConfig, k as RefreshOutcome, l as RefreshedSession, m as SSO_ERROR_MESSAGES, n as SessionGateDecision, o as SessionGateInput, p as SessionRefresher, q as SessionRefresherConfig, r as SessionUser, T as TokenPayload, s as applyForwardedClientHeaders, t as authErrorMessage, u as brokerPaths, v as createDeviceIdentity, w as createHandoffSource, x as createHandoffTarget, y as createSessionRefresher, z as decodeBase64Url, B as decodeDefaultSession, E as decodeTokenPayload, F as deleteCookie, G as evaluateSessionGate, I as forwardedRequestHeaders, J as handoffErrorMessage, K as hasSessionShape, L as hasTokenAudience, M as isAuthErrorCode, N as isHandoffErrorCode, O as isTokenExpired, P as isTokenExpiringSoon, Q as parseAuthResponse, U as parseRefreshEnvelope, V as readCookie, W as safeInternalPath, X as serializeCookie, Y as setCookie, Z as sourceStartPath, _ as tokenMaxAgeSeconds } from './index-Blx3FNX2.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The error vocabulary every first-party platform surface uses at its edge.
|
package/dist/next.js
CHANGED
|
@@ -467,6 +467,7 @@ function createHandoffSource(config) {
|
|
|
467
467
|
}
|
|
468
468
|
|
|
469
469
|
// src/session-refresh.ts
|
|
470
|
+
var REJECTION_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
|
|
470
471
|
var DEFAULT_TTL_MS = 5e3;
|
|
471
472
|
var DEFAULT_MAX = 1e3;
|
|
472
473
|
function parseRefreshEnvelope(value) {
|
|
@@ -500,11 +501,22 @@ function createSessionRefresher(config) {
|
|
|
500
501
|
timeoutMs,
|
|
501
502
|
fetch: config.fetch
|
|
502
503
|
});
|
|
503
|
-
if (!response.ok)
|
|
504
|
-
|
|
505
|
-
|
|
504
|
+
if (!response.ok) {
|
|
505
|
+
return REJECTION_STATUSES.has(response.status) ? { status: "rejected" } : { status: "unavailable" };
|
|
506
|
+
}
|
|
507
|
+
let parsed;
|
|
508
|
+
try {
|
|
509
|
+
parsed = await response.json();
|
|
510
|
+
} catch {
|
|
511
|
+
return { status: "unavailable" };
|
|
512
|
+
}
|
|
513
|
+
const refreshed = parseRefreshEnvelope(parsed);
|
|
514
|
+
if (!refreshed || !hasTokenAudience(refreshed.accessToken, config.audience)) {
|
|
515
|
+
return { status: "rejected" };
|
|
516
|
+
}
|
|
517
|
+
return { status: "refreshed", session: refreshed };
|
|
506
518
|
} catch {
|
|
507
|
-
return
|
|
519
|
+
return { status: "unavailable" };
|
|
508
520
|
}
|
|
509
521
|
}
|
|
510
522
|
function prune(now) {
|
|
@@ -517,19 +529,32 @@ function createSessionRefresher(config) {
|
|
|
517
529
|
inflight.delete(oldest);
|
|
518
530
|
}
|
|
519
531
|
}
|
|
532
|
+
function attempt(refreshToken) {
|
|
533
|
+
const now = Date.now();
|
|
534
|
+
prune(now);
|
|
535
|
+
const existing = inflight.get(refreshToken);
|
|
536
|
+
if (existing && existing.expiresAt > now) return existing.promise;
|
|
537
|
+
const promise = perform(refreshToken).then((outcome) => {
|
|
538
|
+
if (outcome.status !== "refreshed") inflight.delete(refreshToken);
|
|
539
|
+
return outcome;
|
|
540
|
+
});
|
|
541
|
+
inflight.set(refreshToken, { expiresAt: now + ttl, promise });
|
|
542
|
+
return promise;
|
|
543
|
+
}
|
|
520
544
|
return {
|
|
545
|
+
/**
|
|
546
|
+
* The session, or null for any failure.
|
|
547
|
+
*
|
|
548
|
+
* Kept for callers that cannot act on the difference. Anything deciding
|
|
549
|
+
* whether to CLEAR COOKIES must use {@link attemptRefresh} instead — null
|
|
550
|
+
* here means "rejected or unreachable", and treating that as rejected
|
|
551
|
+
* signs a user out because the backend restarted.
|
|
552
|
+
*/
|
|
521
553
|
refresh(refreshToken) {
|
|
522
|
-
|
|
523
|
-
prune(now);
|
|
524
|
-
const existing = inflight.get(refreshToken);
|
|
525
|
-
if (existing && existing.expiresAt > now) return existing.promise;
|
|
526
|
-
const promise = perform(refreshToken).then((result) => {
|
|
527
|
-
if (!result) inflight.delete(refreshToken);
|
|
528
|
-
return result;
|
|
529
|
-
});
|
|
530
|
-
inflight.set(refreshToken, { expiresAt: now + ttl, promise });
|
|
531
|
-
return promise;
|
|
554
|
+
return attempt(refreshToken).then((outcome) => outcome.status === "refreshed" ? outcome.session : null);
|
|
532
555
|
},
|
|
556
|
+
/** The refresh outcome, keeping a rejected token apart from an unreachable backend. */
|
|
557
|
+
attemptRefresh: attempt,
|
|
533
558
|
size() {
|
|
534
559
|
return inflight.size;
|
|
535
560
|
}
|
|
@@ -545,17 +570,23 @@ function hasSessionShape(token) {
|
|
|
545
570
|
async function evaluateSessionGate(input) {
|
|
546
571
|
const { accessToken, refreshToken, audience, refresher, isPublic, isLoginPath, isApiPath } = input;
|
|
547
572
|
const live = !!accessToken && hasSessionShape(accessToken) && hasTokenAudience(accessToken, audience) && !isTokenExpired(accessToken);
|
|
573
|
+
const attempt = async (token) => refresher.attemptRefresh ? await refresher.attemptRefresh(token) : await refresher.refresh(token).then(
|
|
574
|
+
(session) => session ? { status: "refreshed", session } : { status: "rejected" }
|
|
575
|
+
);
|
|
548
576
|
if (live) {
|
|
549
577
|
if (isPublic && isLoginPath) return { kind: "redirect_home" };
|
|
550
578
|
if (refreshToken && isTokenExpiringSoon(accessToken, input.expiringSoonBufferSeconds)) {
|
|
551
|
-
const
|
|
552
|
-
if (refreshed) return { kind: "pass", refreshed };
|
|
579
|
+
const outcome = await attempt(refreshToken);
|
|
580
|
+
if (outcome.status === "refreshed") return { kind: "pass", refreshed: outcome.session };
|
|
553
581
|
}
|
|
554
582
|
return { kind: "pass" };
|
|
555
583
|
}
|
|
556
584
|
if (refreshToken) {
|
|
557
|
-
const
|
|
558
|
-
if (
|
|
585
|
+
const outcome = await attempt(refreshToken);
|
|
586
|
+
if (outcome.status === "refreshed") {
|
|
587
|
+
return isPublic ? { kind: "redirect_home", refreshed: outcome.session } : { kind: "pass", refreshed: outcome.session };
|
|
588
|
+
}
|
|
589
|
+
if (outcome.status === "unavailable" && !isPublic) return { kind: "unavailable" };
|
|
559
590
|
}
|
|
560
591
|
if (isPublic) return { kind: "anonymous_public" };
|
|
561
592
|
if (isApiPath) return { kind: "unauthorized_api" };
|
package/dist/service-accounts.js
CHANGED
|
@@ -1,29 +1,5 @@
|
|
|
1
|
-
// ../
|
|
1
|
+
// ../resilience/src/index.ts
|
|
2
2
|
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
3
|
-
var TransportError = class extends Error {
|
|
4
|
-
constructor(failure) {
|
|
5
|
-
super(failure.message || failure.outcome);
|
|
6
|
-
this.failure = failure;
|
|
7
|
-
this.name = "TransportError";
|
|
8
|
-
}
|
|
9
|
-
failure;
|
|
10
|
-
};
|
|
11
|
-
function isTransportError(error) {
|
|
12
|
-
return error instanceof TransportError || typeof error === "object" && error !== null && error.name === "TransportError";
|
|
13
|
-
}
|
|
14
|
-
function positiveNumber(value, label, fail) {
|
|
15
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw fail(`${label} must be a positive finite number`);
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
18
|
-
function nonNegativeInteger(value, label, fail) {
|
|
19
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw fail(`${label} must be a non-negative safe integer`);
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
function requiredString(value, label, fail) {
|
|
23
|
-
const normalized = typeof value === "string" ? value.trim() : "";
|
|
24
|
-
if (!normalized) throw fail(`${label} is required`);
|
|
25
|
-
return normalized;
|
|
26
|
-
}
|
|
27
3
|
function backoffDelay(attempt, maxDelayMs) {
|
|
28
4
|
const base = Math.min(250 * 2 ** Math.max(attempt, 0), maxDelayMs);
|
|
29
5
|
return Math.floor(Math.random() * base);
|
|
@@ -49,6 +25,32 @@ function isRetryableRequest(method, headers, retry) {
|
|
|
49
25
|
if (method === "GET") return true;
|
|
50
26
|
return retry.idempotentWrites === true && headers.has("Idempotency-Key");
|
|
51
27
|
}
|
|
28
|
+
|
|
29
|
+
// ../transport/src/index.ts
|
|
30
|
+
var TransportError = class extends Error {
|
|
31
|
+
constructor(failure) {
|
|
32
|
+
super(failure.message || failure.outcome);
|
|
33
|
+
this.failure = failure;
|
|
34
|
+
this.name = "TransportError";
|
|
35
|
+
}
|
|
36
|
+
failure;
|
|
37
|
+
};
|
|
38
|
+
function isTransportError(error) {
|
|
39
|
+
return error instanceof TransportError || typeof error === "object" && error !== null && error.name === "TransportError";
|
|
40
|
+
}
|
|
41
|
+
function positiveNumber(value, label, fail) {
|
|
42
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw fail(`${label} must be a positive finite number`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function nonNegativeInteger(value, label, fail) {
|
|
46
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw fail(`${label} must be a non-negative safe integer`);
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
function requiredString(value, label, fail) {
|
|
50
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
51
|
+
if (!normalized) throw fail(`${label} is required`);
|
|
52
|
+
return normalized;
|
|
53
|
+
}
|
|
52
54
|
async function performRequest(transport, url, init, observe, decode) {
|
|
53
55
|
const method = (init.method ?? "GET").toUpperCase();
|
|
54
56
|
const headers = new Headers(init.headers);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c1m-public/platform-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Session and authorization helpers for C1M platform surfaces. The root is runtime-neutral (Web Request/Response): token/session reads, the first-party handoff (PKCE target + source), proxy session gate and coalesced refresh; ./next adds the server-only guard; ./access carries the resolved access snapshot, typed permission keys, predicates, a Next loader and React bindings",
|
|
@@ -56,9 +56,10 @@
|
|
|
56
56
|
"next": "16.3.1",
|
|
57
57
|
"react": "19.2.4",
|
|
58
58
|
"react-dom": "19.2.4",
|
|
59
|
+
"tsup": "^8.5.1",
|
|
59
60
|
"typescript": "5.9.3",
|
|
60
61
|
"vitest": "^4.1.4",
|
|
61
|
-
"
|
|
62
|
+
"@c1m-public/resilience": "0.1.0",
|
|
62
63
|
"@c1m-public/transport": "0.0.0"
|
|
63
64
|
},
|
|
64
65
|
"peerDependencies": {
|