acai-ui-library 0.1.13 → 0.1.14
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/components/alert.cjs +3 -2
- package/dist/components/alert.cjs.map +1 -1
- package/dist/components/alert.js +3 -2
- package/dist/components/alert.js.map +1 -1
- package/dist/components/card.cjs +1 -1
- package/dist/components/card.cjs.map +1 -1
- package/dist/components/card.js +1 -1
- package/dist/components/card.js.map +1 -1
- package/dist/components/checkbox.cjs +2 -1
- package/dist/components/checkbox.cjs.map +1 -1
- package/dist/components/checkbox.js +2 -1
- package/dist/components/checkbox.js.map +1 -1
- package/dist/components/data-table.cjs +2 -1
- package/dist/components/data-table.cjs.map +1 -1
- package/dist/components/data-table.js +2 -1
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.cjs.map +1 -1
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/impersonation-banner.cjs +137 -0
- package/dist/components/impersonation-banner.cjs.map +1 -0
- package/dist/components/impersonation-banner.d.cts +36 -0
- package/dist/components/impersonation-banner.d.ts +36 -0
- package/dist/components/impersonation-banner.js +101 -0
- package/dist/components/impersonation-banner.js.map +1 -0
- package/dist/components/input.cjs +2 -1
- package/dist/components/input.cjs.map +1 -1
- package/dist/components/input.js +2 -1
- package/dist/components/input.js.map +1 -1
- package/dist/components/item.cjs +1 -2
- package/dist/components/item.cjs.map +1 -1
- package/dist/components/item.js +1 -2
- package/dist/components/item.js.map +1 -1
- package/dist/components/pagination.cjs +3 -1
- package/dist/components/pagination.cjs.map +1 -1
- package/dist/components/pagination.d.cts +1 -1
- package/dist/components/pagination.d.ts +1 -1
- package/dist/components/pagination.js +3 -1
- package/dist/components/pagination.js.map +1 -1
- package/dist/components/read-only-guard.cjs +103 -0
- package/dist/components/read-only-guard.cjs.map +1 -0
- package/dist/components/read-only-guard.d.cts +40 -0
- package/dist/components/read-only-guard.d.ts +40 -0
- package/dist/components/read-only-guard.js +64 -0
- package/dist/components/read-only-guard.js.map +1 -0
- package/dist/components/stepper.cjs.map +1 -1
- package/dist/components/stepper.js.map +1 -1
- package/dist/components/typography.cjs +4 -4
- package/dist/components/typography.cjs.map +1 -1
- package/dist/components/typography.js +4 -4
- package/dist/components/typography.js.map +1 -1
- package/dist/index.cjs +236 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +224 -12
- package/dist/index.js.map +1 -1
- package/dist/lib/impersonation.cjs +127 -0
- package/dist/lib/impersonation.cjs.map +1 -0
- package/dist/lib/impersonation.d.cts +45 -0
- package/dist/lib/impersonation.d.ts +45 -0
- package/dist/lib/impersonation.js +86 -0
- package/dist/lib/impersonation.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/lib/impersonation.ts
|
|
31
|
+
var impersonation_exports = {};
|
|
32
|
+
__export(impersonation_exports, {
|
|
33
|
+
INACTIVE_IMPERSONATION: () => INACTIVE_IMPERSONATION,
|
|
34
|
+
OPERATION_CLASSES: () => OPERATION_CLASSES,
|
|
35
|
+
approvedOpsOf: () => approvedOpsOf,
|
|
36
|
+
decodeJwtPayload: () => decodeJwtPayload,
|
|
37
|
+
modeOfOps: () => modeOfOps,
|
|
38
|
+
readImpersonation: () => readImpersonation,
|
|
39
|
+
useImpersonation: () => useImpersonation
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(impersonation_exports);
|
|
42
|
+
var React = __toESM(require("react"), 1);
|
|
43
|
+
var OPERATION_CLASSES = ["READ", "WRITE", "DESTRUCTIVE", "FINANCIAL", "SECURITY"];
|
|
44
|
+
var PLATFORM_BLOCKED = /* @__PURE__ */ new Set(["FINANCIAL", "SECURITY"]);
|
|
45
|
+
var INACTIVE_IMPERSONATION = Object.freeze({
|
|
46
|
+
active: false,
|
|
47
|
+
mode: null,
|
|
48
|
+
ops: [],
|
|
49
|
+
actorSub: null,
|
|
50
|
+
actorUserType: null,
|
|
51
|
+
sessionId: null,
|
|
52
|
+
expiresAt: null
|
|
53
|
+
});
|
|
54
|
+
function base64UrlDecode(segment) {
|
|
55
|
+
const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
56
|
+
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
57
|
+
const binary = atob(padded);
|
|
58
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
59
|
+
return new TextDecoder().decode(bytes);
|
|
60
|
+
}
|
|
61
|
+
function decodeJwtPayload(token) {
|
|
62
|
+
if (typeof token !== "string") return null;
|
|
63
|
+
const parts = token.split(".");
|
|
64
|
+
if (parts.length !== 3 || !parts[1]) return null;
|
|
65
|
+
try {
|
|
66
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
67
|
+
return payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var nonEmpty = (v) => typeof v === "string" && v.trim() !== "" ? v : null;
|
|
73
|
+
function approvedOpsOf(claims) {
|
|
74
|
+
let ops;
|
|
75
|
+
if (Array.isArray(claims.imp_ops)) ops = claims.imp_ops.filter((v) => typeof v === "string");
|
|
76
|
+
else if (claims.imp_mode === "WRITE") ops = ["READ", "WRITE"];
|
|
77
|
+
else ops = ["READ"];
|
|
78
|
+
const set = new Set(ops.filter((op) => !PLATFORM_BLOCKED.has(op)));
|
|
79
|
+
return OPERATION_CLASSES.filter((c) => set.has(c));
|
|
80
|
+
}
|
|
81
|
+
function modeOfOps(ops) {
|
|
82
|
+
return ops.every((op) => op === "READ") ? "READ" : "WRITE";
|
|
83
|
+
}
|
|
84
|
+
function readImpersonation(token, now = Date.now()) {
|
|
85
|
+
const claims = decodeJwtPayload(token);
|
|
86
|
+
if (!claims) return INACTIVE_IMPERSONATION;
|
|
87
|
+
const amr = Array.isArray(claims.amr) ? claims.amr : [];
|
|
88
|
+
if (!amr.includes("impersonation")) return INACTIVE_IMPERSONATION;
|
|
89
|
+
const act = claims.act !== null && typeof claims.act === "object" ? claims.act : {};
|
|
90
|
+
const actorSub = nonEmpty(act.sub);
|
|
91
|
+
const sessionId = nonEmpty(claims.imp_sid);
|
|
92
|
+
const expiresAt = typeof claims.exp === "number" ? new Date(claims.exp * 1e3) : null;
|
|
93
|
+
if (!actorSub || !sessionId || !expiresAt || expiresAt.getTime() <= now) return INACTIVE_IMPERSONATION;
|
|
94
|
+
const ops = approvedOpsOf(claims);
|
|
95
|
+
return {
|
|
96
|
+
active: true,
|
|
97
|
+
mode: modeOfOps(ops),
|
|
98
|
+
ops,
|
|
99
|
+
actorSub,
|
|
100
|
+
actorUserType: nonEmpty(act.userType),
|
|
101
|
+
sessionId,
|
|
102
|
+
expiresAt
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function useImpersonation(token) {
|
|
106
|
+
const [tick, setTick] = React.useState(0);
|
|
107
|
+
const state = React.useMemo(() => readImpersonation(token), [token, tick]);
|
|
108
|
+
const expiresAtMs = state.expiresAt?.getTime() ?? null;
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
if (expiresAtMs === null) return void 0;
|
|
111
|
+
const delay = Math.max(0, expiresAtMs - Date.now());
|
|
112
|
+
const timer = setTimeout(() => setTick((t) => t + 1), Math.min(delay + 50, 2147e6));
|
|
113
|
+
return () => clearTimeout(timer);
|
|
114
|
+
}, [expiresAtMs]);
|
|
115
|
+
return state;
|
|
116
|
+
}
|
|
117
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
118
|
+
0 && (module.exports = {
|
|
119
|
+
INACTIVE_IMPERSONATION,
|
|
120
|
+
OPERATION_CLASSES,
|
|
121
|
+
approvedOpsOf,
|
|
122
|
+
decodeJwtPayload,
|
|
123
|
+
modeOfOps,
|
|
124
|
+
readImpersonation,
|
|
125
|
+
useImpersonation
|
|
126
|
+
});
|
|
127
|
+
//# sourceMappingURL=impersonation.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/impersonation.ts"],"sourcesContent":["import * as React from \"react\"\n\n/**\n * Support impersonation helpers (acai-ai-platform#1274).\n *\n * An impersonation access token is issued by auth-sso-server (#1272) for the target user and carries:\n * amr: [\"impersonation\"], act: { sub, userType }, imp_sid, imp_ops, imp_mode, exp\n * imp_ops is the set of approved operation classes (READ, WRITE, DESTRUCTIVE). imp_mode is READ when only READ is\n * approved, otherwise WRITE.\n *\n * Everything here decodes the token on the client FOR DISPLAY ONLY. Nothing is verified. Backends enforce the\n * session; never use these values to grant access.\n */\n\nexport const OPERATION_CLASSES = [\"READ\", \"WRITE\", \"DESTRUCTIVE\", \"FINANCIAL\", \"SECURITY\"] as const\nexport type OperationClass = (typeof OPERATION_CLASSES)[number]\nexport type ImpersonationMode = \"READ\" | \"WRITE\"\n\n/** Classes no support session can ever use, whatever a token says. */\nconst PLATFORM_BLOCKED: ReadonlySet<string> = new Set([\"FINANCIAL\", \"SECURITY\"])\n\nexport interface ImpersonationState {\n /** True only for an unexpired token with amr \"impersonation\", an actor and a session id. */\n active: boolean\n /** READ or WRITE; null when not active. */\n mode: ImpersonationMode | null\n /** Approved operation classes (never FINANCIAL or SECURITY); empty when not active. */\n ops: OperationClass[]\n /** ssoUserId of the support person acting (act.sub). */\n actorSub: string | null\n /** SUPER_ADMIN or ACAI_STAFF (act.userType), when the token says. */\n actorUserType: string | null\n /** The SSO impersonation session id (imp_sid). */\n sessionId: string | null\n /** When the token stops working. */\n expiresAt: Date | null\n}\n\nexport const INACTIVE_IMPERSONATION: ImpersonationState = Object.freeze({\n active: false,\n mode: null,\n ops: [],\n actorSub: null,\n actorUserType: null,\n sessionId: null,\n expiresAt: null,\n}) as ImpersonationState\n\nfunction base64UrlDecode(segment: string): string {\n const b64 = segment.replace(/-/g, \"+\").replace(/_/g, \"/\")\n const padded = b64 + \"=\".repeat((4 - (b64.length % 4)) % 4)\n const binary = atob(padded)\n const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0))\n return new TextDecoder().decode(bytes)\n}\n\n/** The JWT payload, or null when the token is not a decodable JWT. Not verified. */\nexport function decodeJwtPayload(token: string | null | undefined): Record<string, unknown> | null {\n if (typeof token !== \"string\") return null\n const parts = token.split(\".\")\n if (parts.length !== 3 || !parts[1]) return null\n try {\n const payload = JSON.parse(base64UrlDecode(parts[1])) as unknown\n return payload !== null && typeof payload === \"object\" && !Array.isArray(payload)\n ? (payload as Record<string, unknown>)\n : null\n } catch {\n return null\n }\n}\n\nconst nonEmpty = (v: unknown): string | null => (typeof v === \"string\" && v.trim() !== \"\" ? v : null)\n\n/** Approved classes, read the same way the backends do: imp_ops, else imp_mode WRITE = READ+WRITE, else READ. */\nexport function approvedOpsOf(claims: Record<string, unknown>): OperationClass[] {\n let ops: string[]\n if (Array.isArray(claims.imp_ops)) ops = claims.imp_ops.filter((v): v is string => typeof v === \"string\")\n else if (claims.imp_mode === \"WRITE\") ops = [\"READ\", \"WRITE\"]\n else ops = [\"READ\"]\n const set = new Set(ops.filter((op) => !PLATFORM_BLOCKED.has(op)))\n return OPERATION_CLASSES.filter((c) => set.has(c))\n}\n\nexport function modeOfOps(ops: readonly string[]): ImpersonationMode {\n return ops.every((op) => op === \"READ\") ? \"READ\" : \"WRITE\"\n}\n\n/** Decode an access token's impersonation state. For display only: the token is not verified. */\nexport function readImpersonation(token: string | null | undefined, now: number = Date.now()): ImpersonationState {\n const claims = decodeJwtPayload(token)\n if (!claims) return INACTIVE_IMPERSONATION\n const amr = Array.isArray(claims.amr) ? claims.amr : []\n if (!amr.includes(\"impersonation\")) return INACTIVE_IMPERSONATION\n const act = claims.act !== null && typeof claims.act === \"object\" ? (claims.act as Record<string, unknown>) : {}\n const actorSub = nonEmpty(act.sub)\n const sessionId = nonEmpty(claims.imp_sid)\n const expiresAt = typeof claims.exp === \"number\" ? new Date(claims.exp * 1000) : null\n if (!actorSub || !sessionId || !expiresAt || expiresAt.getTime() <= now) return INACTIVE_IMPERSONATION\n const ops = approvedOpsOf(claims)\n return {\n active: true,\n mode: modeOfOps(ops),\n ops,\n actorSub,\n actorUserType: nonEmpty(act.userType),\n sessionId,\n expiresAt,\n }\n}\n\n/**\n * `useImpersonation(token)`: the impersonation state of an access token, for display (banner, disabled controls).\n * Re-renders when the token expires, so `active` turns false on time. Not a security check.\n */\nexport function useImpersonation(token: string | null | undefined): ImpersonationState {\n // Bumped when the token expires; the state is always computed against the current clock.\n const [tick, setTick] = React.useState(0)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const state = React.useMemo(() => readImpersonation(token), [token, tick])\n const expiresAtMs = state.expiresAt?.getTime() ?? null\n\n React.useEffect(() => {\n if (expiresAtMs === null) return undefined\n const delay = Math.max(0, expiresAtMs - Date.now())\n // setTimeout caps at about 24.8 days; impersonation tokens live at most an hour.\n const timer = setTimeout(() => setTick((t) => t + 1), Math.min(delay + 50, 2_147_000_000))\n return () => clearTimeout(timer)\n }, [expiresAtMs])\n\n return state\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Support impersonation helpers (acai-ai-platform#1274).
|
|
3
|
+
*
|
|
4
|
+
* An impersonation access token is issued by auth-sso-server (#1272) for the target user and carries:
|
|
5
|
+
* amr: ["impersonation"], act: { sub, userType }, imp_sid, imp_ops, imp_mode, exp
|
|
6
|
+
* imp_ops is the set of approved operation classes (READ, WRITE, DESTRUCTIVE). imp_mode is READ when only READ is
|
|
7
|
+
* approved, otherwise WRITE.
|
|
8
|
+
*
|
|
9
|
+
* Everything here decodes the token on the client FOR DISPLAY ONLY. Nothing is verified. Backends enforce the
|
|
10
|
+
* session; never use these values to grant access.
|
|
11
|
+
*/
|
|
12
|
+
declare const OPERATION_CLASSES: readonly ["READ", "WRITE", "DESTRUCTIVE", "FINANCIAL", "SECURITY"];
|
|
13
|
+
type OperationClass = (typeof OPERATION_CLASSES)[number];
|
|
14
|
+
type ImpersonationMode = "READ" | "WRITE";
|
|
15
|
+
interface ImpersonationState {
|
|
16
|
+
/** True only for an unexpired token with amr "impersonation", an actor and a session id. */
|
|
17
|
+
active: boolean;
|
|
18
|
+
/** READ or WRITE; null when not active. */
|
|
19
|
+
mode: ImpersonationMode | null;
|
|
20
|
+
/** Approved operation classes (never FINANCIAL or SECURITY); empty when not active. */
|
|
21
|
+
ops: OperationClass[];
|
|
22
|
+
/** ssoUserId of the support person acting (act.sub). */
|
|
23
|
+
actorSub: string | null;
|
|
24
|
+
/** SUPER_ADMIN or ACAI_STAFF (act.userType), when the token says. */
|
|
25
|
+
actorUserType: string | null;
|
|
26
|
+
/** The SSO impersonation session id (imp_sid). */
|
|
27
|
+
sessionId: string | null;
|
|
28
|
+
/** When the token stops working. */
|
|
29
|
+
expiresAt: Date | null;
|
|
30
|
+
}
|
|
31
|
+
declare const INACTIVE_IMPERSONATION: ImpersonationState;
|
|
32
|
+
/** The JWT payload, or null when the token is not a decodable JWT. Not verified. */
|
|
33
|
+
declare function decodeJwtPayload(token: string | null | undefined): Record<string, unknown> | null;
|
|
34
|
+
/** Approved classes, read the same way the backends do: imp_ops, else imp_mode WRITE = READ+WRITE, else READ. */
|
|
35
|
+
declare function approvedOpsOf(claims: Record<string, unknown>): OperationClass[];
|
|
36
|
+
declare function modeOfOps(ops: readonly string[]): ImpersonationMode;
|
|
37
|
+
/** Decode an access token's impersonation state. For display only: the token is not verified. */
|
|
38
|
+
declare function readImpersonation(token: string | null | undefined, now?: number): ImpersonationState;
|
|
39
|
+
/**
|
|
40
|
+
* `useImpersonation(token)`: the impersonation state of an access token, for display (banner, disabled controls).
|
|
41
|
+
* Re-renders when the token expires, so `active` turns false on time. Not a security check.
|
|
42
|
+
*/
|
|
43
|
+
declare function useImpersonation(token: string | null | undefined): ImpersonationState;
|
|
44
|
+
|
|
45
|
+
export { INACTIVE_IMPERSONATION, type ImpersonationMode, type ImpersonationState, OPERATION_CLASSES, type OperationClass, approvedOpsOf, decodeJwtPayload, modeOfOps, readImpersonation, useImpersonation };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Support impersonation helpers (acai-ai-platform#1274).
|
|
3
|
+
*
|
|
4
|
+
* An impersonation access token is issued by auth-sso-server (#1272) for the target user and carries:
|
|
5
|
+
* amr: ["impersonation"], act: { sub, userType }, imp_sid, imp_ops, imp_mode, exp
|
|
6
|
+
* imp_ops is the set of approved operation classes (READ, WRITE, DESTRUCTIVE). imp_mode is READ when only READ is
|
|
7
|
+
* approved, otherwise WRITE.
|
|
8
|
+
*
|
|
9
|
+
* Everything here decodes the token on the client FOR DISPLAY ONLY. Nothing is verified. Backends enforce the
|
|
10
|
+
* session; never use these values to grant access.
|
|
11
|
+
*/
|
|
12
|
+
declare const OPERATION_CLASSES: readonly ["READ", "WRITE", "DESTRUCTIVE", "FINANCIAL", "SECURITY"];
|
|
13
|
+
type OperationClass = (typeof OPERATION_CLASSES)[number];
|
|
14
|
+
type ImpersonationMode = "READ" | "WRITE";
|
|
15
|
+
interface ImpersonationState {
|
|
16
|
+
/** True only for an unexpired token with amr "impersonation", an actor and a session id. */
|
|
17
|
+
active: boolean;
|
|
18
|
+
/** READ or WRITE; null when not active. */
|
|
19
|
+
mode: ImpersonationMode | null;
|
|
20
|
+
/** Approved operation classes (never FINANCIAL or SECURITY); empty when not active. */
|
|
21
|
+
ops: OperationClass[];
|
|
22
|
+
/** ssoUserId of the support person acting (act.sub). */
|
|
23
|
+
actorSub: string | null;
|
|
24
|
+
/** SUPER_ADMIN or ACAI_STAFF (act.userType), when the token says. */
|
|
25
|
+
actorUserType: string | null;
|
|
26
|
+
/** The SSO impersonation session id (imp_sid). */
|
|
27
|
+
sessionId: string | null;
|
|
28
|
+
/** When the token stops working. */
|
|
29
|
+
expiresAt: Date | null;
|
|
30
|
+
}
|
|
31
|
+
declare const INACTIVE_IMPERSONATION: ImpersonationState;
|
|
32
|
+
/** The JWT payload, or null when the token is not a decodable JWT. Not verified. */
|
|
33
|
+
declare function decodeJwtPayload(token: string | null | undefined): Record<string, unknown> | null;
|
|
34
|
+
/** Approved classes, read the same way the backends do: imp_ops, else imp_mode WRITE = READ+WRITE, else READ. */
|
|
35
|
+
declare function approvedOpsOf(claims: Record<string, unknown>): OperationClass[];
|
|
36
|
+
declare function modeOfOps(ops: readonly string[]): ImpersonationMode;
|
|
37
|
+
/** Decode an access token's impersonation state. For display only: the token is not verified. */
|
|
38
|
+
declare function readImpersonation(token: string | null | undefined, now?: number): ImpersonationState;
|
|
39
|
+
/**
|
|
40
|
+
* `useImpersonation(token)`: the impersonation state of an access token, for display (banner, disabled controls).
|
|
41
|
+
* Re-renders when the token expires, so `active` turns false on time. Not a security check.
|
|
42
|
+
*/
|
|
43
|
+
declare function useImpersonation(token: string | null | undefined): ImpersonationState;
|
|
44
|
+
|
|
45
|
+
export { INACTIVE_IMPERSONATION, type ImpersonationMode, type ImpersonationState, OPERATION_CLASSES, type OperationClass, approvedOpsOf, decodeJwtPayload, modeOfOps, readImpersonation, useImpersonation };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/lib/impersonation.ts
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
var OPERATION_CLASSES = ["READ", "WRITE", "DESTRUCTIVE", "FINANCIAL", "SECURITY"];
|
|
4
|
+
var PLATFORM_BLOCKED = /* @__PURE__ */ new Set(["FINANCIAL", "SECURITY"]);
|
|
5
|
+
var INACTIVE_IMPERSONATION = Object.freeze({
|
|
6
|
+
active: false,
|
|
7
|
+
mode: null,
|
|
8
|
+
ops: [],
|
|
9
|
+
actorSub: null,
|
|
10
|
+
actorUserType: null,
|
|
11
|
+
sessionId: null,
|
|
12
|
+
expiresAt: null
|
|
13
|
+
});
|
|
14
|
+
function base64UrlDecode(segment) {
|
|
15
|
+
const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
16
|
+
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
17
|
+
const binary = atob(padded);
|
|
18
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
19
|
+
return new TextDecoder().decode(bytes);
|
|
20
|
+
}
|
|
21
|
+
function decodeJwtPayload(token) {
|
|
22
|
+
if (typeof token !== "string") return null;
|
|
23
|
+
const parts = token.split(".");
|
|
24
|
+
if (parts.length !== 3 || !parts[1]) return null;
|
|
25
|
+
try {
|
|
26
|
+
const payload = JSON.parse(base64UrlDecode(parts[1]));
|
|
27
|
+
return payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
var nonEmpty = (v) => typeof v === "string" && v.trim() !== "" ? v : null;
|
|
33
|
+
function approvedOpsOf(claims) {
|
|
34
|
+
let ops;
|
|
35
|
+
if (Array.isArray(claims.imp_ops)) ops = claims.imp_ops.filter((v) => typeof v === "string");
|
|
36
|
+
else if (claims.imp_mode === "WRITE") ops = ["READ", "WRITE"];
|
|
37
|
+
else ops = ["READ"];
|
|
38
|
+
const set = new Set(ops.filter((op) => !PLATFORM_BLOCKED.has(op)));
|
|
39
|
+
return OPERATION_CLASSES.filter((c) => set.has(c));
|
|
40
|
+
}
|
|
41
|
+
function modeOfOps(ops) {
|
|
42
|
+
return ops.every((op) => op === "READ") ? "READ" : "WRITE";
|
|
43
|
+
}
|
|
44
|
+
function readImpersonation(token, now = Date.now()) {
|
|
45
|
+
const claims = decodeJwtPayload(token);
|
|
46
|
+
if (!claims) return INACTIVE_IMPERSONATION;
|
|
47
|
+
const amr = Array.isArray(claims.amr) ? claims.amr : [];
|
|
48
|
+
if (!amr.includes("impersonation")) return INACTIVE_IMPERSONATION;
|
|
49
|
+
const act = claims.act !== null && typeof claims.act === "object" ? claims.act : {};
|
|
50
|
+
const actorSub = nonEmpty(act.sub);
|
|
51
|
+
const sessionId = nonEmpty(claims.imp_sid);
|
|
52
|
+
const expiresAt = typeof claims.exp === "number" ? new Date(claims.exp * 1e3) : null;
|
|
53
|
+
if (!actorSub || !sessionId || !expiresAt || expiresAt.getTime() <= now) return INACTIVE_IMPERSONATION;
|
|
54
|
+
const ops = approvedOpsOf(claims);
|
|
55
|
+
return {
|
|
56
|
+
active: true,
|
|
57
|
+
mode: modeOfOps(ops),
|
|
58
|
+
ops,
|
|
59
|
+
actorSub,
|
|
60
|
+
actorUserType: nonEmpty(act.userType),
|
|
61
|
+
sessionId,
|
|
62
|
+
expiresAt
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function useImpersonation(token) {
|
|
66
|
+
const [tick, setTick] = React.useState(0);
|
|
67
|
+
const state = React.useMemo(() => readImpersonation(token), [token, tick]);
|
|
68
|
+
const expiresAtMs = state.expiresAt?.getTime() ?? null;
|
|
69
|
+
React.useEffect(() => {
|
|
70
|
+
if (expiresAtMs === null) return void 0;
|
|
71
|
+
const delay = Math.max(0, expiresAtMs - Date.now());
|
|
72
|
+
const timer = setTimeout(() => setTick((t) => t + 1), Math.min(delay + 50, 2147e6));
|
|
73
|
+
return () => clearTimeout(timer);
|
|
74
|
+
}, [expiresAtMs]);
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
INACTIVE_IMPERSONATION,
|
|
79
|
+
OPERATION_CLASSES,
|
|
80
|
+
approvedOpsOf,
|
|
81
|
+
decodeJwtPayload,
|
|
82
|
+
modeOfOps,
|
|
83
|
+
readImpersonation,
|
|
84
|
+
useImpersonation
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=impersonation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/impersonation.ts"],"sourcesContent":["import * as React from \"react\"\n\n/**\n * Support impersonation helpers (acai-ai-platform#1274).\n *\n * An impersonation access token is issued by auth-sso-server (#1272) for the target user and carries:\n * amr: [\"impersonation\"], act: { sub, userType }, imp_sid, imp_ops, imp_mode, exp\n * imp_ops is the set of approved operation classes (READ, WRITE, DESTRUCTIVE). imp_mode is READ when only READ is\n * approved, otherwise WRITE.\n *\n * Everything here decodes the token on the client FOR DISPLAY ONLY. Nothing is verified. Backends enforce the\n * session; never use these values to grant access.\n */\n\nexport const OPERATION_CLASSES = [\"READ\", \"WRITE\", \"DESTRUCTIVE\", \"FINANCIAL\", \"SECURITY\"] as const\nexport type OperationClass = (typeof OPERATION_CLASSES)[number]\nexport type ImpersonationMode = \"READ\" | \"WRITE\"\n\n/** Classes no support session can ever use, whatever a token says. */\nconst PLATFORM_BLOCKED: ReadonlySet<string> = new Set([\"FINANCIAL\", \"SECURITY\"])\n\nexport interface ImpersonationState {\n /** True only for an unexpired token with amr \"impersonation\", an actor and a session id. */\n active: boolean\n /** READ or WRITE; null when not active. */\n mode: ImpersonationMode | null\n /** Approved operation classes (never FINANCIAL or SECURITY); empty when not active. */\n ops: OperationClass[]\n /** ssoUserId of the support person acting (act.sub). */\n actorSub: string | null\n /** SUPER_ADMIN or ACAI_STAFF (act.userType), when the token says. */\n actorUserType: string | null\n /** The SSO impersonation session id (imp_sid). */\n sessionId: string | null\n /** When the token stops working. */\n expiresAt: Date | null\n}\n\nexport const INACTIVE_IMPERSONATION: ImpersonationState = Object.freeze({\n active: false,\n mode: null,\n ops: [],\n actorSub: null,\n actorUserType: null,\n sessionId: null,\n expiresAt: null,\n}) as ImpersonationState\n\nfunction base64UrlDecode(segment: string): string {\n const b64 = segment.replace(/-/g, \"+\").replace(/_/g, \"/\")\n const padded = b64 + \"=\".repeat((4 - (b64.length % 4)) % 4)\n const binary = atob(padded)\n const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0))\n return new TextDecoder().decode(bytes)\n}\n\n/** The JWT payload, or null when the token is not a decodable JWT. Not verified. */\nexport function decodeJwtPayload(token: string | null | undefined): Record<string, unknown> | null {\n if (typeof token !== \"string\") return null\n const parts = token.split(\".\")\n if (parts.length !== 3 || !parts[1]) return null\n try {\n const payload = JSON.parse(base64UrlDecode(parts[1])) as unknown\n return payload !== null && typeof payload === \"object\" && !Array.isArray(payload)\n ? (payload as Record<string, unknown>)\n : null\n } catch {\n return null\n }\n}\n\nconst nonEmpty = (v: unknown): string | null => (typeof v === \"string\" && v.trim() !== \"\" ? v : null)\n\n/** Approved classes, read the same way the backends do: imp_ops, else imp_mode WRITE = READ+WRITE, else READ. */\nexport function approvedOpsOf(claims: Record<string, unknown>): OperationClass[] {\n let ops: string[]\n if (Array.isArray(claims.imp_ops)) ops = claims.imp_ops.filter((v): v is string => typeof v === \"string\")\n else if (claims.imp_mode === \"WRITE\") ops = [\"READ\", \"WRITE\"]\n else ops = [\"READ\"]\n const set = new Set(ops.filter((op) => !PLATFORM_BLOCKED.has(op)))\n return OPERATION_CLASSES.filter((c) => set.has(c))\n}\n\nexport function modeOfOps(ops: readonly string[]): ImpersonationMode {\n return ops.every((op) => op === \"READ\") ? \"READ\" : \"WRITE\"\n}\n\n/** Decode an access token's impersonation state. For display only: the token is not verified. */\nexport function readImpersonation(token: string | null | undefined, now: number = Date.now()): ImpersonationState {\n const claims = decodeJwtPayload(token)\n if (!claims) return INACTIVE_IMPERSONATION\n const amr = Array.isArray(claims.amr) ? claims.amr : []\n if (!amr.includes(\"impersonation\")) return INACTIVE_IMPERSONATION\n const act = claims.act !== null && typeof claims.act === \"object\" ? (claims.act as Record<string, unknown>) : {}\n const actorSub = nonEmpty(act.sub)\n const sessionId = nonEmpty(claims.imp_sid)\n const expiresAt = typeof claims.exp === \"number\" ? new Date(claims.exp * 1000) : null\n if (!actorSub || !sessionId || !expiresAt || expiresAt.getTime() <= now) return INACTIVE_IMPERSONATION\n const ops = approvedOpsOf(claims)\n return {\n active: true,\n mode: modeOfOps(ops),\n ops,\n actorSub,\n actorUserType: nonEmpty(act.userType),\n sessionId,\n expiresAt,\n }\n}\n\n/**\n * `useImpersonation(token)`: the impersonation state of an access token, for display (banner, disabled controls).\n * Re-renders when the token expires, so `active` turns false on time. Not a security check.\n */\nexport function useImpersonation(token: string | null | undefined): ImpersonationState {\n // Bumped when the token expires; the state is always computed against the current clock.\n const [tick, setTick] = React.useState(0)\n // eslint-disable-next-line react-hooks/exhaustive-deps\n const state = React.useMemo(() => readImpersonation(token), [token, tick])\n const expiresAtMs = state.expiresAt?.getTime() ?? null\n\n React.useEffect(() => {\n if (expiresAtMs === null) return undefined\n const delay = Math.max(0, expiresAtMs - Date.now())\n // setTimeout caps at about 24.8 days; impersonation tokens live at most an hour.\n const timer = setTimeout(() => setTick((t) => t + 1), Math.min(delay + 50, 2_147_000_000))\n return () => clearTimeout(timer)\n }, [expiresAtMs])\n\n return state\n}\n"],"mappings":"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","names":[]}
|