@godxjp/server-sdk 0.10.0 → 0.11.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/README.md CHANGED
@@ -120,8 +120,9 @@ Every export per subpath, checked against `src/*.ts`.
120
120
 
121
121
  | Export | Signature (condensed) | One line |
122
122
  |---|---|---|
123
- | `Principal` | `interface { sub, organizationId, organizationContextId?, accessMode, scopes, serviceInstanceId?, clientId?, expiresAt, name?, email? }` | The verified, frozen identity. `organizationId` = claim `organization_id` (internal id); `organizationContextId` = claim `organization_context_id` (console id — the one `fetchEffectivePermissions` needs). `accessMode` defaults to `read_only` unless the claim says `full` (fail closed). |
124
- | `verifyAccessToken` | `(token, {issuer, serviceSlug, audience?}) => Promise<Principal>` | The one parse gate: JWKS signature, exact `iss`, `aud` (= instance slug, defaults to `serviceSlug`), `exp`, then requires `organization_id`. Failures quote expected vs actual `iss`/`aud`. |
123
+ | `Principal` | `interface { sub, organizationId, organizationContextId?, accessMode, scopes, serviceInstanceId?, clientId?, expiresAt, name?, email?, issuer?, resource?, tokenId?, issuedAt?, executionId?, argumentsDigest?, serviceAuthorization? }` | The verified, frozen identity. `organizationId` = claim `organization_id` (internal id); `organizationContextId` = claim `organization_context_id` (console id — the one `fetchEffectivePermissions` needs). `accessMode` defaults to `read_only` unless the claim says `full` (fail closed). (0.11.0) `issuer`/`resource` = what the token was verified against; `executionId` = `gx_execution_id` (mutation tokens only); `argumentsDigest` = `gx_arguments_sha256`; `serviceAuthorization` = the embedded snapshot of a one-call execution token, kept only when its `execution_id` and `service` are this token's and this audience's. |
124
+ | `EmbeddedAuthorization` | `interface { authoritative: true, service, role, permissions, branchId, evaluatedAt, executionId }` | The `service_authorization` claim Platform embeds in a one-call execution token (godx-jp/id#2190): what `POST /api/sso/introspect` would answer, computed live at minting. (0.11.0) |
125
+ | `verifyAccessToken` | `(token, {issuer, serviceSlug, audience?, http?}) => Promise<Principal>` | The one parse gate: JWKS signature, exact `iss`, `aud` (= instance slug, defaults to `serviceSlug`), `exp`, then requires `organization_id`. Failures quote expected vs actual `iss`/`aud`. |
125
126
  | `Pkce` | `interface { verifier, challenge, method: "S256" }` | A PKCE pair. |
126
127
  | `createPkce` | `() => Promise<Pkce>` | S256 challenge via WebCrypto. |
127
128
  | `randomToken` | `(bytes = 16) => string` | base64url random token (state, nonce). |
@@ -134,6 +135,30 @@ Every export per subpath, checked against `src/*.ts`.
134
135
  |---|---|---|
135
136
  | `EffectiveAccess` | `interface { permissions, roles, serviceAccess, contractVersion?, evaluatedAt?, authoritative }` | The effective-access read model. |
136
137
  | `fetchEffectivePermissions` | `({issuer, accessToken, organizationId, branchId?, http?}) => Promise<EffectiveAccess>` | `GET /api/sso/me/permissions`. Pass `principal.organizationContextId` (console id) — the internal `organizationId` answers 403 CONTEXT_UNAVAILABLE; `authoritative` passed through as-is — never cache a non-authoritative answer as a positive grant. |
138
+ | `embeddedAccess` | `(principal) => EffectiveAccess \| null` | The permissions a one-call execution token carries (`principal.serviceAuthorization`) as `EffectiveAccess` — zero Platform calls; `null` for any other token, then fall back to `fetchEffectivePermissions`. (0.11.0) |
139
+ | `canonicalArgumentsDigest` | `(args) => Promise<string>` | SHA-256 hex of the arguments in the canonical form Platform locks on an approved execution (`gx_arguments_sha256`): keys sorted recursively by code point, lists kept, `{}` → `[]`, an object keyed `0..n-1` → list. Byte-identical to php-sdk's `CanonicalArguments::digest` (tests pin three PHP-computed digests). Floats are not digest-stable across languages — send decimals as strings. (0.11.0) |
140
+ | `ExecutionOnceOnly` | `class { constructor(store: OnceOnlyStore); claim(principal, args?) => Promise<boolean>; static key(principal) => Promise<string> }` | The service's once-only gate for a gx execution: true exactly once per (issuer, resource, executionId); false on replay, for a token without an execution, and — when `args` are given — for arguments whose digest is not `principal.argumentsDigest`. Call it BEFORE the write, on a committed store. Rows are kept `ONCE_ONLY_RETENTION_SECONDS` (7 days) past `exp`; key = `sha256("gx-execution\|iss\|resource\|executionId")`, same as php-sdk. (0.11.0) |
141
+ | `OnceOnlyStore` | `interface { claim(key, keepUntil) => Promise<boolean> }` | A committed, uniquely-keyed store (php-sdk's `ReplayStore` contract). |
142
+ | `d1OnceOnlyStore` / `memoryOnceOnlyStore` | `(db) => OnceOnlyStore` / `(now?) => OnceOnlyStore` | D1 store (`INSERT OR IGNORE`, then read the owner back — no reliance on `meta.changes`; call it OUTSIDE your own `batch` so the row commits first) and a per-isolate store for tests and dev servers only. |
143
+ | `ONCE_ONLY_TABLE_SQL` / `pruneOnceOnlySql` / `ONCE_ONLY_RETENTION_SECONDS` | `string` / `string` / `604800` | DDL for `godx_once_only` (copy into a migration verbatim); the prune statement for a Cron Trigger (bind current Unix seconds); the retention. |
144
+
145
+ #### Executing a gx mutation with zero Platform calls (0.11.0)
146
+
147
+ ```ts
148
+ import { verifyAccessToken } from "@godxjp/server-sdk/auth";
149
+ import { ExecutionOnceOnly, d1OnceOnlyStore, embeddedAccess, fetchEffectivePermissions } from "@godxjp/server-sdk/authorization";
150
+
151
+ const principal = await verifyAccessToken(token, { issuer, serviceSlug, audience: resourceUri });
152
+ // 1. permissions: from the token when it is a one-call execution token, else one Platform call
153
+ const access = embeddedAccess(principal) ?? (await fetchEffectivePermissions({ issuer, accessToken: token, organizationId: principal.organizationContextId! }));
154
+ if (!access.permissions.includes("task.create")) return forbidden();
155
+ // 2. once-only + arguments check, COMMITTED before any side effect (outside your own D1 batch)
156
+ const once = new ExecutionOnceOnly(d1OnceOnlyStore(env.DB));
157
+ if (!(await once.claim(principal, args))) return conflict("execution already ran, or arguments differ from the approved ones");
158
+ // 3. your write, your transaction — then report the outcome AFTER the response (ctx.waitUntil)
159
+ ```
160
+
161
+ The token lives 45 s and is bound to your resource, the approved arguments and the caller's DPoP key; verify the DPoP proof as for any gx token. Contract: godx-jp/id `docs/guide/gx-consumer-registration.md`, "One call, one token, zero Platform calls on the service (#2190)".
137
162
 
138
163
  ### `./http`
139
164
 
@@ -576,7 +601,15 @@ CREATE INDEX IF NOT EXISTS outbox_unpublished_idx ON outbox (published_at, seque
576
601
  npx vitest run # from packages/server-sdk
577
602
  ```
578
603
 
579
- 40 tests, doubling as executable behaviour docs:
604
+ 74 tests, doubling as executable behaviour docs:
605
+
606
+ - `auth.test.ts` (2): a signed one-call execution token through the real parse gate — issuer,
607
+ resource, jti, iat, execution id, argument digest, embedded snapshot kept; a snapshot for another
608
+ execution / another service / non-authoritative dropped; a read token carries no execution.
609
+ - `authorization.test.ts` (9): canonical digest parity with php-sdk (three PHP-computed digests:
610
+ nested sort, `{}` → `[]`, object keyed 0..n-1 → list, code-point key order), once-only key parity,
611
+ claim-once + 7-day retention, refusals (read token, tampered arguments, digest-less token), D1 and
612
+ memory stores, `embeddedAccess` mapping.
580
613
 
581
614
  - `sync.test.ts` (23): envelope validation (specversion/subject/source pinning), webhook
582
615
  sign/verify (tamper, replay, rotation, raw-secret fallback), processor outcomes (duplicate,
package/dist/auth.d.ts CHANGED
@@ -22,6 +22,39 @@ interface Principal {
22
22
  /** From userinfo, when the adapter fetched it at login. */
23
23
  readonly name?: string;
24
24
  readonly email?: string;
25
+ /** The exact `iss` the token was verified against (discovery issuer). Absent on sessions sealed before 0.11.0. */
26
+ readonly issuer?: string;
27
+ /** The audience the token was verified against — your resource URI / instance slug. Absent on sessions sealed before 0.11.0. */
28
+ readonly resource?: string;
29
+ /** Claim `jti`. */
30
+ readonly tokenId?: string;
31
+ /** Unix seconds, from `iat`. */
32
+ readonly issuedAt?: number;
33
+ /**
34
+ * Claim `gx_execution_id` — set only on a gx MUTATION token (a one-call execution token or a
35
+ * checkpointed one); never on a read token. The service's once-only gate keys on it (godx-jp/id#2190).
36
+ */
37
+ readonly executionId?: string;
38
+ /** Claim `gx_arguments_sha256` — SHA-256 hex of the approved arguments in canonical form (one-call execution token). */
39
+ readonly argumentsDigest?: string;
40
+ /**
41
+ * Claim `service_authorization` of a one-call execution token: byte-for-byte what
42
+ * `POST /api/sso/introspect` would answer for this token, computed live at minting (F = 0). Only
43
+ * present when its `execution_id` names THIS token's execution and its `service` is THIS audience —
44
+ * otherwise it is dropped at parse and the caller falls back to `fetchEffectivePermissions`.
45
+ */
46
+ readonly serviceAuthorization?: EmbeddedAuthorization;
47
+ }
48
+ /** The `service_authorization` snapshot Platform embeds in a one-call execution token (godx-jp/id#2190). */
49
+ interface EmbeddedAuthorization {
50
+ readonly authoritative: true;
51
+ readonly service: string;
52
+ readonly role: string | null;
53
+ readonly permissions: readonly string[];
54
+ readonly branchId: string | null;
55
+ /** ISO-8601, the moment Platform computed the snapshot — a few ms before `iat`. */
56
+ readonly evaluatedAt: string;
57
+ readonly executionId: string;
25
58
  }
26
59
  /**
27
60
  * The one parse gate. Validates signature (JWKS), iss (discovery issuer,
@@ -47,4 +80,4 @@ declare function sealData(payload: Record<string, unknown>, secret: string, ttlS
47
80
  /** Returns null for anything invalid or expired — never throws on bad input. */
48
81
  declare function openData<T = Record<string, unknown>>(token: string, secret: string): Promise<T | null>;
49
82
 
50
- export { type Pkce, type Principal, createPkce, openData, randomToken, sealData, verifyAccessToken };
83
+ export { type EmbeddedAuthorization, type Pkce, type Principal, createPkce, openData, randomToken, sealData, verifyAccessToken };
package/dist/auth.js CHANGED
@@ -4,8 +4,8 @@ import {
4
4
  randomToken,
5
5
  sealData,
6
6
  verifyAccessToken
7
- } from "./chunk-KJCSW2NE.js";
8
- import "./chunk-6XZK5EKW.js";
7
+ } from "./chunk-K3AG5TCN.js";
8
+ import "./chunk-2N6BMNKT.js";
9
9
  export {
10
10
  createPkce,
11
11
  openData,
@@ -1,3 +1,4 @@
1
+ import { Principal } from './auth.js';
1
2
  import { HttpOptions } from './http.js';
2
3
  import 'jose';
3
4
 
@@ -27,5 +28,72 @@ declare function fetchEffectivePermissions(params: {
27
28
  /** `headers` (send your X-Request-Id), `timeoutMs` (a hung Platform must not hang you), `fetch`. */
28
29
  http?: HttpOptions;
29
30
  }): Promise<EffectiveAccess>;
31
+ /**
32
+ * The permissions a one-call execution token carries with it (godx-jp/id#2190): `EffectiveAccess`
33
+ * built from `principal.serviceAuthorization`, so a mutation needs ZERO Platform calls on the service.
34
+ * `null` for any other token — then call `fetchEffectivePermissions` as before. The snapshot was
35
+ * computed live at minting and the token lives 45 s, so its age is bounded by `exp`; nothing here is
36
+ * cached beyond the token itself.
37
+ */
38
+ declare function embeddedAccess(principal: Principal): EffectiveAccess | null;
39
+ /**
40
+ * The argument digest Platform locks on an approved execution and puts in the one-call execution
41
+ * token as `gx_arguments_sha256` — computed HERE the same way as php-sdk's CanonicalArguments and
42
+ * Platform's ActionIntentService::digest(): object keys sorted recursively (byte order, as PHP's
43
+ * `SORT_STRING`), lists kept in order, JSON without escaped slashes or unicode, SHA-256 hex.
44
+ *
45
+ * PHP decodes JSON into arrays, and two of its habits are mirrored on purpose: an EMPTY object
46
+ * digests as `[]`, and an object whose sorted keys read exactly "0","1",…,"n-1" digests as a list.
47
+ * Floats are the one thing no JS code can mirror (`1.0` keeps its zero fraction in PHP, JS has no
48
+ * such number): send decimals as strings through gx, as the platform guide says.
49
+ */
50
+ declare function canonicalArgumentsDigest(args: Record<string, unknown> | unknown[]): Promise<string>;
51
+ /**
52
+ * A committed, uniquely-keyed store: `claim` returns true exactly once per key until `keepUntil`
53
+ * (Unix seconds) has passed. Same contract as php-sdk's ReplayStore.
54
+ */
55
+ interface OnceOnlyStore {
56
+ claim(key: string, keepUntil: number): Promise<boolean>;
57
+ }
58
+ /** Structural subset of D1Database, so this module has no CF type dependency (see ./producer). */
59
+ interface D1Like {
60
+ prepare(sql: string): {
61
+ bind(...values: unknown[]): {
62
+ first<T = unknown>(): Promise<T | null>;
63
+ run(): Promise<unknown>;
64
+ };
65
+ };
66
+ }
67
+ /** DDL for the once-only table — copy into a migration verbatim. Prune with `pruneOnceOnlySql`. */
68
+ declare const ONCE_ONLY_TABLE_SQL = "CREATE TABLE IF NOT EXISTS godx_once_only (\n claim_key TEXT PRIMARY KEY,\n owner TEXT NOT NULL,\n keep_until INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS godx_once_only_keep_until ON godx_once_only (keep_until);";
69
+ /** Run from a Cron Trigger (bind the current Unix seconds); rows are kept 7 days past the token's exp on purpose. */
70
+ declare const pruneOnceOnlySql = "DELETE FROM godx_once_only WHERE keep_until < ?";
71
+ /**
72
+ * D1-backed store. Insert-or-ignore then read the owner back, so the verdict does not depend on
73
+ * `meta.changes` being reported. The row must COMMIT before any side effect, which on D1 means:
74
+ * call `claim` outside your own `batch`, not inside it.
75
+ */
76
+ declare function d1OnceOnlyStore(db: D1Like): OnceOnlyStore;
77
+ /** Per-isolate store for tests and single-process dev servers. NOT once-only across isolates or restarts. */
78
+ declare function memoryOnceOnlyStore(now?: () => number): OnceOnlyStore;
79
+ /** How long a consumed execution stays on record after its token expired (php-sdk: the same 7 days). */
80
+ declare const ONCE_ONLY_RETENTION_SECONDS: number;
81
+ /**
82
+ * The service's own once-only gate for a gx execution (godx-jp/id#2190). With the one-call token the
83
+ * service no longer asks Platform to checkpoint before each write, so "this execution runs at most
84
+ * once" is the service's promise: `claim` BEFORE applying the write; a retry, a replayed request or a
85
+ * second worker gets false. The row outlives the token by ONCE_ONLY_RETENTION_SECONDS — a row pruned
86
+ * at exp plus verifier leeway is a replay window — and, when the tool arguments are handed over, they
87
+ * must digest to the token's `gx_arguments_sha256` (the comparison the synchronous checkpoint used to
88
+ * make). Mirrors php-sdk's ExecutionOnceOnly, key included.
89
+ */
90
+ declare class ExecutionOnceOnly {
91
+ private readonly store;
92
+ constructor(store: OnceOnlyStore);
93
+ /** True exactly once per (issuer, resource, execution id); false on replay, for a token without an execution, and for arguments that are not the approved ones. */
94
+ claim(principal: Principal, args?: Record<string, unknown> | unknown[]): Promise<boolean>;
95
+ /** sha256("gx-execution|" + issuer + "|" + resource + "|" + executionId) — byte-identical to php-sdk's key. */
96
+ static key(principal: Principal): Promise<string>;
97
+ }
30
98
 
31
- export { type EffectiveAccess, fetchEffectivePermissions };
99
+ export { type EffectiveAccess, ExecutionOnceOnly, ONCE_ONLY_RETENTION_SECONDS, ONCE_ONLY_TABLE_SQL, type OnceOnlyStore, canonicalArgumentsDigest, d1OnceOnlyStore, embeddedAccess, fetchEffectivePermissions, memoryOnceOnlyStore, pruneOnceOnlySql };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  GodxHttpError,
3
3
  platformFetch
4
- } from "./chunk-6XZK5EKW.js";
4
+ } from "./chunk-2N6BMNKT.js";
5
5
 
6
6
  // src/authorization.ts
7
7
  async function fetchEffectivePermissions(params) {
@@ -26,7 +26,115 @@ async function fetchEffectivePermissions(params) {
26
26
  authoritative: body.authoritative === true
27
27
  };
28
28
  }
29
+ function embeddedAccess(principal) {
30
+ const snapshot = principal.serviceAuthorization;
31
+ if (!snapshot) return null;
32
+ return {
33
+ permissions: [...snapshot.permissions],
34
+ roles: snapshot.role ? [snapshot.role] : [],
35
+ serviceAccess: {},
36
+ evaluatedAt: snapshot.evaluatedAt,
37
+ authoritative: true
38
+ };
39
+ }
40
+ async function canonicalArgumentsDigest(args) {
41
+ const json = JSON.stringify(canonical(args));
42
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(json));
43
+ return hex(new Uint8Array(digest));
44
+ }
45
+ function canonical(value) {
46
+ if (Array.isArray(value)) return value.map(canonical);
47
+ if (typeof value !== "object" || value === null) return value;
48
+ const keys = Object.keys(value).sort(compareCodePoints);
49
+ const record = value;
50
+ if (keys.every((key, index) => key === String(index))) {
51
+ return keys.map((key) => canonical(record[key]));
52
+ }
53
+ const out = {};
54
+ for (const key of keys) out[key] = canonical(record[key]);
55
+ return out;
56
+ }
57
+ function compareCodePoints(a, b) {
58
+ const left = Array.from(a, (c) => c.codePointAt(0) ?? 0);
59
+ const right = Array.from(b, (c) => c.codePointAt(0) ?? 0);
60
+ for (let i = 0; i < Math.min(left.length, right.length); i++) {
61
+ if (left[i] !== right[i]) return (left[i] ?? 0) - (right[i] ?? 0);
62
+ }
63
+ return left.length - right.length;
64
+ }
65
+ function hex(bytes) {
66
+ let out = "";
67
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
68
+ return out;
69
+ }
70
+ function digestsEqual(a, b) {
71
+ if (a.length !== b.length) return false;
72
+ let diff = 0;
73
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
74
+ return diff === 0;
75
+ }
76
+ var ONCE_ONLY_TABLE_SQL = `CREATE TABLE IF NOT EXISTS godx_once_only (
77
+ claim_key TEXT PRIMARY KEY,
78
+ owner TEXT NOT NULL,
79
+ keep_until INTEGER NOT NULL
80
+ );
81
+ CREATE INDEX IF NOT EXISTS godx_once_only_keep_until ON godx_once_only (keep_until);`;
82
+ var pruneOnceOnlySql = "DELETE FROM godx_once_only WHERE keep_until < ?";
83
+ function d1OnceOnlyStore(db) {
84
+ return {
85
+ async claim(key, keepUntil) {
86
+ const owner = crypto.randomUUID();
87
+ await db.prepare("INSERT OR IGNORE INTO godx_once_only (claim_key, owner, keep_until) VALUES (?, ?, ?)").bind(key, owner, keepUntil).run();
88
+ const row = await db.prepare("SELECT owner FROM godx_once_only WHERE claim_key = ?").bind(key).first();
89
+ return row?.owner === owner;
90
+ }
91
+ };
92
+ }
93
+ function memoryOnceOnlyStore(now = () => Math.floor(Date.now() / 1e3)) {
94
+ const rows = /* @__PURE__ */ new Map();
95
+ return {
96
+ async claim(key, keepUntil) {
97
+ const current = now();
98
+ for (const [k, until] of rows) if (until < current) rows.delete(k);
99
+ if (rows.has(key)) return false;
100
+ rows.set(key, keepUntil);
101
+ return true;
102
+ }
103
+ };
104
+ }
105
+ var ONCE_ONLY_RETENTION_SECONDS = 7 * 86400;
106
+ var ExecutionOnceOnly = class _ExecutionOnceOnly {
107
+ constructor(store) {
108
+ this.store = store;
109
+ }
110
+ store;
111
+ /** True exactly once per (issuer, resource, execution id); false on replay, for a token without an execution, and for arguments that are not the approved ones. */
112
+ async claim(principal, args) {
113
+ if (!principal.executionId) return false;
114
+ if (args !== void 0) {
115
+ if (!principal.argumentsDigest) return false;
116
+ if (!digestsEqual(principal.argumentsDigest, await canonicalArgumentsDigest(args))) return false;
117
+ }
118
+ return this.store.claim(await _ExecutionOnceOnly.key(principal), principal.expiresAt + ONCE_ONLY_RETENTION_SECONDS);
119
+ }
120
+ /** sha256("gx-execution|" + issuer + "|" + resource + "|" + executionId) — byte-identical to php-sdk's key. */
121
+ static async key(principal) {
122
+ if (!principal.issuer || !principal.resource || !principal.executionId) {
123
+ throw new Error("ExecutionOnceOnly needs a Principal from verifyAccessToken \u2265 0.11.0 carrying issuer, resource and executionId");
124
+ }
125
+ const material = `gx-execution|${principal.issuer}|${principal.resource}|${principal.executionId}`;
126
+ return hex(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(material))));
127
+ }
128
+ };
29
129
  export {
30
- fetchEffectivePermissions
130
+ ExecutionOnceOnly,
131
+ ONCE_ONLY_RETENTION_SECONDS,
132
+ ONCE_ONLY_TABLE_SQL,
133
+ canonicalArgumentsDigest,
134
+ d1OnceOnlyStore,
135
+ embeddedAccess,
136
+ fetchEffectivePermissions,
137
+ memoryOnceOnlyStore,
138
+ pruneOnceOnlySql
31
139
  };
32
140
  //# sourceMappingURL=authorization.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/authorization.ts"],"sourcesContent":["/**\n * ./authorization — effective-access read model. Mirrors php-sdk's\n * Authorization module: results carry the platform's authoritative flag,\n * nothing is cached as a positive grant here.\n */\nimport { GodxHttpError, type HttpOptions, platformFetch } from \"./http\";\n\nexport interface EffectiveAccess {\n permissions: string[];\n roles: string[];\n serviceAccess: Record<string, unknown>;\n contractVersion?: string;\n evaluatedAt?: string;\n authoritative: boolean;\n}\n\nexport async function fetchEffectivePermissions(params: {\n issuer: string;\n accessToken: string;\n /**\n * The CONSOLE organization id — `principal.organizationContextId`, not\n * `principal.organizationId` (the internal id answers 403 CONTEXT_UNAVAILABLE).\n */\n organizationId: string;\n branchId?: string;\n /** `headers` (send your X-Request-Id), `timeoutMs` (a hung Platform must not hang you), `fetch`. */\n http?: HttpOptions;\n}): Promise<EffectiveAccess> {\n const url = new URL(\"/api/sso/me/permissions\", params.issuer);\n url.search = new URLSearchParams({\n organization_id: params.organizationId,\n ...(params.branchId ? { branch_id: params.branchId } : {}),\n }).toString();\n\n const res = await platformFetch(url, {\n headers: { Authorization: `Bearer ${params.accessToken}` },\n }, params.http);\n if (!res.ok) {\n throw new GodxHttpError(res.status, await res.text(), \"effective permissions failed\");\n }\n const body = (await res.json()) as {\n permissions?: string[];\n roles?: string[];\n service_access?: Record<string, unknown>;\n contract_version?: string;\n evaluated_at?: string;\n authoritative?: boolean;\n };\n return {\n permissions: body.permissions ?? [],\n roles: body.roles ?? [],\n serviceAccess: body.service_access ?? {},\n contractVersion: body.contract_version,\n evaluatedAt: body.evaluated_at,\n authoritative: body.authoritative === true,\n };\n}\n"],"mappings":";;;;;;AAgBA,eAAsB,0BAA0B,QAWnB;AAC3B,QAAM,MAAM,IAAI,IAAI,2BAA2B,OAAO,MAAM;AAC5D,MAAI,SAAS,IAAI,gBAAgB;AAAA,IAC/B,iBAAiB,OAAO;AAAA,IACxB,GAAI,OAAO,WAAW,EAAE,WAAW,OAAO,SAAS,IAAI,CAAC;AAAA,EAC1D,CAAC,EAAE,SAAS;AAEZ,QAAM,MAAM,MAAM,cAAc,KAAK;AAAA,IACnC,SAAS,EAAE,eAAe,UAAU,OAAO,WAAW,GAAG;AAAA,EAC3D,GAAG,OAAO,IAAI;AACd,MAAI,CAAC,IAAI,IAAI;AACX,UAAM,IAAI,cAAc,IAAI,QAAQ,MAAM,IAAI,KAAK,GAAG,8BAA8B;AAAA,EACtF;AACA,QAAM,OAAQ,MAAM,IAAI,KAAK;AAQ7B,SAAO;AAAA,IACL,aAAa,KAAK,eAAe,CAAC;AAAA,IAClC,OAAO,KAAK,SAAS,CAAC;AAAA,IACtB,eAAe,KAAK,kBAAkB,CAAC;AAAA,IACvC,iBAAiB,KAAK;AAAA,IACtB,aAAa,KAAK;AAAA,IAClB,eAAe,KAAK,kBAAkB;AAAA,EACxC;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/authorization.ts"],"sourcesContent":["/**\n * ./authorization — effective-access read model. Mirrors php-sdk's\n * Authorization module: results carry the platform's authoritative flag,\n * nothing is cached as a positive grant here.\n */\nimport type { Principal } from \"./auth\";\nimport { GodxHttpError, type HttpOptions, platformFetch } from \"./http\";\n\nexport interface EffectiveAccess {\n permissions: string[];\n roles: string[];\n serviceAccess: Record<string, unknown>;\n contractVersion?: string;\n evaluatedAt?: string;\n authoritative: boolean;\n}\n\nexport async function fetchEffectivePermissions(params: {\n issuer: string;\n accessToken: string;\n /**\n * The CONSOLE organization id — `principal.organizationContextId`, not\n * `principal.organizationId` (the internal id answers 403 CONTEXT_UNAVAILABLE).\n */\n organizationId: string;\n branchId?: string;\n /** `headers` (send your X-Request-Id), `timeoutMs` (a hung Platform must not hang you), `fetch`. */\n http?: HttpOptions;\n}): Promise<EffectiveAccess> {\n const url = new URL(\"/api/sso/me/permissions\", params.issuer);\n url.search = new URLSearchParams({\n organization_id: params.organizationId,\n ...(params.branchId ? { branch_id: params.branchId } : {}),\n }).toString();\n\n const res = await platformFetch(url, {\n headers: { Authorization: `Bearer ${params.accessToken}` },\n }, params.http);\n if (!res.ok) {\n throw new GodxHttpError(res.status, await res.text(), \"effective permissions failed\");\n }\n const body = (await res.json()) as {\n permissions?: string[];\n roles?: string[];\n service_access?: Record<string, unknown>;\n contract_version?: string;\n evaluated_at?: string;\n authoritative?: boolean;\n };\n return {\n permissions: body.permissions ?? [],\n roles: body.roles ?? [],\n serviceAccess: body.service_access ?? {},\n contractVersion: body.contract_version,\n evaluatedAt: body.evaluated_at,\n authoritative: body.authoritative === true,\n };\n}\n\n/**\n * The permissions a one-call execution token carries with it (godx-jp/id#2190): `EffectiveAccess`\n * built from `principal.serviceAuthorization`, so a mutation needs ZERO Platform calls on the service.\n * `null` for any other token — then call `fetchEffectivePermissions` as before. The snapshot was\n * computed live at minting and the token lives 45 s, so its age is bounded by `exp`; nothing here is\n * cached beyond the token itself.\n */\nexport function embeddedAccess(principal: Principal): EffectiveAccess | null {\n const snapshot = principal.serviceAuthorization;\n if (!snapshot) return null;\n return {\n permissions: [...snapshot.permissions],\n roles: snapshot.role ? [snapshot.role] : [],\n serviceAccess: {},\n evaluatedAt: snapshot.evaluatedAt,\n authoritative: true,\n };\n}\n\n/**\n * The argument digest Platform locks on an approved execution and puts in the one-call execution\n * token as `gx_arguments_sha256` — computed HERE the same way as php-sdk's CanonicalArguments and\n * Platform's ActionIntentService::digest(): object keys sorted recursively (byte order, as PHP's\n * `SORT_STRING`), lists kept in order, JSON without escaped slashes or unicode, SHA-256 hex.\n *\n * PHP decodes JSON into arrays, and two of its habits are mirrored on purpose: an EMPTY object\n * digests as `[]`, and an object whose sorted keys read exactly \"0\",\"1\",…,\"n-1\" digests as a list.\n * Floats are the one thing no JS code can mirror (`1.0` keeps its zero fraction in PHP, JS has no\n * such number): send decimals as strings through gx, as the platform guide says.\n */\nexport async function canonicalArgumentsDigest(args: Record<string, unknown> | unknown[]): Promise<string> {\n const json = JSON.stringify(canonical(args));\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(json));\n return hex(new Uint8Array(digest));\n}\n\nfunction canonical(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonical);\n if (typeof value !== \"object\" || value === null) return value;\n const keys = Object.keys(value as Record<string, unknown>).sort(compareCodePoints);\n const record = value as Record<string, unknown>;\n if (keys.every((key, index) => key === String(index))) {\n return keys.map((key) => canonical(record[key]));\n }\n const out: Record<string, unknown> = {};\n for (const key of keys) out[key] = canonical(record[key]);\n return out;\n}\n\n/** UTF-8 byte order == code point order; JS's default sort compares UTF-16 units, which differs above U+D7FF. */\nfunction compareCodePoints(a: string, b: string): number {\n const left = Array.from(a, (c) => c.codePointAt(0) ?? 0);\n const right = Array.from(b, (c) => c.codePointAt(0) ?? 0);\n for (let i = 0; i < Math.min(left.length, right.length); i++) {\n if (left[i] !== right[i]) return (left[i] ?? 0) - (right[i] ?? 0);\n }\n return left.length - right.length;\n}\n\nfunction hex(bytes: Uint8Array): string {\n let out = \"\";\n for (const b of bytes) out += b.toString(16).padStart(2, \"0\");\n return out;\n}\n\n/** Constant-time equality for two hex digests. */\nfunction digestsEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);\n return diff === 0;\n}\n\n/**\n * A committed, uniquely-keyed store: `claim` returns true exactly once per key until `keepUntil`\n * (Unix seconds) has passed. Same contract as php-sdk's ReplayStore.\n */\nexport interface OnceOnlyStore {\n claim(key: string, keepUntil: number): Promise<boolean>;\n}\n\n/** Structural subset of D1Database, so this module has no CF type dependency (see ./producer). */\ninterface D1Like {\n prepare(sql: string): {\n bind(...values: unknown[]): {\n first<T = unknown>(): Promise<T | null>;\n run(): Promise<unknown>;\n };\n };\n}\n\n/** DDL for the once-only table — copy into a migration verbatim. Prune with `pruneOnceOnlySql`. */\nexport const ONCE_ONLY_TABLE_SQL = `CREATE TABLE IF NOT EXISTS godx_once_only (\n claim_key TEXT PRIMARY KEY,\n owner TEXT NOT NULL,\n keep_until INTEGER NOT NULL\n);\nCREATE INDEX IF NOT EXISTS godx_once_only_keep_until ON godx_once_only (keep_until);`;\n\n/** Run from a Cron Trigger (bind the current Unix seconds); rows are kept 7 days past the token's exp on purpose. */\nexport const pruneOnceOnlySql = \"DELETE FROM godx_once_only WHERE keep_until < ?\";\n\n/**\n * D1-backed store. Insert-or-ignore then read the owner back, so the verdict does not depend on\n * `meta.changes` being reported. The row must COMMIT before any side effect, which on D1 means:\n * call `claim` outside your own `batch`, not inside it.\n */\nexport function d1OnceOnlyStore(db: D1Like): OnceOnlyStore {\n return {\n async claim(key, keepUntil) {\n const owner = crypto.randomUUID();\n await db\n .prepare(\"INSERT OR IGNORE INTO godx_once_only (claim_key, owner, keep_until) VALUES (?, ?, ?)\")\n .bind(key, owner, keepUntil)\n .run();\n const row = await db.prepare(\"SELECT owner FROM godx_once_only WHERE claim_key = ?\").bind(key).first<{ owner: string }>();\n return row?.owner === owner;\n },\n };\n}\n\n/** Per-isolate store for tests and single-process dev servers. NOT once-only across isolates or restarts. */\nexport function memoryOnceOnlyStore(now: () => number = () => Math.floor(Date.now() / 1000)): OnceOnlyStore {\n const rows = new Map<string, number>();\n return {\n async claim(key, keepUntil) {\n const current = now();\n for (const [k, until] of rows) if (until < current) rows.delete(k);\n if (rows.has(key)) return false;\n rows.set(key, keepUntil);\n return true;\n },\n };\n}\n\n/** How long a consumed execution stays on record after its token expired (php-sdk: the same 7 days). */\nexport const ONCE_ONLY_RETENTION_SECONDS = 7 * 86400;\n\n/**\n * The service's own once-only gate for a gx execution (godx-jp/id#2190). With the one-call token the\n * service no longer asks Platform to checkpoint before each write, so \"this execution runs at most\n * once\" is the service's promise: `claim` BEFORE applying the write; a retry, a replayed request or a\n * second worker gets false. The row outlives the token by ONCE_ONLY_RETENTION_SECONDS — a row pruned\n * at exp plus verifier leeway is a replay window — and, when the tool arguments are handed over, they\n * must digest to the token's `gx_arguments_sha256` (the comparison the synchronous checkpoint used to\n * make). Mirrors php-sdk's ExecutionOnceOnly, key included.\n */\nexport class ExecutionOnceOnly {\n constructor(private readonly store: OnceOnlyStore) {}\n\n /** True exactly once per (issuer, resource, execution id); false on replay, for a token without an execution, and for arguments that are not the approved ones. */\n async claim(principal: Principal, args?: Record<string, unknown> | unknown[]): Promise<boolean> {\n if (!principal.executionId) return false;\n if (args !== undefined) {\n if (!principal.argumentsDigest) return false;\n if (!digestsEqual(principal.argumentsDigest, await canonicalArgumentsDigest(args))) return false;\n }\n return this.store.claim(await ExecutionOnceOnly.key(principal), principal.expiresAt + ONCE_ONLY_RETENTION_SECONDS);\n }\n\n /** sha256(\"gx-execution|\" + issuer + \"|\" + resource + \"|\" + executionId) — byte-identical to php-sdk's key. */\n static async key(principal: Principal): Promise<string> {\n if (!principal.issuer || !principal.resource || !principal.executionId) {\n throw new Error(\"ExecutionOnceOnly needs a Principal from verifyAccessToken ≥ 0.11.0 carrying issuer, resource and executionId\");\n }\n const material = `gx-execution|${principal.issuer}|${principal.resource}|${principal.executionId}`;\n return hex(new Uint8Array(await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(material))));\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,5 @@
1
1
  // src/http.ts
2
- import { createRemoteJWKSet } from "jose";
2
+ import { createRemoteJWKSet, customFetch } from "jose";
3
3
  var GodxTimeoutError = class extends Error {
4
4
  url;
5
5
  timeoutMs;
@@ -61,7 +61,10 @@ function loadIssuer(issuerUrl, options = {}) {
61
61
  const jwks = createRemoteJWKSet(new URL(metadata.jwks_uri), {
62
62
  ...options.timeoutMs ? { timeoutDuration: options.timeoutMs } : {},
63
63
  ...jwksHeaders ? { headers: jwksHeaders } : {},
64
- ...options.fetch ? { [/* @__PURE__ */ Symbol.for("jose.fetch")]: options.fetch } : {}
64
+ // jose keys its fetch override by its OWN exported symbol (a plain Symbol(), not
65
+ // Symbol.for): 0.10.0 passed Symbol.for("jose.fetch") and the JWKS fetch silently ignored
66
+ // `http.fetch` — caught by pages' lead (godx-jp/server-sdk#19).
67
+ ...options.fetch ? { [customFetch]: options.fetch } : {}
65
68
  });
66
69
  return { metadata, jwks };
67
70
  })();
@@ -177,4 +180,4 @@ export {
177
180
  GodxSsoClient,
178
181
  createServiceApi
179
182
  };
180
- //# sourceMappingURL=chunk-6XZK5EKW.js.map
183
+ //# sourceMappingURL=chunk-2N6BMNKT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/http.ts"],"sourcesContent":["/**\n * ./http — endpoint contract from OIDC discovery, the SSO client, and the\n * server-to-server service API. Runtime-agnostic: fetch + WebCrypto only.\n * Mirrors php-sdk's Http module boundary.\n */\nimport { createRemoteJWKSet, customFetch } from \"jose\";\n\n/**\n * Per-call knobs for every outbound request this SDK makes (godx-jp/server-sdk#17, from the\n * pages.godx.jp incident of 2026-10-04 where a Platform call hung 25–180 s with no request id\n * to find it by). Pass them on the client/config (`http`) or per call where a function takes\n * them; defaults are exactly the old behaviour.\n */\nexport interface HttpOptions {\n /**\n * Extra headers on every request — `X-Request-Id` is the one that makes a hang findable in\n * Platform's log. A function is called per request (mint a fresh id each time); a header the\n * SDK itself sets (Authorization, Content-Type, X-Service-*) always wins.\n */\n headers?: Record<string, string> | (() => Record<string, string>);\n /** Abort a request after this many milliseconds and throw {@link GodxTimeoutError}. Unset = the runtime's default (often none). */\n timeoutMs?: number;\n /** The fetch to use (tests, custom dispatchers). Default `globalThis.fetch`, read at call time. */\n fetch?: typeof fetch;\n}\n\n/** The request did not answer within `timeoutMs`; `url` and `timeoutMs` say which and how long. */\nexport class GodxTimeoutError extends Error {\n readonly url: string;\n readonly timeoutMs: number;\n constructor(url: string, timeoutMs: number) {\n super(`GoDX platform request to ${url} did not answer within ${timeoutMs} ms`);\n this.name = \"GodxTimeoutError\";\n this.url = url;\n this.timeoutMs = timeoutMs;\n }\n}\n\n/**\n * `fetch` with {@link HttpOptions} applied: merged headers, a deadline, an injectable fetch.\n * Every outbound call in this package goes through here, so one option reaches all of them.\n */\nexport async function platformFetch(\n input: URL | string,\n init: RequestInit = {},\n options: HttpOptions = {},\n): Promise<Response> {\n const headers = new Headers(init.headers);\n const extra = typeof options.headers === \"function\" ? options.headers() : options.headers;\n for (const [name, value] of Object.entries(extra ?? {})) {\n if (!headers.has(name)) headers.set(name, value);\n }\n const doFetch = options.fetch ?? globalThis.fetch;\n if (options.timeoutMs === undefined || options.timeoutMs <= 0) {\n return doFetch(input, { ...init, headers });\n }\n const controller = new AbortController();\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, options.timeoutMs);\n init.signal?.addEventListener(\"abort\", () => controller.abort(), { once: true });\n try {\n return await doFetch(input, { ...init, headers, signal: controller.signal });\n } catch (error) {\n if (timedOut) throw new GodxTimeoutError(String(input), options.timeoutMs);\n throw error;\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport interface IssuerMetadata {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n userinfo_endpoint: string;\n jwks_uri: string;\n end_session_endpoint?: string;\n revocation_endpoint?: string;\n}\n\nexport interface Issuer {\n metadata: IssuerMetadata;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n}\n\nexport class GodxHttpError extends Error {\n readonly status: number;\n readonly body: string;\n\n constructor(status: number, body: string, message?: string) {\n super(message ?? `GoDX platform request failed with ${status}`);\n this.name = \"GodxHttpError\";\n this.status = status;\n this.body = body;\n }\n}\n\n// Per-isolate cache; the platform docs require reading discovery, not hardcoding paths.\nconst issuerCache = new Map<string, Promise<Issuer>>();\n\n/**\n * Discovery + JWKS, cached per isolate and issuer. `options` applies to the discovery request and\n * to every JWKS refresh of the cached set (`timeoutMs` → jose `timeoutDuration`; static headers\n * only — a header FUNCTION cannot run inside jose's refresh, so it is evaluated once here). The\n * first call for an issuer decides the JWKS options; a failed load evicts itself.\n */\nexport function loadIssuer(issuerUrl: string, options: HttpOptions = {}): Promise<Issuer> {\n let cached = issuerCache.get(issuerUrl);\n if (!cached) {\n cached = (async () => {\n const res = await platformFetch(new URL(\"/.well-known/openid-configuration\", issuerUrl), {}, options);\n if (!res.ok) {\n issuerCache.delete(issuerUrl);\n throw new GodxHttpError(res.status, await res.text(), \"OIDC discovery failed\");\n }\n const metadata = (await res.json()) as IssuerMetadata;\n const jwksHeaders = typeof options.headers === \"function\" ? options.headers() : options.headers;\n const jwks = createRemoteJWKSet(new URL(metadata.jwks_uri), {\n ...(options.timeoutMs ? { timeoutDuration: options.timeoutMs } : {}),\n ...(jwksHeaders ? { headers: jwksHeaders } : {}),\n // jose keys its fetch override by its OWN exported symbol (a plain Symbol(), not\n // Symbol.for): 0.10.0 passed Symbol.for(\"jose.fetch\") and the JWKS fetch silently ignored\n // `http.fetch` — caught by pages' lead (godx-jp/server-sdk#19).\n ...(options.fetch ? { [customFetch]: options.fetch } : {}),\n });\n return { metadata, jwks };\n })();\n issuerCache.set(issuerUrl, cached);\n cached.catch(() => issuerCache.delete(issuerUrl));\n }\n return cached;\n}\n\nexport interface SsoClientConfig {\n issuer: string;\n serviceSlug: string;\n clientId: string;\n /** Omit for a public (secretless) instance — PKCE alone then. */\n clientSecret?: string;\n redirectUri: string;\n /** Token audience = the ServiceInstance slug (defaults to serviceSlug). */\n audience?: string;\n /** Headers, deadline and fetch for every request this client makes. */\n http?: HttpOptions;\n}\n\nexport interface TokenResponse {\n access_token: string;\n token_type: string;\n expires_in: number;\n id_token?: string;\n refresh_token?: string;\n scope?: string;\n}\n\nexport interface UserinfoResponse {\n sub: string;\n email?: string;\n email_verified?: boolean;\n name?: string;\n locale?: string;\n timezone?: string;\n}\n\nexport class GodxSsoClient {\n readonly config: SsoClientConfig;\n\n constructor(config: SsoClientConfig) {\n this.config = config;\n }\n\n async authorizeUrl(params: {\n state: string;\n nonce: string;\n codeChallenge: string;\n scope?: string;\n /** console_organization_id — required by the launch gates; usually arrives on the launch URL. */\n organizationContextId?: string;\n }): Promise<string> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n const url = new URL(metadata.authorization_endpoint);\n url.search = new URLSearchParams({\n service_slug: this.config.serviceSlug,\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n response_type: \"code\",\n scope: params.scope ?? \"openid profile email\",\n state: params.state,\n nonce: params.nonce,\n code_challenge: params.codeChallenge,\n code_challenge_method: \"S256\",\n ...(params.organizationContextId\n ? { organization_context_id: params.organizationContextId }\n : {}),\n }).toString();\n return url.toString();\n }\n\n async exchangeCode(params: { code: string; codeVerifier: string }): Promise<TokenResponse> {\n return this.tokenRequest(\"token_endpoint\", {\n grant_type: \"authorization_code\",\n redirect_uri: this.config.redirectUri,\n code: params.code,\n code_verifier: params.codeVerifier,\n });\n }\n\n async refresh(refreshToken: string): Promise<TokenResponse> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n return this.postForm(new URL(\"/api/sso/refresh\", metadata.issuer).toString(), {\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n // The platform files a refresh token under the INSTANCE slug (the token's audience,\n // e.g. \"godx-logger-prod\"), not the catalog service slug: sending the service slug\n // finds no row and answers invalid_grant (measured against id.godx.jp).\n service_slug: this.config.audience ?? this.config.serviceSlug,\n });\n }\n\n /** Best-effort revocation; failures are the caller's decision to ignore. */\n async revoke(token: string): Promise<void> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n if (!metadata.revocation_endpoint) return;\n await this.postForm(metadata.revocation_endpoint, { token });\n }\n\n async userinfo(accessToken: string): Promise<UserinfoResponse> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n const res = await platformFetch(metadata.userinfo_endpoint, {\n headers: { Authorization: `Bearer ${accessToken}` },\n }, this.config.http);\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), \"userinfo failed\");\n return (await res.json()) as UserinfoResponse;\n }\n\n private async tokenRequest(\n endpoint: \"token_endpoint\",\n fields: Record<string, string>,\n ): Promise<TokenResponse> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n return this.postForm(metadata[endpoint], fields);\n }\n\n private async postForm(url: string, fields: Record<string, string>): Promise<TokenResponse> {\n const body = new URLSearchParams({\n service_slug: this.config.serviceSlug,\n client_id: this.config.clientId,\n ...(this.config.clientSecret ? { client_secret: this.config.clientSecret } : {}),\n ...fields,\n });\n const res = await platformFetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n }, this.config.http);\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), \"token request failed\");\n return (await res.json()) as TokenResponse;\n }\n}\n\n/**\n * Server-to-server reads with service credentials (X-Service-Slug/X-Service-Secret).\n * Rate-limited by the platform (429) — cache results and back off; no auto-retry here.\n */\nexport function createServiceApi(config: {\n issuer: string;\n serviceSlug: string;\n serviceSecret: string;\n http?: HttpOptions;\n}) {\n const get = async <T>(path: string, query: Record<string, string>): Promise<T> => {\n const url = new URL(path, config.issuer);\n url.search = new URLSearchParams(query).toString();\n const res = await platformFetch(url, {\n headers: {\n \"X-Service-Slug\": config.serviceSlug,\n \"X-Service-Secret\": config.serviceSecret,\n },\n }, config.http);\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), `service API ${path} failed`);\n return (await res.json()) as T;\n };\n\n return {\n users: (q: { organizationSlug: string; perPage?: number; page?: number }) =>\n get<unknown>(\"/api/sso/service/users\", {\n organization_slug: q.organizationSlug,\n ...(q.perPage ? { per_page: String(q.perPage) } : {}),\n ...(q.page ? { page: String(q.page) } : {}),\n }),\n branches: (q: { organizationSlug: string }) =>\n get<unknown>(\"/api/sso/service/branches\", { organization_slug: q.organizationSlug }),\n brands: (q: { organizationSlug: string }) =>\n get<unknown>(\"/api/sso/service/brands\", { organization_slug: q.organizationSlug }),\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadIssuer
3
- } from "./chunk-6XZK5EKW.js";
3
+ } from "./chunk-2N6BMNKT.js";
4
4
 
5
5
  // src/auth.ts
6
6
  import { EncryptJWT, jwtDecrypt, jwtVerify } from "jose";
@@ -27,8 +27,16 @@ async function verifyAccessToken(token, opts) {
27
27
  if (typeof payload.sub !== "string" || typeof payload.exp !== "number") {
28
28
  throw new Error("access token is missing sub/exp");
29
29
  }
30
+ const executionId = nonEmptyString(payload["gx_execution_id"]);
30
31
  return Object.freeze({
31
32
  sub: payload.sub,
33
+ issuer: metadata.issuer,
34
+ resource: expectedAudience,
35
+ tokenId: nonEmptyString(payload.jti),
36
+ issuedAt: typeof payload.iat === "number" ? payload.iat : void 0,
37
+ executionId,
38
+ argumentsDigest: nonEmptyString(payload["gx_arguments_sha256"]),
39
+ serviceAuthorization: embeddedAuthorization(payload["service_authorization"], executionId, expectedAudience),
32
40
  organizationId,
33
41
  organizationContextId: typeof payload["organization_context_id"] === "string" && payload["organization_context_id"] !== "" ? payload["organization_context_id"] : void 0,
34
42
  accessMode: payload["organization_access_mode"] === "full" ? "full" : "read_only",
@@ -48,6 +56,26 @@ function randomToken(bytes = 16) {
48
56
  crypto.getRandomValues(buf);
49
57
  return base64url(buf);
50
58
  }
59
+ function nonEmptyString(value) {
60
+ return typeof value === "string" && value !== "" ? value : void 0;
61
+ }
62
+ function embeddedAuthorization(raw, executionId, resource) {
63
+ if (!executionId || typeof raw !== "object" || raw === null) return void 0;
64
+ const snapshot = raw;
65
+ const permissions = snapshot["permissions"];
66
+ if (snapshot["authoritative"] !== true || snapshot["service"] !== resource || snapshot["execution_id"] !== executionId || !Array.isArray(permissions) || !permissions.every((p) => typeof p === "string") || typeof snapshot["evaluated_at"] !== "string") {
67
+ return void 0;
68
+ }
69
+ return Object.freeze({
70
+ authoritative: true,
71
+ service: resource,
72
+ role: typeof snapshot["role"] === "string" ? snapshot["role"] : null,
73
+ permissions: Object.freeze([...permissions]),
74
+ branchId: typeof snapshot["branch_id"] === "string" ? snapshot["branch_id"] : null,
75
+ evaluatedAt: snapshot["evaluated_at"],
76
+ executionId
77
+ });
78
+ }
51
79
  function decodeUnverified(token) {
52
80
  try {
53
81
  const part = token.split(".")[1] ?? "";
@@ -85,4 +113,4 @@ export {
85
113
  sealData,
86
114
  openData
87
115
  };
88
- //# sourceMappingURL=chunk-KJCSW2NE.js.map
116
+ //# sourceMappingURL=chunk-K3AG5TCN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/auth.ts"],"sourcesContent":["/**\n * ./auth — verified immutable Principal, PKCE, and sealed session values.\n * Mirrors php-sdk's Auth module: the Principal only comes out of one parse\n * gate (verifyAccessToken) and is frozen after that.\n */\nimport { EncryptJWT, jwtDecrypt, jwtVerify } from \"jose\";\nimport { type HttpOptions, loadIssuer } from \"./http\";\n\nexport interface Principal {\n readonly sub: string;\n /** Claim organization_id — the organization's INTERNAL id. */\n readonly organizationId: string;\n /**\n * Claim organization_context_id — the CONSOLE organization id, when the token carries one.\n * This is the id the platform's organization-scoped APIs look organizations up by\n * (`/api/sso/me/permissions`, `fetchEffectivePermissions`); passing `organizationId` there\n * answers 403 CONTEXT_UNAVAILABLE (godx-jp/server-sdk#6).\n */\n readonly organizationContextId?: string;\n /** Defaults to read_only unless the claim says full — fail closed. */\n readonly accessMode: \"full\" | \"read_only\";\n readonly scopes: readonly string[];\n readonly serviceInstanceId?: string;\n readonly clientId?: string;\n /** Unix seconds, from the access token's exp. */\n readonly expiresAt: number;\n /** From userinfo, when the adapter fetched it at login. */\n readonly name?: string;\n readonly email?: string;\n /** The exact `iss` the token was verified against (discovery issuer). Absent on sessions sealed before 0.11.0. */\n readonly issuer?: string;\n /** The audience the token was verified against — your resource URI / instance slug. Absent on sessions sealed before 0.11.0. */\n readonly resource?: string;\n /** Claim `jti`. */\n readonly tokenId?: string;\n /** Unix seconds, from `iat`. */\n readonly issuedAt?: number;\n /**\n * Claim `gx_execution_id` — set only on a gx MUTATION token (a one-call execution token or a\n * checkpointed one); never on a read token. The service's once-only gate keys on it (godx-jp/id#2190).\n */\n readonly executionId?: string;\n /** Claim `gx_arguments_sha256` — SHA-256 hex of the approved arguments in canonical form (one-call execution token). */\n readonly argumentsDigest?: string;\n /**\n * Claim `service_authorization` of a one-call execution token: byte-for-byte what\n * `POST /api/sso/introspect` would answer for this token, computed live at minting (F = 0). Only\n * present when its `execution_id` names THIS token's execution and its `service` is THIS audience —\n * otherwise it is dropped at parse and the caller falls back to `fetchEffectivePermissions`.\n */\n readonly serviceAuthorization?: EmbeddedAuthorization;\n}\n\n/** The `service_authorization` snapshot Platform embeds in a one-call execution token (godx-jp/id#2190). */\nexport interface EmbeddedAuthorization {\n readonly authoritative: true;\n readonly service: string;\n readonly role: string | null;\n readonly permissions: readonly string[];\n readonly branchId: string | null;\n /** ISO-8601, the moment Platform computed the snapshot — a few ms before `iat`. */\n readonly evaluatedAt: string;\n readonly executionId: string;\n}\n\n/**\n * The one parse gate. Validates signature (JWKS), iss (discovery issuer,\n * exact), aud (= service slug), exp — then requires organization_id.\n */\nexport async function verifyAccessToken(\n token: string,\n opts: {\n issuer: string;\n serviceSlug: string;\n /** The token audience = your ServiceInstance slug. Defaults to serviceSlug. */\n audience?: string;\n /** Headers, deadline and fetch for discovery and JWKS (first call per issuer decides the JWKS options). */\n http?: HttpOptions;\n },\n): Promise<Principal> {\n const expectedAudience = opts.audience ?? opts.serviceSlug;\n const { metadata, jwks } = await loadIssuer(opts.issuer, opts.http);\n let payload;\n try {\n ({ payload } = await jwtVerify(token, jwks, {\n issuer: metadata.issuer,\n audience: expectedAudience,\n }));\n } catch (e) {\n // Say what was expected vs what the token actually carries — a bare\n // \"unexpected claim\" costs whoever reads it a debugging session.\n const actual = decodeUnverified(token);\n throw new Error(\n `${e instanceof Error ? e.message : String(e)} — expected iss=\"${metadata.issuer}\" aud=\"${expectedAudience}\", ` +\n `token carries iss=\"${String(actual?.iss ?? \"?\")}\" aud=${JSON.stringify(actual?.aud ?? \"?\")}`,\n { cause: e },\n );\n }\n\n const organizationId = payload[\"organization_id\"];\n if (typeof organizationId !== \"string\" || organizationId === \"\") {\n throw new Error(\"access token has no organization_id claim\");\n }\n if (typeof payload.sub !== \"string\" || typeof payload.exp !== \"number\") {\n throw new Error(\"access token is missing sub/exp\");\n }\n\n const executionId = nonEmptyString(payload[\"gx_execution_id\"]);\n return Object.freeze({\n sub: payload.sub,\n issuer: metadata.issuer,\n resource: expectedAudience,\n tokenId: nonEmptyString(payload.jti),\n issuedAt: typeof payload.iat === \"number\" ? payload.iat : undefined,\n executionId,\n argumentsDigest: nonEmptyString(payload[\"gx_arguments_sha256\"]),\n serviceAuthorization: embeddedAuthorization(payload[\"service_authorization\"], executionId, expectedAudience),\n organizationId,\n organizationContextId:\n typeof payload[\"organization_context_id\"] === \"string\" && payload[\"organization_context_id\"] !== \"\"\n ? payload[\"organization_context_id\"]\n : undefined,\n accessMode: payload[\"organization_access_mode\"] === \"full\" ? \"full\" : \"read_only\",\n scopes: Object.freeze(String(payload[\"scope\"] ?? \"\").split(\" \").filter(Boolean)),\n serviceInstanceId: typeof payload[\"service_instance_id\"] === \"string\" ? payload[\"service_instance_id\"] : undefined,\n clientId: typeof payload[\"client_id\"] === \"string\" ? payload[\"client_id\"] : undefined,\n expiresAt: payload.exp,\n } satisfies Principal);\n}\n\nexport interface Pkce {\n verifier: string;\n challenge: string;\n method: \"S256\";\n}\n\nexport async function createPkce(): Promise<Pkce> {\n const verifier = randomToken(32);\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier));\n return { verifier, challenge: base64url(new Uint8Array(digest)), method: \"S256\" };\n}\n\nexport function randomToken(bytes = 16): string {\n const buf = new Uint8Array(bytes);\n crypto.getRandomValues(buf);\n return base64url(buf);\n}\n\nfunction nonEmptyString(value: unknown): string | undefined {\n return typeof value === \"string\" && value !== \"\" ? value : undefined;\n}\n\n/**\n * Accept the embedded snapshot only when it is provably THIS token's and THIS service's — the same\n * four checks php-sdk's TokenEmbeddedPermissionSource makes. Anything else → undefined (fall back to\n * introspection); never a partial object.\n */\nfunction embeddedAuthorization(\n raw: unknown,\n executionId: string | undefined,\n resource: string,\n): EmbeddedAuthorization | undefined {\n if (!executionId || typeof raw !== \"object\" || raw === null) return undefined;\n const snapshot = raw as Record<string, unknown>;\n const permissions = snapshot[\"permissions\"];\n if (\n snapshot[\"authoritative\"] !== true ||\n snapshot[\"service\"] !== resource ||\n snapshot[\"execution_id\"] !== executionId ||\n !Array.isArray(permissions) ||\n !permissions.every((p): p is string => typeof p === \"string\") ||\n typeof snapshot[\"evaluated_at\"] !== \"string\"\n ) {\n return undefined;\n }\n return Object.freeze({\n authoritative: true,\n service: resource,\n role: typeof snapshot[\"role\"] === \"string\" ? snapshot[\"role\"] : null,\n permissions: Object.freeze([...permissions]),\n branchId: typeof snapshot[\"branch_id\"] === \"string\" ? snapshot[\"branch_id\"] : null,\n evaluatedAt: snapshot[\"evaluated_at\"],\n executionId,\n });\n}\n\n/** Diagnostics only — an UNVERIFIED read of the payload for error messages. */\nfunction decodeUnverified(token: string): Record<string, unknown> | null {\n try {\n const part = token.split(\".\")[1] ?? \"\";\n const pad = part.replace(/-/g, \"+\").replace(/_/g, \"/\");\n return JSON.parse(atob(pad)) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n return btoa(bin).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function sessionKey(secret: string): Promise<Uint8Array> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(secret));\n return new Uint8Array(digest);\n}\n\n/** Encrypt + sign an arbitrary payload into a compact cookie value (JWE dir/A256GCM). */\nexport async function sealData(\n payload: Record<string, unknown>,\n secret: string,\n ttlSeconds: number,\n): Promise<string> {\n return new EncryptJWT(payload)\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(Math.floor(Date.now() / 1000) + ttlSeconds)\n .encrypt(await sessionKey(secret));\n}\n\n/** Returns null for anything invalid or expired — never throws on bad input. */\nexport async function openData<T = Record<string, unknown>>(\n token: string,\n secret: string,\n): Promise<T | null> {\n try {\n const { payload } = await jwtDecrypt(token, await sessionKey(secret));\n return payload as T;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/hono.js CHANGED
@@ -4,12 +4,12 @@ import {
4
4
  randomToken,
5
5
  sealData,
6
6
  verifyAccessToken
7
- } from "./chunk-KJCSW2NE.js";
7
+ } from "./chunk-K3AG5TCN.js";
8
8
  import {
9
9
  GodxSsoClient,
10
10
  loadIssuer,
11
11
  platformFetch
12
- } from "./chunk-6XZK5EKW.js";
12
+ } from "./chunk-2N6BMNKT.js";
13
13
 
14
14
  // src/hono.ts
15
15
  import { Hono } from "hono";
package/dist/http.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  createServiceApi,
6
6
  loadIssuer,
7
7
  platformFetch
8
- } from "./chunk-6XZK5EKW.js";
8
+ } from "./chunk-2N6BMNKT.js";
9
9
  export {
10
10
  GodxHttpError,
11
11
  GodxSsoClient,
package/dist/org.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  GodxHttpError,
3
3
  platformFetch
4
- } from "./chunk-6XZK5EKW.js";
4
+ } from "./chunk-2N6BMNKT.js";
5
5
 
6
6
  // src/org.ts
7
7
  var OrgApiError = class extends GodxHttpError {
package/dist/sync.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  GodxHttpError,
3
3
  platformFetch
4
- } from "./chunk-6XZK5EKW.js";
4
+ } from "./chunk-2N6BMNKT.js";
5
5
 
6
6
  // src/sync.ts
7
7
  var KNOWN_CONTRACT_VERSIONS = ["v1", "v2"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godxjp/server-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "GoDX server-side SDK (Workers/Node) — holds service credentials; for browser code use @godxjp/web-sdk. Counterpart of godx-jp/php-sdk.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/http.ts"],"sourcesContent":["/**\n * ./http — endpoint contract from OIDC discovery, the SSO client, and the\n * server-to-server service API. Runtime-agnostic: fetch + WebCrypto only.\n * Mirrors php-sdk's Http module boundary.\n */\nimport { createRemoteJWKSet } from \"jose\";\n\n/**\n * Per-call knobs for every outbound request this SDK makes (godx-jp/server-sdk#17, from the\n * pages.godx.jp incident of 2026-10-04 where a Platform call hung 25–180 s with no request id\n * to find it by). Pass them on the client/config (`http`) or per call where a function takes\n * them; defaults are exactly the old behaviour.\n */\nexport interface HttpOptions {\n /**\n * Extra headers on every request — `X-Request-Id` is the one that makes a hang findable in\n * Platform's log. A function is called per request (mint a fresh id each time); a header the\n * SDK itself sets (Authorization, Content-Type, X-Service-*) always wins.\n */\n headers?: Record<string, string> | (() => Record<string, string>);\n /** Abort a request after this many milliseconds and throw {@link GodxTimeoutError}. Unset = the runtime's default (often none). */\n timeoutMs?: number;\n /** The fetch to use (tests, custom dispatchers). Default `globalThis.fetch`, read at call time. */\n fetch?: typeof fetch;\n}\n\n/** The request did not answer within `timeoutMs`; `url` and `timeoutMs` say which and how long. */\nexport class GodxTimeoutError extends Error {\n readonly url: string;\n readonly timeoutMs: number;\n constructor(url: string, timeoutMs: number) {\n super(`GoDX platform request to ${url} did not answer within ${timeoutMs} ms`);\n this.name = \"GodxTimeoutError\";\n this.url = url;\n this.timeoutMs = timeoutMs;\n }\n}\n\n/**\n * `fetch` with {@link HttpOptions} applied: merged headers, a deadline, an injectable fetch.\n * Every outbound call in this package goes through here, so one option reaches all of them.\n */\nexport async function platformFetch(\n input: URL | string,\n init: RequestInit = {},\n options: HttpOptions = {},\n): Promise<Response> {\n const headers = new Headers(init.headers);\n const extra = typeof options.headers === \"function\" ? options.headers() : options.headers;\n for (const [name, value] of Object.entries(extra ?? {})) {\n if (!headers.has(name)) headers.set(name, value);\n }\n const doFetch = options.fetch ?? globalThis.fetch;\n if (options.timeoutMs === undefined || options.timeoutMs <= 0) {\n return doFetch(input, { ...init, headers });\n }\n const controller = new AbortController();\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n controller.abort();\n }, options.timeoutMs);\n init.signal?.addEventListener(\"abort\", () => controller.abort(), { once: true });\n try {\n return await doFetch(input, { ...init, headers, signal: controller.signal });\n } catch (error) {\n if (timedOut) throw new GodxTimeoutError(String(input), options.timeoutMs);\n throw error;\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport interface IssuerMetadata {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n userinfo_endpoint: string;\n jwks_uri: string;\n end_session_endpoint?: string;\n revocation_endpoint?: string;\n}\n\nexport interface Issuer {\n metadata: IssuerMetadata;\n jwks: ReturnType<typeof createRemoteJWKSet>;\n}\n\nexport class GodxHttpError extends Error {\n readonly status: number;\n readonly body: string;\n\n constructor(status: number, body: string, message?: string) {\n super(message ?? `GoDX platform request failed with ${status}`);\n this.name = \"GodxHttpError\";\n this.status = status;\n this.body = body;\n }\n}\n\n// Per-isolate cache; the platform docs require reading discovery, not hardcoding paths.\nconst issuerCache = new Map<string, Promise<Issuer>>();\n\n/**\n * Discovery + JWKS, cached per isolate and issuer. `options` applies to the discovery request and\n * to every JWKS refresh of the cached set (`timeoutMs` → jose `timeoutDuration`; static headers\n * only — a header FUNCTION cannot run inside jose's refresh, so it is evaluated once here). The\n * first call for an issuer decides the JWKS options; a failed load evicts itself.\n */\nexport function loadIssuer(issuerUrl: string, options: HttpOptions = {}): Promise<Issuer> {\n let cached = issuerCache.get(issuerUrl);\n if (!cached) {\n cached = (async () => {\n const res = await platformFetch(new URL(\"/.well-known/openid-configuration\", issuerUrl), {}, options);\n if (!res.ok) {\n issuerCache.delete(issuerUrl);\n throw new GodxHttpError(res.status, await res.text(), \"OIDC discovery failed\");\n }\n const metadata = (await res.json()) as IssuerMetadata;\n const jwksHeaders = typeof options.headers === \"function\" ? options.headers() : options.headers;\n const jwks = createRemoteJWKSet(new URL(metadata.jwks_uri), {\n ...(options.timeoutMs ? { timeoutDuration: options.timeoutMs } : {}),\n ...(jwksHeaders ? { headers: jwksHeaders } : {}),\n ...(options.fetch ? { [Symbol.for(\"jose.fetch\")]: options.fetch } : {}),\n });\n return { metadata, jwks };\n })();\n issuerCache.set(issuerUrl, cached);\n cached.catch(() => issuerCache.delete(issuerUrl));\n }\n return cached;\n}\n\nexport interface SsoClientConfig {\n issuer: string;\n serviceSlug: string;\n clientId: string;\n /** Omit for a public (secretless) instance — PKCE alone then. */\n clientSecret?: string;\n redirectUri: string;\n /** Token audience = the ServiceInstance slug (defaults to serviceSlug). */\n audience?: string;\n /** Headers, deadline and fetch for every request this client makes. */\n http?: HttpOptions;\n}\n\nexport interface TokenResponse {\n access_token: string;\n token_type: string;\n expires_in: number;\n id_token?: string;\n refresh_token?: string;\n scope?: string;\n}\n\nexport interface UserinfoResponse {\n sub: string;\n email?: string;\n email_verified?: boolean;\n name?: string;\n locale?: string;\n timezone?: string;\n}\n\nexport class GodxSsoClient {\n readonly config: SsoClientConfig;\n\n constructor(config: SsoClientConfig) {\n this.config = config;\n }\n\n async authorizeUrl(params: {\n state: string;\n nonce: string;\n codeChallenge: string;\n scope?: string;\n /** console_organization_id — required by the launch gates; usually arrives on the launch URL. */\n organizationContextId?: string;\n }): Promise<string> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n const url = new URL(metadata.authorization_endpoint);\n url.search = new URLSearchParams({\n service_slug: this.config.serviceSlug,\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n response_type: \"code\",\n scope: params.scope ?? \"openid profile email\",\n state: params.state,\n nonce: params.nonce,\n code_challenge: params.codeChallenge,\n code_challenge_method: \"S256\",\n ...(params.organizationContextId\n ? { organization_context_id: params.organizationContextId }\n : {}),\n }).toString();\n return url.toString();\n }\n\n async exchangeCode(params: { code: string; codeVerifier: string }): Promise<TokenResponse> {\n return this.tokenRequest(\"token_endpoint\", {\n grant_type: \"authorization_code\",\n redirect_uri: this.config.redirectUri,\n code: params.code,\n code_verifier: params.codeVerifier,\n });\n }\n\n async refresh(refreshToken: string): Promise<TokenResponse> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n return this.postForm(new URL(\"/api/sso/refresh\", metadata.issuer).toString(), {\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n // The platform files a refresh token under the INSTANCE slug (the token's audience,\n // e.g. \"godx-logger-prod\"), not the catalog service slug: sending the service slug\n // finds no row and answers invalid_grant (measured against id.godx.jp).\n service_slug: this.config.audience ?? this.config.serviceSlug,\n });\n }\n\n /** Best-effort revocation; failures are the caller's decision to ignore. */\n async revoke(token: string): Promise<void> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n if (!metadata.revocation_endpoint) return;\n await this.postForm(metadata.revocation_endpoint, { token });\n }\n\n async userinfo(accessToken: string): Promise<UserinfoResponse> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n const res = await platformFetch(metadata.userinfo_endpoint, {\n headers: { Authorization: `Bearer ${accessToken}` },\n }, this.config.http);\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), \"userinfo failed\");\n return (await res.json()) as UserinfoResponse;\n }\n\n private async tokenRequest(\n endpoint: \"token_endpoint\",\n fields: Record<string, string>,\n ): Promise<TokenResponse> {\n const { metadata } = await loadIssuer(this.config.issuer, this.config.http);\n return this.postForm(metadata[endpoint], fields);\n }\n\n private async postForm(url: string, fields: Record<string, string>): Promise<TokenResponse> {\n const body = new URLSearchParams({\n service_slug: this.config.serviceSlug,\n client_id: this.config.clientId,\n ...(this.config.clientSecret ? { client_secret: this.config.clientSecret } : {}),\n ...fields,\n });\n const res = await platformFetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body,\n }, this.config.http);\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), \"token request failed\");\n return (await res.json()) as TokenResponse;\n }\n}\n\n/**\n * Server-to-server reads with service credentials (X-Service-Slug/X-Service-Secret).\n * Rate-limited by the platform (429) — cache results and back off; no auto-retry here.\n */\nexport function createServiceApi(config: {\n issuer: string;\n serviceSlug: string;\n serviceSecret: string;\n http?: HttpOptions;\n}) {\n const get = async <T>(path: string, query: Record<string, string>): Promise<T> => {\n const url = new URL(path, config.issuer);\n url.search = new URLSearchParams(query).toString();\n const res = await platformFetch(url, {\n headers: {\n \"X-Service-Slug\": config.serviceSlug,\n \"X-Service-Secret\": config.serviceSecret,\n },\n }, config.http);\n if (!res.ok) throw new GodxHttpError(res.status, await res.text(), `service API ${path} failed`);\n return (await res.json()) as T;\n };\n\n return {\n users: (q: { organizationSlug: string; perPage?: number; page?: number }) =>\n get<unknown>(\"/api/sso/service/users\", {\n organization_slug: q.organizationSlug,\n ...(q.perPage ? { per_page: String(q.perPage) } : {}),\n ...(q.page ? { page: String(q.page) } : {}),\n }),\n branches: (q: { organizationSlug: string }) =>\n get<unknown>(\"/api/sso/service/branches\", { organization_slug: q.organizationSlug }),\n brands: (q: { organizationSlug: string }) =>\n get<unknown>(\"/api/sso/service/brands\", { organization_slug: q.organizationSlug }),\n };\n}\n"],"mappings":";AAKA,SAAS,0BAA0B;AAsB5B,IAAM,mBAAN,cAA+B,MAAM;AAAA,EACjC;AAAA,EACA;AAAA,EACT,YAAY,KAAa,WAAmB;AAC1C,UAAM,4BAA4B,GAAG,0BAA0B,SAAS,KAAK;AAC7E,SAAK,OAAO;AACZ,SAAK,MAAM;AACX,SAAK,YAAY;AAAA,EACnB;AACF;AAMA,eAAsB,cACpB,OACA,OAAoB,CAAC,GACrB,UAAuB,CAAC,GACL;AACnB,QAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,QAAM,QAAQ,OAAO,QAAQ,YAAY,aAAa,QAAQ,QAAQ,IAAI,QAAQ;AAClF,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,SAAS,CAAC,CAAC,GAAG;AACvD,QAAI,CAAC,QAAQ,IAAI,IAAI,EAAG,SAAQ,IAAI,MAAM,KAAK;AAAA,EACjD;AACA,QAAM,UAAU,QAAQ,SAAS,WAAW;AAC5C,MAAI,QAAQ,cAAc,UAAa,QAAQ,aAAa,GAAG;AAC7D,WAAO,QAAQ,OAAO,EAAE,GAAG,MAAM,QAAQ,CAAC;AAAA,EAC5C;AACA,QAAM,aAAa,IAAI,gBAAgB;AACvC,MAAI,WAAW;AACf,QAAM,QAAQ,WAAW,MAAM;AAC7B,eAAW;AACX,eAAW,MAAM;AAAA,EACnB,GAAG,QAAQ,SAAS;AACpB,OAAK,QAAQ,iBAAiB,SAAS,MAAM,WAAW,MAAM,GAAG,EAAE,MAAM,KAAK,CAAC;AAC/E,MAAI;AACF,WAAO,MAAM,QAAQ,OAAO,EAAE,GAAG,MAAM,SAAS,QAAQ,WAAW,OAAO,CAAC;AAAA,EAC7E,SAAS,OAAO;AACd,QAAI,SAAU,OAAM,IAAI,iBAAiB,OAAO,KAAK,GAAG,QAAQ,SAAS;AACzE,UAAM;AAAA,EACR,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;AAiBO,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAC9B;AAAA,EACA;AAAA,EAET,YAAY,QAAgB,MAAc,SAAkB;AAC1D,UAAM,WAAW,qCAAqC,MAAM,EAAE;AAC9D,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EACd;AACF;AAGA,IAAM,cAAc,oBAAI,IAA6B;AAQ9C,SAAS,WAAW,WAAmB,UAAuB,CAAC,GAAoB;AACxF,MAAI,SAAS,YAAY,IAAI,SAAS;AACtC,MAAI,CAAC,QAAQ;AACX,cAAU,YAAY;AACpB,YAAM,MAAM,MAAM,cAAc,IAAI,IAAI,qCAAqC,SAAS,GAAG,CAAC,GAAG,OAAO;AACpG,UAAI,CAAC,IAAI,IAAI;AACX,oBAAY,OAAO,SAAS;AAC5B,cAAM,IAAI,cAAc,IAAI,QAAQ,MAAM,IAAI,KAAK,GAAG,uBAAuB;AAAA,MAC/E;AACA,YAAM,WAAY,MAAM,IAAI,KAAK;AACjC,YAAM,cAAc,OAAO,QAAQ,YAAY,aAAa,QAAQ,QAAQ,IAAI,QAAQ;AACxF,YAAM,OAAO,mBAAmB,IAAI,IAAI,SAAS,QAAQ,GAAG;AAAA,QAC1D,GAAI,QAAQ,YAAY,EAAE,iBAAiB,QAAQ,UAAU,IAAI,CAAC;AAAA,QAClE,GAAI,cAAc,EAAE,SAAS,YAAY,IAAI,CAAC;AAAA,QAC9C,GAAI,QAAQ,QAAQ,EAAE,CAAC,uBAAO,IAAI,YAAY,CAAC,GAAG,QAAQ,MAAM,IAAI,CAAC;AAAA,MACvE,CAAC;AACD,aAAO,EAAE,UAAU,KAAK;AAAA,IAC1B,GAAG;AACH,gBAAY,IAAI,WAAW,MAAM;AACjC,WAAO,MAAM,MAAM,YAAY,OAAO,SAAS,CAAC;AAAA,EAClD;AACA,SAAO;AACT;AAiCO,IAAM,gBAAN,MAAoB;AAAA,EAChB;AAAA,EAET,YAAY,QAAyB;AACnC,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,MAAM,aAAa,QAOC;AAClB,UAAM,EAAE,SAAS,IAAI,MAAM,WAAW,KAAK,OAAO,QAAQ,KAAK,OAAO,IAAI;AAC1E,UAAM,MAAM,IAAI,IAAI,SAAS,sBAAsB;AACnD,QAAI,SAAS,IAAI,gBAAgB;AAAA,MAC/B,cAAc,KAAK,OAAO;AAAA,MAC1B,WAAW,KAAK,OAAO;AAAA,MACvB,cAAc,KAAK,OAAO;AAAA,MAC1B,eAAe;AAAA,MACf,OAAO,OAAO,SAAS;AAAA,MACvB,OAAO,OAAO;AAAA,MACd,OAAO,OAAO;AAAA,MACd,gBAAgB,OAAO;AAAA,MACvB,uBAAuB;AAAA,MACvB,GAAI,OAAO,wBACP,EAAE,yBAAyB,OAAO,sBAAsB,IACxD,CAAC;AAAA,IACP,CAAC,EAAE,SAAS;AACZ,WAAO,IAAI,SAAS;AAAA,EACtB;AAAA,EAEA,MAAM,aAAa,QAAwE;AACzF,WAAO,KAAK,aAAa,kBAAkB;AAAA,MACzC,YAAY;AAAA,MACZ,cAAc,KAAK,OAAO;AAAA,MAC1B,MAAM,OAAO;AAAA,MACb,eAAe,OAAO;AAAA,IACxB,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAQ,cAA8C;AAC1D,UAAM,EAAE,SAAS,IAAI,MAAM,WAAW,KAAK,OAAO,QAAQ,KAAK,OAAO,IAAI;AAC1E,WAAO,KAAK,SAAS,IAAI,IAAI,oBAAoB,SAAS,MAAM,EAAE,SAAS,GAAG;AAAA,MAC5E,YAAY;AAAA,MACZ,eAAe;AAAA;AAAA;AAAA;AAAA,MAIf,cAAc,KAAK,OAAO,YAAY,KAAK,OAAO;AAAA,IACpD,CAAC;AAAA,EACH;AAAA;AAAA,EAGA,MAAM,OAAO,OAA8B;AACzC,UAAM,EAAE,SAAS,IAAI,MAAM,WAAW,KAAK,OAAO,QAAQ,KAAK,OAAO,IAAI;AAC1E,QAAI,CAAC,SAAS,oBAAqB;AACnC,UAAM,KAAK,SAAS,SAAS,qBAAqB,EAAE,MAAM,CAAC;AAAA,EAC7D;AAAA,EAEA,MAAM,SAAS,aAAgD;AAC7D,UAAM,EAAE,SAAS,IAAI,MAAM,WAAW,KAAK,OAAO,QAAQ,KAAK,OAAO,IAAI;AAC1E,UAAM,MAAM,MAAM,cAAc,SAAS,mBAAmB;AAAA,MAC1D,SAAS,EAAE,eAAe,UAAU,WAAW,GAAG;AAAA,IACpD,GAAG,KAAK,OAAO,IAAI;AACnB,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,cAAc,IAAI,QAAQ,MAAM,IAAI,KAAK,GAAG,iBAAiB;AACpF,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAAA,EAEA,MAAc,aACZ,UACA,QACwB;AACxB,UAAM,EAAE,SAAS,IAAI,MAAM,WAAW,KAAK,OAAO,QAAQ,KAAK,OAAO,IAAI;AAC1E,WAAO,KAAK,SAAS,SAAS,QAAQ,GAAG,MAAM;AAAA,EACjD;AAAA,EAEA,MAAc,SAAS,KAAa,QAAwD;AAC1F,UAAM,OAAO,IAAI,gBAAgB;AAAA,MAC/B,cAAc,KAAK,OAAO;AAAA,MAC1B,WAAW,KAAK,OAAO;AAAA,MACvB,GAAI,KAAK,OAAO,eAAe,EAAE,eAAe,KAAK,OAAO,aAAa,IAAI,CAAC;AAAA,MAC9E,GAAG;AAAA,IACL,CAAC;AACD,UAAM,MAAM,MAAM,cAAc,KAAK;AAAA,MACnC,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,MAC/D;AAAA,IACF,GAAG,KAAK,OAAO,IAAI;AACnB,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,cAAc,IAAI,QAAQ,MAAM,IAAI,KAAK,GAAG,sBAAsB;AACzF,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AACF;AAMO,SAAS,iBAAiB,QAK9B;AACD,QAAM,MAAM,OAAU,MAAc,UAA8C;AAChF,UAAM,MAAM,IAAI,IAAI,MAAM,OAAO,MAAM;AACvC,QAAI,SAAS,IAAI,gBAAgB,KAAK,EAAE,SAAS;AACjD,UAAM,MAAM,MAAM,cAAc,KAAK;AAAA,MACnC,SAAS;AAAA,QACP,kBAAkB,OAAO;AAAA,QACzB,oBAAoB,OAAO;AAAA,MAC7B;AAAA,IACF,GAAG,OAAO,IAAI;AACd,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,cAAc,IAAI,QAAQ,MAAM,IAAI,KAAK,GAAG,eAAe,IAAI,SAAS;AAC/F,WAAQ,MAAM,IAAI,KAAK;AAAA,EACzB;AAEA,SAAO;AAAA,IACL,OAAO,CAAC,MACN,IAAa,0BAA0B;AAAA,MACrC,mBAAmB,EAAE;AAAA,MACrB,GAAI,EAAE,UAAU,EAAE,UAAU,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACnD,GAAI,EAAE,OAAO,EAAE,MAAM,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC;AAAA,IAC3C,CAAC;AAAA,IACH,UAAU,CAAC,MACT,IAAa,6BAA6B,EAAE,mBAAmB,EAAE,iBAAiB,CAAC;AAAA,IACrF,QAAQ,CAAC,MACP,IAAa,2BAA2B,EAAE,mBAAmB,EAAE,iBAAiB,CAAC;AAAA,EACrF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/auth.ts"],"sourcesContent":["/**\n * ./auth — verified immutable Principal, PKCE, and sealed session values.\n * Mirrors php-sdk's Auth module: the Principal only comes out of one parse\n * gate (verifyAccessToken) and is frozen after that.\n */\nimport { EncryptJWT, jwtDecrypt, jwtVerify } from \"jose\";\nimport { type HttpOptions, loadIssuer } from \"./http\";\n\nexport interface Principal {\n readonly sub: string;\n /** Claim organization_id — the organization's INTERNAL id. */\n readonly organizationId: string;\n /**\n * Claim organization_context_id — the CONSOLE organization id, when the token carries one.\n * This is the id the platform's organization-scoped APIs look organizations up by\n * (`/api/sso/me/permissions`, `fetchEffectivePermissions`); passing `organizationId` there\n * answers 403 CONTEXT_UNAVAILABLE (godx-jp/server-sdk#6).\n */\n readonly organizationContextId?: string;\n /** Defaults to read_only unless the claim says full — fail closed. */\n readonly accessMode: \"full\" | \"read_only\";\n readonly scopes: readonly string[];\n readonly serviceInstanceId?: string;\n readonly clientId?: string;\n /** Unix seconds, from the access token's exp. */\n readonly expiresAt: number;\n /** From userinfo, when the adapter fetched it at login. */\n readonly name?: string;\n readonly email?: string;\n}\n\n/**\n * The one parse gate. Validates signature (JWKS), iss (discovery issuer,\n * exact), aud (= service slug), exp — then requires organization_id.\n */\nexport async function verifyAccessToken(\n token: string,\n opts: {\n issuer: string;\n serviceSlug: string;\n /** The token audience = your ServiceInstance slug. Defaults to serviceSlug. */\n audience?: string;\n /** Headers, deadline and fetch for discovery and JWKS (first call per issuer decides the JWKS options). */\n http?: HttpOptions;\n },\n): Promise<Principal> {\n const expectedAudience = opts.audience ?? opts.serviceSlug;\n const { metadata, jwks } = await loadIssuer(opts.issuer, opts.http);\n let payload;\n try {\n ({ payload } = await jwtVerify(token, jwks, {\n issuer: metadata.issuer,\n audience: expectedAudience,\n }));\n } catch (e) {\n // Say what was expected vs what the token actually carries — a bare\n // \"unexpected claim\" costs whoever reads it a debugging session.\n const actual = decodeUnverified(token);\n throw new Error(\n `${e instanceof Error ? e.message : String(e)} — expected iss=\"${metadata.issuer}\" aud=\"${expectedAudience}\", ` +\n `token carries iss=\"${String(actual?.iss ?? \"?\")}\" aud=${JSON.stringify(actual?.aud ?? \"?\")}`,\n { cause: e },\n );\n }\n\n const organizationId = payload[\"organization_id\"];\n if (typeof organizationId !== \"string\" || organizationId === \"\") {\n throw new Error(\"access token has no organization_id claim\");\n }\n if (typeof payload.sub !== \"string\" || typeof payload.exp !== \"number\") {\n throw new Error(\"access token is missing sub/exp\");\n }\n\n return Object.freeze({\n sub: payload.sub,\n organizationId,\n organizationContextId:\n typeof payload[\"organization_context_id\"] === \"string\" && payload[\"organization_context_id\"] !== \"\"\n ? payload[\"organization_context_id\"]\n : undefined,\n accessMode: payload[\"organization_access_mode\"] === \"full\" ? \"full\" : \"read_only\",\n scopes: Object.freeze(String(payload[\"scope\"] ?? \"\").split(\" \").filter(Boolean)),\n serviceInstanceId: typeof payload[\"service_instance_id\"] === \"string\" ? payload[\"service_instance_id\"] : undefined,\n clientId: typeof payload[\"client_id\"] === \"string\" ? payload[\"client_id\"] : undefined,\n expiresAt: payload.exp,\n } satisfies Principal);\n}\n\nexport interface Pkce {\n verifier: string;\n challenge: string;\n method: \"S256\";\n}\n\nexport async function createPkce(): Promise<Pkce> {\n const verifier = randomToken(32);\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(verifier));\n return { verifier, challenge: base64url(new Uint8Array(digest)), method: \"S256\" };\n}\n\nexport function randomToken(bytes = 16): string {\n const buf = new Uint8Array(bytes);\n crypto.getRandomValues(buf);\n return base64url(buf);\n}\n\n/** Diagnostics only — an UNVERIFIED read of the payload for error messages. */\nfunction decodeUnverified(token: string): Record<string, unknown> | null {\n try {\n const part = token.split(\".\")[1] ?? \"\";\n const pad = part.replace(/-/g, \"+\").replace(/_/g, \"/\");\n return JSON.parse(atob(pad)) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction base64url(bytes: Uint8Array): string {\n let bin = \"\";\n for (const b of bytes) bin += String.fromCharCode(b);\n return btoa(bin).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function sessionKey(secret: string): Promise<Uint8Array> {\n const digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(secret));\n return new Uint8Array(digest);\n}\n\n/** Encrypt + sign an arbitrary payload into a compact cookie value (JWE dir/A256GCM). */\nexport async function sealData(\n payload: Record<string, unknown>,\n secret: string,\n ttlSeconds: number,\n): Promise<string> {\n return new EncryptJWT(payload)\n .setProtectedHeader({ alg: \"dir\", enc: \"A256GCM\" })\n .setIssuedAt()\n .setExpirationTime(Math.floor(Date.now() / 1000) + ttlSeconds)\n .encrypt(await sessionKey(secret));\n}\n\n/** Returns null for anything invalid or expired — never throws on bad input. */\nexport async function openData<T = Record<string, unknown>>(\n token: string,\n secret: string,\n): Promise<T | null> {\n try {\n const { payload } = await jwtDecrypt(token, await sessionKey(secret));\n return payload as T;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}