@godxjp/server-sdk 0.5.0 → 0.7.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
@@ -84,7 +84,7 @@ back to `defaultOrganizationContextId` for local dev). A session is bound to the
84
84
 
85
85
  | Member | Type | Behaviour |
86
86
  |---|---|---|
87
- | `routes` | `Hono` | `GET /login` (PKCE + state in a sealed 5-min `gx_txn` cookie, forwards `organization_context_id`), `GET /callback` (code exchange → `verifyAccessToken` → one `userinfo` call for display name → seals `{principal, accessToken, refreshToken?}` into `gx_sess`), `POST /refresh` (renews the session with the refresh token: 204 + new cookie, or 401 and the cookie cleared), `GET /logout` (best-effort revoke of the access AND refresh token, then redirect to `end_session_endpoint`). Every failure redirects to `/?sso_error=…&sso_error_description=…`. |
87
+ | `routes` | `Hono` | `GET /login` (PKCE + state in a sealed 5-min `gx_txn` cookie, forwards `organization_context_id`, keeps `return_to` when it is a same-origin path — see `safeReturnTo`), `GET /callback` (code exchange → `verifyAccessToken` → one `userinfo` call for display name → seals `{principal, accessToken, refreshToken?}` into `gx_sess`, then redirects to `return_to` or `/`), `POST /refresh` (renews the session with the refresh token: 204 + new cookie, or 401 and the cookie cleared), `GET /logout` (best-effort revoke of the access AND refresh token, then redirect to `end_session_endpoint`). Every failure redirects to `/?sso_error=…&sso_error_description=…`. |
88
88
  | `requireAuth` | middleware | Opens `gx_sess`; 401 `{"error":"unauthenticated","refreshable":bool}` when missing/expired — `refreshable: true` means `POST /refresh` can renew it without a new sign-in. Sets `c.set("principal", …)` and `c.set("accessToken", …)` (the `GodxVariables` pair). |
89
89
  | `requireWritable` | middleware | Enforces `organization_access_mode`: non-GET/HEAD/OPTIONS from a `read_only` org answers 403. |
90
90
  | `dockProxy` | middleware | Forwards `/api/dock/v1/*` to the platform with the session's Bearer token — the GoDX Dock `apiBase` lane: no CORS, no token in the page. Mount it behind `requireAuth` and **before** any `requireWritable` (Dock actions are the platform's to authorize). Passes `X-Request-Id` back through. |
@@ -107,6 +107,11 @@ async function api(path: string, init?: RequestInit): Promise<Response> {
107
107
  }
108
108
  ```
109
109
 
110
+ A 401 that is not (or no longer) refreshable means the session is over: send the browser to
111
+ `/auth/login?return_to=${encodeURIComponent(location.pathname + location.search)}` — once, however
112
+ many requests failed. The platform returns immediately while its own session is alive, and the
113
+ callback lands back on that page.
114
+
110
115
  ## API reference
111
116
 
112
117
  Every export per subpath, checked against `src/*.ts`.
@@ -153,7 +158,7 @@ Every export per subpath, checked against `src/*.ts`.
153
158
 
154
159
  | Export | Signature | One line |
155
160
  |---|---|---|
156
- | `IdentityFamily` | union type | `organization \| brand \| branch \| user \| employee \| service_user_access \| org_service_role \| role_assignment`. |
161
+ | `IdentityFamily` | union type | `organization \| brand \| branch \| user \| employee \| service_user_access \| org_service_role \| role_assignment \| organization_member \| team \| team_member`. |
157
162
  | `KNOWN_CONTRACT_VERSIONS` | `["v1", "v2"]` | An unknown `contract_version` **stops** the consumer, never guesses. |
158
163
  | `IdentityEnvelope` | interface | CloudEvents 1.0 envelope as the relay publishes it (`id` = idempotency key, `sequence` = ordering fence, `subject` = `{family}/{id}`). |
159
164
  | `ParsedSubject` | `{ family, id }` | Split subject. |
package/dist/hono.d.ts CHANGED
@@ -28,6 +28,12 @@ interface GodxSsoConfig {
28
28
  */
29
29
  defaultOrganizationContextId?: string;
30
30
  }
31
+ /**
32
+ * `return_to` for `/login`: only a same-origin path ("/mail?x=1") is kept — anything that a
33
+ * browser could resolve to another origin ("//evil", "/\\evil", "https://…", control
34
+ * characters) is dropped, so the login flow can never become an open redirect.
35
+ */
36
+ declare function safeReturnTo(raw: string | undefined | null): string | undefined;
31
37
  type GodxVariables = {
32
38
  principal: Principal;
33
39
  accessToken: string;
@@ -41,4 +47,4 @@ declare function godxSso<E extends Env & {
41
47
  dockProxy: MiddlewareHandler<E>;
42
48
  };
43
49
 
44
- export { type GodxSsoConfig, type GodxVariables, godxSso };
50
+ export { type GodxSsoConfig, type GodxVariables, godxSso, safeReturnTo };
package/dist/hono.js CHANGED
@@ -16,6 +16,14 @@ import { deleteCookie, getCookie, setCookie } from "hono/cookie";
16
16
  var DEFAULT_SCOPE = "openid profile email offline_access";
17
17
  var DEFAULT_SESSION_MAX_AGE = 30 * 24 * 3600;
18
18
  var TXN_COOKIE = "gx_txn";
19
+ function safeReturnTo(raw) {
20
+ if (!raw || raw.length > 2048 || !/^\/(?![/\\])/.test(raw) || /[\u0000-\u001f\u007f\\]/.test(raw)) {
21
+ return void 0;
22
+ }
23
+ const base = "https://return-to.invalid";
24
+ const url = new URL(raw, base);
25
+ return url.origin === base ? url.pathname + url.search + url.hash : void 0;
26
+ }
19
27
  var SESSION_COOKIE = "gx_sess";
20
28
  function godxSso(getConfig) {
21
29
  const client = (c) => new GodxSsoClient(getConfig(c.env));
@@ -39,7 +47,8 @@ function godxSso(getConfig) {
39
47
  const state = randomToken();
40
48
  const nonce = randomToken();
41
49
  const organizationContextId = c.req.query("organization_context_id") ?? config.defaultOrganizationContextId;
42
- setCookie(c, TXN_COOKIE, await sealData({ v: pkce.verifier, s: state }, config.sessionSecret, 300), {
50
+ const returnTo = safeReturnTo(c.req.query("return_to"));
51
+ setCookie(c, TXN_COOKIE, await sealData({ v: pkce.verifier, s: state, r: returnTo }, config.sessionSecret, 300), {
43
52
  httpOnly: true,
44
53
  secure: secure(c),
45
54
  sameSite: "Lax",
@@ -108,7 +117,7 @@ function godxSso(getConfig) {
108
117
  } catch {
109
118
  }
110
119
  await writeSession(c, { p: principal, at: tokens.access_token, rt: tokens.refresh_token });
111
- return c.redirect("/");
120
+ return c.redirect(safeReturnTo(txn.r) ?? "/");
112
121
  });
113
122
  routes.post("/refresh", async (c) => {
114
123
  const config = cfg(c);
@@ -182,6 +191,7 @@ function godxSso(getConfig) {
182
191
  return { routes, requireAuth, requireWritable, dockProxy };
183
192
  }
184
193
  export {
185
- godxSso
194
+ godxSso,
195
+ safeReturnTo
186
196
  };
187
197
  //# sourceMappingURL=hono.js.map
package/dist/hono.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hono.ts"],"sourcesContent":["/**\n * ./hono — thin adapter over ./auth + ./http for Hono apps on Workers.\n * Mounts the browser SSO routes and exposes the auth middlewares.\n * Everything platform-shaped stays in the core modules.\n */\nimport { Hono } from \"hono\";\nimport type { Context, Env as HonoEnv, MiddlewareHandler } from \"hono\";\nimport { deleteCookie, getCookie, setCookie } from \"hono/cookie\";\nimport { createPkce, openData, randomToken, sealData, verifyAccessToken } from \"./auth\";\nimport type { Principal } from \"./auth\";\nimport { GodxSsoClient, loadIssuer } from \"./http\";\n\nexport interface GodxSsoConfig {\n issuer: string;\n serviceSlug: string;\n clientId: string;\n clientSecret?: string;\n redirectUri: string;\n sessionSecret: string;\n /**\n * Requested scopes. Default `openid profile email offline_access`: `offline_access` gets a\n * refresh token, so a session outlives one access token (see `POST <mount>/refresh`).\n */\n scope?: string;\n /**\n * How long a session with a refresh token may live, in seconds (default 30 days = the\n * platform's refresh-token lifetime). The access token inside is still short-lived.\n */\n sessionMaxAge?: number;\n /** Token audience = your ServiceInstance slug (defaults to serviceSlug). */\n audience?: string;\n /**\n * console_organization_id to fall back on when the login was not started\n * from a platform launch URL (which carries organization_context_id).\n * Useful in local dev; leave unset in production multi-org setups.\n */\n defaultOrganizationContextId?: string;\n}\n\n/** p = verified principal, at = access token, rt = refresh token (offline_access). */\ntype SessionData = { p: Principal; at: string; rt?: string };\n\nconst DEFAULT_SCOPE = \"openid profile email offline_access\";\nconst DEFAULT_SESSION_MAX_AGE = 30 * 24 * 3600;\n\nconst TXN_COOKIE = \"gx_txn\";\nconst SESSION_COOKIE = \"gx_sess\";\n\nexport type GodxVariables = { principal: Principal; accessToken: string };\n\nexport function godxSso<E extends HonoEnv & { Variables: GodxVariables }>(\n getConfig: (env: E[\"Bindings\"]) => GodxSsoConfig,\n) {\n const client = (c: Context<E>) => new GodxSsoClient(getConfig(c.env));\n const cfg = (c: Context<E>) => getConfig(c.env);\n const secure = (c: Context<E>) => c.req.url.startsWith(\"https:\");\n\n /**\n * With a refresh token the cookie lives as long as the refresh token may (the access token\n * inside still expires on time — requireAuth checks it); without one, exactly as long as the\n * access token, as before.\n */\n const writeSession = async (c: Context<E>, data: SessionData) => {\n const config = cfg(c);\n const ttl = data.rt\n ? (config.sessionMaxAge ?? DEFAULT_SESSION_MAX_AGE)\n : Math.max(0, data.p.expiresAt - Math.floor(Date.now() / 1000));\n setCookie(c, SESSION_COOKIE, await sealData(data, config.sessionSecret, ttl), {\n httpOnly: true,\n secure: secure(c),\n sameSite: \"Lax\",\n path: \"/\",\n maxAge: ttl,\n });\n };\n\n const routes = new Hono<E>();\n\n routes.get(\"/login\", async (c) => {\n const config = cfg(c);\n const pkce = await createPkce();\n const state = randomToken();\n const nonce = randomToken();\n // The platform launch URL carries organization_context_id; forward it, or\n // fall back to the configured default (single-org / local dev).\n const organizationContextId =\n c.req.query(\"organization_context_id\") ?? config.defaultOrganizationContextId;\n setCookie(c, TXN_COOKIE, await sealData({ v: pkce.verifier, s: state }, config.sessionSecret, 300), {\n httpOnly: true,\n secure: secure(c),\n sameSite: \"Lax\",\n path: \"/\",\n maxAge: 300,\n });\n return c.redirect(\n await client(c).authorizeUrl({\n state,\n nonce,\n codeChallenge: pkce.challenge,\n scope: config.scope ?? DEFAULT_SCOPE,\n organizationContextId,\n }),\n );\n });\n\n // Every failure lands back on the app with a human-readable description —\n // the SPA renders it; a bare 400 helps nobody.\n const failRedirect = (c: Context<E>, error: string, description: string) =>\n c.redirect(\n `/?sso_error=${encodeURIComponent(error)}&sso_error_description=${encodeURIComponent(description)}`,\n );\n\n routes.get(\"/callback\", async (c) => {\n const config = cfg(c);\n\n // The platform denied the launch (access_denied etc.) and already wrote a\n // localized, human-readable explanation into error_description. Show it.\n const platformError = c.req.query(\"error\");\n if (platformError) {\n return failRedirect(\n c,\n platformError,\n c.req.query(\"error_description\") ?? `サインインが拒否されました(${platformError})。`,\n );\n }\n\n const code = c.req.query(\"code\");\n const state = c.req.query(\"state\");\n const txnCookie = getCookie(c, TXN_COOKIE);\n deleteCookie(c, TXN_COOKIE, { path: \"/\" });\n\n const txn = txnCookie ? await openData<{ v: string; s: string }>(txnCookie, config.sessionSecret) : null;\n if (!code || !state || !txn || txn.s !== state) {\n return failRedirect(\n c,\n \"transaction_invalid\",\n \"ログイン処理の有効期限が切れたか、途中で中断されました。もう一度サインインしてください。\",\n );\n }\n\n let tokens;\n try {\n tokens = await client(c).exchangeCode({ code, codeVerifier: txn.v });\n } catch (e) {\n return failRedirect(\n c,\n \"token_exchange_failed\",\n `認可コードの交換に失敗しました(認可コードは一度しか使えません)。もう一度サインインしてください。詳細: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n\n let verified;\n try {\n verified = await verifyAccessToken(tokens.access_token, config);\n } catch (e) {\n return failRedirect(\n c,\n \"token_verification_failed\",\n `トークンの検証に失敗しました。サービス設定(issuer / service_slug)を確認してください。詳細: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n\n // One userinfo call at login so the UI has a display name without further round-trips.\n let principal = verified;\n try {\n const info = await client(c).userinfo(tokens.access_token);\n principal = Object.freeze({ ...verified, name: info.name, email: info.email });\n } catch {\n // profile display data is optional; the verified principal stands on its own\n }\n\n await writeSession(c, { p: principal, at: tokens.access_token, rt: tokens.refresh_token });\n return c.redirect(\"/\");\n });\n\n /**\n * Renews an expired (or expiring) session with its refresh token. The platform ROTATES refresh\n * tokens and revokes the whole family when an old one is replayed, so this is a dedicated route\n * the browser calls ONCE (single-flight) when an API answers 401 `refreshable: true`, then\n * retries — refreshing inside `requireAuth` would let parallel requests replay the same token\n * and log the user out.\n *\n * 204 = renewed (new cookie set). 401 = no refresh token, or the platform refused it (expired,\n * revoked, membership gone): the session cookie is cleared and the app should sign in again.\n */\n routes.post(\"/refresh\", async (c) => {\n const config = cfg(c);\n const raw = getCookie(c, SESSION_COOKIE);\n const session = raw ? await openData<SessionData>(raw, config.sessionSecret) : null;\n if (!session?.rt) return c.json({ error: \"unauthenticated\", refreshable: false }, 401);\n try {\n const tokens = await client(c).refresh(session.rt);\n const verified = await verifyAccessToken(tokens.access_token, config);\n // Display data came from userinfo at login; the same person, so it carries over.\n const principal = Object.freeze({ ...verified, name: session.p.name, email: session.p.email });\n await writeSession(c, { p: principal, at: tokens.access_token, rt: tokens.refresh_token ?? session.rt });\n return c.body(null, 204);\n } catch (e) {\n // The platform's reason (invalid_grant, a revoked family, …) is the only clue an operator gets.\n console.warn(\"godx-sso: refresh failed\", e instanceof Error ? e.message : String(e));\n deleteCookie(c, SESSION_COOKIE, { path: \"/\" });\n return c.json({ error: \"unauthenticated\", refreshable: false }, 401);\n }\n });\n\n routes.get(\"/logout\", async (c) => {\n const config = cfg(c);\n const raw = getCookie(c, SESSION_COOKIE);\n deleteCookie(c, SESSION_COOKIE, { path: \"/\" });\n if (raw) {\n const session = await openData<SessionData>(raw, config.sessionSecret);\n if (session) {\n await client(c).revoke(session.at).catch(() => {});\n // The refresh token outlives the access token by weeks: it must not survive a sign-out.\n if (session.rt) await client(c).revoke(session.rt).catch(() => {});\n }\n }\n const { metadata } = await loadIssuer(config.issuer);\n return c.redirect(metadata.end_session_endpoint ?? \"/\");\n });\n\n const requireAuth: MiddlewareHandler<E> = async (c, next) => {\n const raw = getCookie(c, SESSION_COOKIE);\n const session = raw ? await openData<SessionData>(raw, cfg(c).sessionSecret) : null;\n if (!session || session.p.expiresAt <= Math.floor(Date.now() / 1000)) {\n // `refreshable`: POST <mount>/refresh can renew it without a new sign-in.\n return c.json({ error: \"unauthenticated\", refreshable: !!session?.rt }, 401);\n }\n c.set(\"principal\", session.p);\n c.set(\"accessToken\", session.at);\n await next();\n };\n\n /**\n * Forwards /api/dock/v1/* to the platform with the session's Bearer token —\n * the GoDX Dock's `apiBase` lane: no CORS, no token in the page. Mount it\n * BEHIND requireAuth and BEFORE any requireWritable (Dock actions are the\n * platform's calls to authorize, not this app's writes).\n */\n const dockProxy: MiddlewareHandler<E> = async (c) => {\n const config = cfg(c);\n const url = new URL(c.req.url);\n const target = new URL(url.pathname + url.search, config.issuer);\n const method = c.req.method;\n const res = await fetch(target, {\n method,\n headers: {\n Authorization: `Bearer ${c.get(\"accessToken\")}`,\n Accept: \"application/json\",\n \"Content-Type\": c.req.header(\"Content-Type\") ?? \"application/json\",\n },\n body: method === \"GET\" || method === \"HEAD\" ? undefined : await c.req.arrayBuffer(),\n });\n const headers = new Headers({ \"Content-Type\": res.headers.get(\"Content-Type\") ?? \"application/json\" });\n const requestId = res.headers.get(\"X-Request-Id\");\n if (requestId) headers.set(\"X-Request-Id\", requestId);\n return new Response(res.body, { status: res.status, headers });\n };\n\n /** Enforces organization_access_mode: read-only orgs may not write. */\n const requireWritable: MiddlewareHandler<E> = async (c, next) => {\n const safe = c.req.method === \"GET\" || c.req.method === \"HEAD\" || c.req.method === \"OPTIONS\";\n if (!safe && c.get(\"principal\")?.accessMode !== \"full\") {\n return c.json({ error: \"organization is read-only\" }, 403);\n }\n await next();\n };\n\n return { routes, requireAuth, requireWritable, dockProxy };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/hono.ts"],"sourcesContent":["/**\n * ./hono — thin adapter over ./auth + ./http for Hono apps on Workers.\n * Mounts the browser SSO routes and exposes the auth middlewares.\n * Everything platform-shaped stays in the core modules.\n */\nimport { Hono } from \"hono\";\nimport type { Context, Env as HonoEnv, MiddlewareHandler } from \"hono\";\nimport { deleteCookie, getCookie, setCookie } from \"hono/cookie\";\nimport { createPkce, openData, randomToken, sealData, verifyAccessToken } from \"./auth\";\nimport type { Principal } from \"./auth\";\nimport { GodxSsoClient, loadIssuer } from \"./http\";\n\nexport interface GodxSsoConfig {\n issuer: string;\n serviceSlug: string;\n clientId: string;\n clientSecret?: string;\n redirectUri: string;\n sessionSecret: string;\n /**\n * Requested scopes. Default `openid profile email offline_access`: `offline_access` gets a\n * refresh token, so a session outlives one access token (see `POST <mount>/refresh`).\n */\n scope?: string;\n /**\n * How long a session with a refresh token may live, in seconds (default 30 days = the\n * platform's refresh-token lifetime). The access token inside is still short-lived.\n */\n sessionMaxAge?: number;\n /** Token audience = your ServiceInstance slug (defaults to serviceSlug). */\n audience?: string;\n /**\n * console_organization_id to fall back on when the login was not started\n * from a platform launch URL (which carries organization_context_id).\n * Useful in local dev; leave unset in production multi-org setups.\n */\n defaultOrganizationContextId?: string;\n}\n\n/** p = verified principal, at = access token, rt = refresh token (offline_access). */\ntype SessionData = { p: Principal; at: string; rt?: string };\n\nconst DEFAULT_SCOPE = \"openid profile email offline_access\";\nconst DEFAULT_SESSION_MAX_AGE = 30 * 24 * 3600;\n\nconst TXN_COOKIE = \"gx_txn\";\n\n/**\n * `return_to` for `/login`: only a same-origin path (\"/mail?x=1\") is kept — anything that a\n * browser could resolve to another origin (\"//evil\", \"/\\\\evil\", \"https://…\", control\n * characters) is dropped, so the login flow can never become an open redirect.\n */\nexport function safeReturnTo(raw: string | undefined | null): string | undefined {\n if (!raw || raw.length > 2048 || !/^\\/(?![/\\\\])/.test(raw) || /[\\u0000-\\u001f\\u007f\\\\]/.test(raw)) {\n return undefined;\n }\n const base = \"https://return-to.invalid\";\n const url = new URL(raw, base);\n return url.origin === base ? url.pathname + url.search + url.hash : undefined;\n}\n\nconst SESSION_COOKIE = \"gx_sess\";\n\nexport type GodxVariables = { principal: Principal; accessToken: string };\n\nexport function godxSso<E extends HonoEnv & { Variables: GodxVariables }>(\n getConfig: (env: E[\"Bindings\"]) => GodxSsoConfig,\n) {\n const client = (c: Context<E>) => new GodxSsoClient(getConfig(c.env));\n const cfg = (c: Context<E>) => getConfig(c.env);\n const secure = (c: Context<E>) => c.req.url.startsWith(\"https:\");\n\n /**\n * With a refresh token the cookie lives as long as the refresh token may (the access token\n * inside still expires on time — requireAuth checks it); without one, exactly as long as the\n * access token, as before.\n */\n const writeSession = async (c: Context<E>, data: SessionData) => {\n const config = cfg(c);\n const ttl = data.rt\n ? (config.sessionMaxAge ?? DEFAULT_SESSION_MAX_AGE)\n : Math.max(0, data.p.expiresAt - Math.floor(Date.now() / 1000));\n setCookie(c, SESSION_COOKIE, await sealData(data, config.sessionSecret, ttl), {\n httpOnly: true,\n secure: secure(c),\n sameSite: \"Lax\",\n path: \"/\",\n maxAge: ttl,\n });\n };\n\n const routes = new Hono<E>();\n\n routes.get(\"/login\", async (c) => {\n const config = cfg(c);\n const pkce = await createPkce();\n const state = randomToken();\n const nonce = randomToken();\n // The platform launch URL carries organization_context_id; forward it, or\n // fall back to the configured default (single-org / local dev).\n const organizationContextId =\n c.req.query(\"organization_context_id\") ?? config.defaultOrganizationContextId;\n // Where to land after the callback (an expired session re-signs in on the page it was on).\n const returnTo = safeReturnTo(c.req.query(\"return_to\"));\n setCookie(c, TXN_COOKIE, await sealData({ v: pkce.verifier, s: state, r: returnTo }, config.sessionSecret, 300), {\n httpOnly: true,\n secure: secure(c),\n sameSite: \"Lax\",\n path: \"/\",\n maxAge: 300,\n });\n return c.redirect(\n await client(c).authorizeUrl({\n state,\n nonce,\n codeChallenge: pkce.challenge,\n scope: config.scope ?? DEFAULT_SCOPE,\n organizationContextId,\n }),\n );\n });\n\n // Every failure lands back on the app with a human-readable description —\n // the SPA renders it; a bare 400 helps nobody.\n const failRedirect = (c: Context<E>, error: string, description: string) =>\n c.redirect(\n `/?sso_error=${encodeURIComponent(error)}&sso_error_description=${encodeURIComponent(description)}`,\n );\n\n routes.get(\"/callback\", async (c) => {\n const config = cfg(c);\n\n // The platform denied the launch (access_denied etc.) and already wrote a\n // localized, human-readable explanation into error_description. Show it.\n const platformError = c.req.query(\"error\");\n if (platformError) {\n return failRedirect(\n c,\n platformError,\n c.req.query(\"error_description\") ?? `サインインが拒否されました(${platformError})。`,\n );\n }\n\n const code = c.req.query(\"code\");\n const state = c.req.query(\"state\");\n const txnCookie = getCookie(c, TXN_COOKIE);\n deleteCookie(c, TXN_COOKIE, { path: \"/\" });\n\n const txn = txnCookie ? await openData<{ v: string; s: string; r?: string }>(txnCookie, config.sessionSecret) : null;\n if (!code || !state || !txn || txn.s !== state) {\n return failRedirect(\n c,\n \"transaction_invalid\",\n \"ログイン処理の有効期限が切れたか、途中で中断されました。もう一度サインインしてください。\",\n );\n }\n\n let tokens;\n try {\n tokens = await client(c).exchangeCode({ code, codeVerifier: txn.v });\n } catch (e) {\n return failRedirect(\n c,\n \"token_exchange_failed\",\n `認可コードの交換に失敗しました(認可コードは一度しか使えません)。もう一度サインインしてください。詳細: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n\n let verified;\n try {\n verified = await verifyAccessToken(tokens.access_token, config);\n } catch (e) {\n return failRedirect(\n c,\n \"token_verification_failed\",\n `トークンの検証に失敗しました。サービス設定(issuer / service_slug)を確認してください。詳細: ${e instanceof Error ? e.message : String(e)}`,\n );\n }\n\n // One userinfo call at login so the UI has a display name without further round-trips.\n let principal = verified;\n try {\n const info = await client(c).userinfo(tokens.access_token);\n principal = Object.freeze({ ...verified, name: info.name, email: info.email });\n } catch {\n // profile display data is optional; the verified principal stands on its own\n }\n\n await writeSession(c, { p: principal, at: tokens.access_token, rt: tokens.refresh_token });\n return c.redirect(safeReturnTo(txn.r) ?? \"/\");\n });\n\n /**\n * Renews an expired (or expiring) session with its refresh token. The platform ROTATES refresh\n * tokens and revokes the whole family when an old one is replayed, so this is a dedicated route\n * the browser calls ONCE (single-flight) when an API answers 401 `refreshable: true`, then\n * retries — refreshing inside `requireAuth` would let parallel requests replay the same token\n * and log the user out.\n *\n * 204 = renewed (new cookie set). 401 = no refresh token, or the platform refused it (expired,\n * revoked, membership gone): the session cookie is cleared and the app should sign in again.\n */\n routes.post(\"/refresh\", async (c) => {\n const config = cfg(c);\n const raw = getCookie(c, SESSION_COOKIE);\n const session = raw ? await openData<SessionData>(raw, config.sessionSecret) : null;\n if (!session?.rt) return c.json({ error: \"unauthenticated\", refreshable: false }, 401);\n try {\n const tokens = await client(c).refresh(session.rt);\n const verified = await verifyAccessToken(tokens.access_token, config);\n // Display data came from userinfo at login; the same person, so it carries over.\n const principal = Object.freeze({ ...verified, name: session.p.name, email: session.p.email });\n await writeSession(c, { p: principal, at: tokens.access_token, rt: tokens.refresh_token ?? session.rt });\n return c.body(null, 204);\n } catch (e) {\n // The platform's reason (invalid_grant, a revoked family, …) is the only clue an operator gets.\n console.warn(\"godx-sso: refresh failed\", e instanceof Error ? e.message : String(e));\n deleteCookie(c, SESSION_COOKIE, { path: \"/\" });\n return c.json({ error: \"unauthenticated\", refreshable: false }, 401);\n }\n });\n\n routes.get(\"/logout\", async (c) => {\n const config = cfg(c);\n const raw = getCookie(c, SESSION_COOKIE);\n deleteCookie(c, SESSION_COOKIE, { path: \"/\" });\n if (raw) {\n const session = await openData<SessionData>(raw, config.sessionSecret);\n if (session) {\n await client(c).revoke(session.at).catch(() => {});\n // The refresh token outlives the access token by weeks: it must not survive a sign-out.\n if (session.rt) await client(c).revoke(session.rt).catch(() => {});\n }\n }\n const { metadata } = await loadIssuer(config.issuer);\n return c.redirect(metadata.end_session_endpoint ?? \"/\");\n });\n\n const requireAuth: MiddlewareHandler<E> = async (c, next) => {\n const raw = getCookie(c, SESSION_COOKIE);\n const session = raw ? await openData<SessionData>(raw, cfg(c).sessionSecret) : null;\n if (!session || session.p.expiresAt <= Math.floor(Date.now() / 1000)) {\n // `refreshable`: POST <mount>/refresh can renew it without a new sign-in.\n return c.json({ error: \"unauthenticated\", refreshable: !!session?.rt }, 401);\n }\n c.set(\"principal\", session.p);\n c.set(\"accessToken\", session.at);\n await next();\n };\n\n /**\n * Forwards /api/dock/v1/* to the platform with the session's Bearer token —\n * the GoDX Dock's `apiBase` lane: no CORS, no token in the page. Mount it\n * BEHIND requireAuth and BEFORE any requireWritable (Dock actions are the\n * platform's calls to authorize, not this app's writes).\n */\n const dockProxy: MiddlewareHandler<E> = async (c) => {\n const config = cfg(c);\n const url = new URL(c.req.url);\n const target = new URL(url.pathname + url.search, config.issuer);\n const method = c.req.method;\n const res = await fetch(target, {\n method,\n headers: {\n Authorization: `Bearer ${c.get(\"accessToken\")}`,\n Accept: \"application/json\",\n \"Content-Type\": c.req.header(\"Content-Type\") ?? \"application/json\",\n },\n body: method === \"GET\" || method === \"HEAD\" ? undefined : await c.req.arrayBuffer(),\n });\n const headers = new Headers({ \"Content-Type\": res.headers.get(\"Content-Type\") ?? \"application/json\" });\n const requestId = res.headers.get(\"X-Request-Id\");\n if (requestId) headers.set(\"X-Request-Id\", requestId);\n return new Response(res.body, { status: res.status, headers });\n };\n\n /** Enforces organization_access_mode: read-only orgs may not write. */\n const requireWritable: MiddlewareHandler<E> = async (c, next) => {\n const safe = c.req.method === \"GET\" || c.req.method === \"HEAD\" || c.req.method === \"OPTIONS\";\n if (!safe && c.get(\"principal\")?.accessMode !== \"full\") {\n return c.json({ error: \"organization is read-only\" }, 403);\n }\n await next();\n };\n\n return { routes, requireAuth, requireWritable, dockProxy };\n}\n"],"mappings":"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","names":[]}
package/dist/sync.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- type IdentityFamily = "organization" | "brand" | "branch" | "user" | "employee" | "service_user_access" | "org_service_role" | "role_assignment";
1
+ type IdentityFamily = "organization" | "brand" | "branch" | "user" | "employee" | "service_user_access" | "org_service_role" | "role_assignment" | "organization_member" | "team" | "team_member";
2
2
  declare const KNOWN_CONTRACT_VERSIONS: readonly ["v1", "v2"];
3
3
  /** CloudEvents 1.0 envelope as the relay publishes it (contract §4). */
4
4
  interface IdentityEnvelope {
package/dist/sync.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sync.ts"],"sourcesContent":["/**\n * ./sync — the identity-feed contracts (docs/reference/identity-feed-protocol.md,\n * godx-jp/id, contract v2), mirrored from php-sdk's Sync module boundary.\n *\n * What ships today is the HTTP leg — snapshot reconciliation — because it is\n * the one a Cloudflare Worker can run (a JetStream durable needs a long-lived\n * process; run that on a Node host and feed the same Projector). The envelope\n * types and the Transport/Inbox seams are here so both legs share one shape.\n *\n * Contract rules encoded below, in the contract's own words:\n * - \"A consumer that does not recognise contract_version must stop, not guess.\"\n * - Snapshot first, THEN resume the stream from `sequence` — never the reverse.\n * - `data` is a full mirrored snapshot, not a diff; absent ≠ null.\n * - CloudEvents `id` is the idempotency key; `sequence` is the ordering fence.\n */\nimport { GodxHttpError } from \"./http\";\n\nexport type IdentityFamily =\n | \"organization\"\n | \"brand\"\n | \"branch\"\n | \"user\"\n | \"employee\"\n | \"service_user_access\"\n | \"org_service_role\"\n | \"role_assignment\";\n\nexport const KNOWN_CONTRACT_VERSIONS = [\"v1\", \"v2\"] as const;\n\n/** CloudEvents 1.0 envelope as the relay publishes it (contract §4). */\nexport interface IdentityEnvelope {\n specversion: \"1.0\";\n /** UUIDv4 — THE idempotency key (same across multi-tenant fan-out copies). */\n id: string;\n /** Platform origin — pin per environment and verify. */\n source: string;\n /** jp.godx.identity.{family}.{action} */\n type: string;\n /** {family}/{id-the-consumer-holds} — exactly one \"/\", both halves filled. */\n subject: string;\n time: string;\n datacontenttype: \"application/json\";\n /** Globally monotonic ordering fence (identity_outbox.sequence). */\n sequence: number;\n data: Record<string, unknown>;\n}\n\nexport interface ParsedSubject {\n family: IdentityFamily | (string & {});\n id: string;\n}\n\nexport class SyncContractError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"SyncContractError\";\n }\n}\n\n/** Split a CloudEvents/snapshot `subject`. Rejects anything the contract rejects. */\nexport function parseSubject(subject: string): ParsedSubject {\n const parts = subject.split(\"/\");\n if (parts.length !== 2 || parts[0] === \"\" || parts[1] === \"\") {\n throw new SyncContractError(\n `identity feed subject \"${subject}\" is not \"{family}/{id}\" — exactly one \"/\" with both halves filled.`,\n );\n }\n return { family: parts[0], id: parts[1] };\n}\n\n/** Validate a raw payload into an IdentityEnvelope, with expected-vs-actual errors. */\nexport function parseEnvelope(raw: unknown, opts?: { expectedSource?: string }): IdentityEnvelope {\n const e = raw as Partial<IdentityEnvelope> | null;\n if (!e || typeof e !== \"object\") throw new SyncContractError(\"envelope is not an object\");\n if (e.specversion !== \"1.0\") {\n throw new SyncContractError(`envelope specversion is ${JSON.stringify(e.specversion)}, expected \"1.0\" — rejected outright, as the php-sdk does.`);\n }\n if (typeof e.id !== \"string\" || e.id === \"\") throw new SyncContractError(\"envelope has no CloudEvents id (the idempotency key)\");\n if (typeof e.type !== \"string\" || !e.type.startsWith(\"jp.godx.identity.\")) {\n throw new SyncContractError(`envelope type ${JSON.stringify(e.type)} does not start with \"jp.godx.identity.\"`);\n }\n if (typeof e.sequence !== \"number\") throw new SyncContractError(\"envelope has no numeric sequence (the ordering fence)\");\n if (typeof e.subject !== \"string\") throw new SyncContractError(\"envelope has no subject\");\n parseSubject(e.subject);\n if (opts?.expectedSource !== undefined && e.source !== opts.expectedSource) {\n throw new SyncContractError(\n `envelope source is ${JSON.stringify(e.source)}, expected ${JSON.stringify(opts.expectedSource)} — pin the platform origin per environment.`,\n );\n }\n return e as IdentityEnvelope;\n}\n\n/**\n * What a consumer implements to hold a projection — one code path for\n * snapshot rows and stream events, exactly as the contract intends\n * (\"rows[].subject is byte-identical to the CloudEvents subject\").\n */\nexport interface Projector {\n /** Upsert the full mirrored payload for this subject (data is never a diff). */\n apply(subject: ParsedSubject, data: Record<string, unknown>, context: ProjectorContext): Promise<void>;\n}\n\nexport interface ProjectorContext {\n organizationId: string;\n /** The watermark this row is consistent up to. */\n sequence: number;\n}\n\n/** The stream-leg seam (JetStream durable on a Node host). Not implemented here yet. */\nexport interface Transport {\n pull(handler: (envelope: IdentityEnvelope) => Promise<void>): Promise<void>;\n}\n\n/** Dedupe seam keyed on the CloudEvents id — the consumer's job, per contract §4. */\nexport interface Inbox {\n /** true when this id was already applied — skip it. */\n seen(eventId: string): Promise<boolean>;\n record(eventId: string, sequence: number): Promise<void>;\n}\n\n/**\n * Which (source, organization) pairs this consumer accepts — php-sdk's\n * TenantPolicy. Deny is a `terminated` outcome, never an exception: an event\n * for a tenant that is not yours is noise, not an incident.\n */\nexport interface TenantPolicy {\n allows(source: string, organizationId: string | null): boolean;\n}\n\nexport const acceptTenants = (source: string, organizationIds: readonly string[]): TenantPolicy => ({\n allows: (s, org) => s === source && (org === null || organizationIds.includes(org)),\n});\n\n/**\n * What happened to one delivery — mirrors php-sdk's Outcome and the contract's\n * nak-vs-term rule. The HTTP mapping (see handleIngestRequest) is:\n * applied/skipped_* → 2xx (ack) · retry → 5xx (nak, redeliver) ·\n * terminated → 200 with the outcome named (term: redelivery cannot help).\n */\nexport type Outcome =\n | \"applied\"\n | \"skipped_duplicate\"\n | \"skipped_stale\"\n | \"terminated_unknown_type\"\n | \"terminated_tenant_refused\"\n | \"retry\";\n\nexport interface Delivery {\n outcome: Outcome;\n eventId: string;\n /** Present on terminated/retry — the human-readable why. */\n reason?: string;\n}\n\n/**\n * The one pipeline both legs feed: validate → dedupe → tenant gate → project\n * → record. A snapshot pass uses the Projector directly; every EVENT goes\n * through here.\n */\nexport function createProcessor(deps: {\n projector: Projector;\n inbox: Inbox;\n tenantPolicy?: TenantPolicy;\n /** Families this consumer mirrors; events for others are terminated (contract §3 nak-vs-term). */\n families?: readonly IdentityFamily[];\n}) {\n return async (envelope: IdentityEnvelope): Promise<Delivery> => {\n const { id } = envelope;\n let subject: ParsedSubject;\n try {\n subject = parseSubject(envelope.subject);\n } catch (e) {\n return { outcome: \"terminated_unknown_type\", eventId: id, reason: (e as Error).message };\n }\n\n if (deps.families && !deps.families.includes(subject.family as IdentityFamily)) {\n // Unknown family → term, not nak: redelivering cannot make us know it.\n return {\n outcome: \"terminated_unknown_type\",\n eventId: id,\n reason: `family \"${subject.family}\" is not mirrored here (mirrored: ${deps.families.join(\", \")})`,\n };\n }\n\n const organizationId =\n typeof envelope.data[\"console_organization_id\"] === \"string\"\n ? (envelope.data[\"console_organization_id\"] as string)\n : null;\n if (deps.tenantPolicy && !deps.tenantPolicy.allows(envelope.source, organizationId)) {\n return {\n outcome: \"terminated_tenant_refused\",\n eventId: id,\n reason: `source=${envelope.source} organization=${organizationId ?? \"(none)\"} is not accepted by this consumer's tenant policy`,\n };\n }\n\n if (await deps.inbox.seen(id)) {\n return { outcome: \"skipped_duplicate\", eventId: id };\n }\n\n try {\n await deps.projector.apply(subject, envelope.data, {\n organizationId: organizationId ?? \"\",\n sequence: envelope.sequence,\n });\n await deps.inbox.record(id, envelope.sequence);\n return { outcome: \"applied\", eventId: id };\n } catch (e) {\n // Projection failure is transient until proven otherwise → nak/redeliver.\n return { outcome: \"retry\", eventId: id, reason: e instanceof Error ? e.message : String(e) };\n }\n };\n}\n\n// ---------------------------------------------------------------------------\n// Inbox on Cloudflare D1 (or anything with the same prepare/bind surface).\n// ---------------------------------------------------------------------------\n\n/** Structural subset of D1Database, so this module has no CF type dependency. */\ninterface D1Like {\n prepare(sql: string): {\n bind(...values: unknown[]): { first<T = unknown>(): Promise<T | null>; run(): Promise<unknown> };\n };\n}\n\n/** DDL for the inbox table — copy into a migration verbatim. */\nexport const D1_INBOX_DDL = `CREATE TABLE IF NOT EXISTS godx_sync_inbox (\n event_id TEXT PRIMARY KEY,\n sequence INTEGER NOT NULL,\n applied_at INTEGER NOT NULL\n);`;\n\nexport function createD1Inbox(db: D1Like, table = \"godx_sync_inbox\"): Inbox {\n return {\n async seen(eventId) {\n const row = await db.prepare(`SELECT 1 AS x FROM ${table} WHERE event_id = ?`).bind(eventId).first();\n return row !== null;\n },\n async record(eventId, sequence) {\n await db\n .prepare(`INSERT OR IGNORE INTO ${table} (event_id, sequence, applied_at) VALUES (?, ?, ?)`)\n .bind(eventId, sequence, Date.now())\n .run();\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// HTTP ingest — the event leg a Worker CAN run.\n//\n// A JetStream durable needs a long-lived process, so a bridge (Node host, or\n// the platform itself one day) pulls the stream and POSTs each event here.\n// Two international standards, applied as written:\n//\n// - CloudEvents HTTP protocol binding, structured mode:\n// Content-Type application/cloudevents+json (one event) or\n// application/cloudevents-batch+json (an array).\n// - Standard Webhooks (https://www.standardwebhooks.com) authentication:\n// webhook-id / webhook-timestamp / webhook-signature headers, where\n// signature = v1,base64(HMAC-SHA256(secret, \"{id}.{timestamp}.{body}\")),\n// secret encoded as whsec_<base64>, timestamp tolerance ±5 minutes.\n// ---------------------------------------------------------------------------\n\nconst WEBHOOK_TOLERANCE_SECONDS = 5 * 60;\n\nasync function hmacKey(secret: string): Promise<CryptoKey> {\n const raw = secret.startsWith(\"whsec_\")\n ? Uint8Array.from(atob(secret.slice(6)), (ch) => ch.charCodeAt(0))\n : new TextEncoder().encode(secret);\n return crypto.subtle.importKey(\"raw\", raw, { name: \"HMAC\", hash: \"SHA-256\" }, false, [\n \"sign\",\n \"verify\",\n ]);\n}\n\n/** Produce Standard Webhooks headers for a payload — the bridge's half. */\nexport async function signWebhook(\n secret: string,\n body: string,\n opts?: { id?: string; timestamp?: number },\n): Promise<{ \"webhook-id\": string; \"webhook-timestamp\": string; \"webhook-signature\": string }> {\n const id = opts?.id ?? crypto.randomUUID();\n const timestamp = String(opts?.timestamp ?? Math.floor(Date.now() / 1000));\n const key = await hmacKey(secret);\n const mac = await crypto.subtle.sign(\"HMAC\", key, new TextEncoder().encode(`${id}.${timestamp}.${body}`));\n const signature = `v1,${btoa(String.fromCharCode(...new Uint8Array(mac)))}`;\n return { \"webhook-id\": id, \"webhook-timestamp\": timestamp, \"webhook-signature\": signature };\n}\n\n/**\n * Verify Standard Webhooks headers. Returns null when valid, otherwise the\n * human-readable refusal (never throws on bad input).\n */\nexport async function verifyWebhook(\n secret: string,\n body: string,\n headers: { get(name: string): string | null },\n): Promise<string | null> {\n const id = headers.get(\"webhook-id\");\n const timestamp = headers.get(\"webhook-timestamp\");\n const signatureHeader = headers.get(\"webhook-signature\");\n if (!id || !timestamp || !signatureHeader) {\n return \"missing webhook-id / webhook-timestamp / webhook-signature header (Standard Webhooks)\";\n }\n const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));\n if (!Number.isFinite(age) || age > WEBHOOK_TOLERANCE_SECONDS) {\n return `webhook-timestamp is ${timestamp} — outside the ±${WEBHOOK_TOLERANCE_SECONDS}s tolerance (replay guard)`;\n }\n const key = await hmacKey(secret);\n const expected = new TextEncoder().encode(`${id}.${timestamp}.${body}`);\n // The header may carry several space-separated signatures (key rotation).\n for (const part of signatureHeader.split(\" \")) {\n const [version, value] = part.split(\",\", 2);\n if (version !== \"v1\" || !value) continue;\n let mac: Uint8Array<ArrayBuffer>;\n try {\n mac = Uint8Array.from(atob(value), (ch) => ch.charCodeAt(0));\n } catch {\n continue;\n }\n // crypto.subtle.verify is constant-time — never compare digests as strings.\n if (await crypto.subtle.verify(\"HMAC\", key, mac, expected)) return null;\n }\n return \"no webhook-signature entry matched (wrong secret, or the body was altered in transit)\";\n}\n\nexport interface IngestResult {\n deliveries: Delivery[];\n response: Response;\n}\n\n/**\n * Handle one CloudEvents-over-HTTP delivery. Pure fetch API — mount it from\n * any framework in one line. HTTP mapping of outcomes:\n * every outcome except retry → 200 (ack; body lists per-event outcomes)\n * any retry → 500 (nak: the sender must redeliver)\n * bad signature → 401 · wrong content type → 415 · bad JSON/envelope → 400 (term)\n */\nexport async function handleIngestRequest(\n request: Request,\n deps: {\n secret: string;\n process: (envelope: IdentityEnvelope) => Promise<Delivery>;\n expectedSource?: string;\n },\n): Promise<Response> {\n const json = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { \"Content-Type\": \"application/json\" } });\n\n if (request.method !== \"POST\") return json(405, { error: \"POST only\" });\n\n const contentType = (request.headers.get(\"Content-Type\") ?? \"\").split(\";\")[0].trim();\n const batch = contentType === \"application/cloudevents-batch+json\";\n if (!batch && contentType !== \"application/cloudevents+json\") {\n return json(415, {\n error: `Content-Type \"${contentType}\" is not a CloudEvents structured mode — send application/cloudevents+json (one event) or application/cloudevents-batch+json (array).`,\n });\n }\n\n const body = await request.text();\n const refusal = await verifyWebhook(deps.secret, body, request.headers);\n if (refusal !== null) return json(401, { error: refusal });\n\n let envelopes: IdentityEnvelope[];\n try {\n const parsed: unknown = JSON.parse(body);\n const raw = batch ? parsed : [parsed];\n if (!Array.isArray(raw)) throw new SyncContractError(\"batch content-type but the body is not an array\");\n envelopes = raw.map((e) => parseEnvelope(e, { expectedSource: deps.expectedSource }));\n } catch (e) {\n // Malformed is term, not retry: redelivering the same bytes cannot help.\n return json(400, { error: e instanceof Error ? e.message : String(e) });\n }\n\n const deliveries: Delivery[] = [];\n for (const envelope of envelopes) {\n deliveries.push(await deps.process(envelope));\n }\n const mustRetry = deliveries.some((d) => d.outcome === \"retry\");\n return json(mustRetry ? 500 : 200, { deliveries });\n}\n\nexport interface SnapshotRow {\n subject: string;\n data: Record<string, unknown>;\n}\n\nexport interface SnapshotPage {\n contractVersion: string;\n family: string;\n organizationId: string;\n /** Watermark: resume the stream from here AFTER the pass completes. */\n sequence: number;\n rows: SnapshotRow[];\n nextCursor: string | null;\n}\n\nexport interface ReconcileResult {\n organizationId: string;\n /** The highest watermark of the pass — hand this to the stream leg. */\n sequence: number;\n pages: number;\n rows: number;\n}\n\n/**\n * GET /api/sso/service/snapshot/{family} — cursor-paged keyset read of current\n * state (NOT a replay of history). Auth is the service credential; tenancy is\n * the credential's, never a parameter.\n */\nexport function createSnapshotClient(config: {\n issuer: string;\n serviceSlug: string;\n serviceSecret: string;\n}) {\n const fetchPage = async (\n family: IdentityFamily,\n opts?: { cursor?: string; limit?: number },\n ): Promise<SnapshotPage> => {\n const url = new URL(`/api/sso/service/snapshot/${family}`, config.issuer);\n if (opts?.cursor) url.searchParams.set(\"cursor\", opts.cursor);\n if (opts?.limit) url.searchParams.set(\"limit\", String(opts.limit));\n\n const res = await fetch(url, {\n headers: {\n \"X-Service-Slug\": config.serviceSlug,\n \"X-Service-Secret\": config.serviceSecret,\n Accept: \"application/json\",\n },\n });\n if (!res.ok) {\n const body = await res.text();\n const hint =\n res.status === 410\n ? \"the cursor's watermark aged out of the stream — restart the pass from the beginning (no cursor).\"\n : res.status === 400\n ? \"the cursor is malformed or belongs to another family — cursors are opaque, echo next_cursor verbatim.\"\n : res.status === 403\n ? \"the credential lacks service_api_enabled, or the family is not snapshot-able for it.\"\n : \"check X-Service-Slug/X-Service-Secret and the platform logs (X-Request-Id).\";\n throw new GodxHttpError(res.status, body, `identity snapshot ${family} answered ${res.status} — ${hint}`);\n }\n const b = (await res.json()) as {\n contract_version: string;\n family: string;\n organization_id: string;\n sequence: number;\n rows?: SnapshotRow[];\n next_cursor?: string | null;\n };\n if (!KNOWN_CONTRACT_VERSIONS.includes(b.contract_version as (typeof KNOWN_CONTRACT_VERSIONS)[number])) {\n throw new SyncContractError(\n `snapshot answered contract_version ${JSON.stringify(b.contract_version)}; this SDK knows ${KNOWN_CONTRACT_VERSIONS.join(\", \")} — a consumer that does not recognise it must stop, not guess.`,\n );\n }\n return {\n contractVersion: b.contract_version,\n family: b.family,\n organizationId: b.organization_id,\n sequence: b.sequence,\n rows: b.rows ?? [],\n nextCursor: b.next_cursor ?? null,\n };\n };\n\n /** Page the family to completion, applying rows in order through the Projector. */\n const reconcile = async (\n family: IdentityFamily,\n projector: Projector,\n opts?: { limit?: number },\n ): Promise<ReconcileResult> => {\n let cursor: string | undefined;\n let pages = 0;\n let rows = 0;\n let sequence = 0;\n let organizationId = \"\";\n do {\n const page = await fetchPage(family, { cursor, limit: opts?.limit });\n pages++;\n sequence = Math.max(sequence, page.sequence);\n organizationId = page.organizationId;\n for (const row of page.rows) {\n await projector.apply(parseSubject(row.subject), row.data, {\n organizationId: page.organizationId,\n sequence: page.sequence,\n });\n rows++;\n }\n cursor = page.nextCursor ?? undefined;\n } while (cursor !== undefined);\n return { organizationId, sequence, pages, rows };\n };\n\n return { fetchPage, reconcile };\n}\n"],"mappings":"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","names":["body"]}
1
+ {"version":3,"sources":["../src/sync.ts"],"sourcesContent":["/**\n * ./sync — the identity-feed contracts (docs/reference/identity-feed-protocol.md,\n * godx-jp/id, contract v2), mirrored from php-sdk's Sync module boundary.\n *\n * What ships today is the HTTP leg — snapshot reconciliation — because it is\n * the one a Cloudflare Worker can run (a JetStream durable needs a long-lived\n * process; run that on a Node host and feed the same Projector). The envelope\n * types and the Transport/Inbox seams are here so both legs share one shape.\n *\n * Contract rules encoded below, in the contract's own words:\n * - \"A consumer that does not recognise contract_version must stop, not guess.\"\n * - Snapshot first, THEN resume the stream from `sequence` — never the reverse.\n * - `data` is a full mirrored snapshot, not a diff; absent ≠ null.\n * - CloudEvents `id` is the idempotency key; `sequence` is the ordering fence.\n */\nimport { GodxHttpError } from \"./http\";\n\nexport type IdentityFamily =\n | \"organization\"\n | \"brand\"\n | \"branch\"\n | \"user\"\n | \"employee\"\n | \"service_user_access\"\n | \"org_service_role\"\n | \"role_assignment\"\n | \"organization_member\"\n | \"team\"\n | \"team_member\";\n\nexport const KNOWN_CONTRACT_VERSIONS = [\"v1\", \"v2\"] as const;\n\n/** CloudEvents 1.0 envelope as the relay publishes it (contract §4). */\nexport interface IdentityEnvelope {\n specversion: \"1.0\";\n /** UUIDv4 — THE idempotency key (same across multi-tenant fan-out copies). */\n id: string;\n /** Platform origin — pin per environment and verify. */\n source: string;\n /** jp.godx.identity.{family}.{action} */\n type: string;\n /** {family}/{id-the-consumer-holds} — exactly one \"/\", both halves filled. */\n subject: string;\n time: string;\n datacontenttype: \"application/json\";\n /** Globally monotonic ordering fence (identity_outbox.sequence). */\n sequence: number;\n data: Record<string, unknown>;\n}\n\nexport interface ParsedSubject {\n family: IdentityFamily | (string & {});\n id: string;\n}\n\nexport class SyncContractError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"SyncContractError\";\n }\n}\n\n/** Split a CloudEvents/snapshot `subject`. Rejects anything the contract rejects. */\nexport function parseSubject(subject: string): ParsedSubject {\n const parts = subject.split(\"/\");\n if (parts.length !== 2 || parts[0] === \"\" || parts[1] === \"\") {\n throw new SyncContractError(\n `identity feed subject \"${subject}\" is not \"{family}/{id}\" — exactly one \"/\" with both halves filled.`,\n );\n }\n return { family: parts[0], id: parts[1] };\n}\n\n/** Validate a raw payload into an IdentityEnvelope, with expected-vs-actual errors. */\nexport function parseEnvelope(raw: unknown, opts?: { expectedSource?: string }): IdentityEnvelope {\n const e = raw as Partial<IdentityEnvelope> | null;\n if (!e || typeof e !== \"object\") throw new SyncContractError(\"envelope is not an object\");\n if (e.specversion !== \"1.0\") {\n throw new SyncContractError(`envelope specversion is ${JSON.stringify(e.specversion)}, expected \"1.0\" — rejected outright, as the php-sdk does.`);\n }\n if (typeof e.id !== \"string\" || e.id === \"\") throw new SyncContractError(\"envelope has no CloudEvents id (the idempotency key)\");\n if (typeof e.type !== \"string\" || !e.type.startsWith(\"jp.godx.identity.\")) {\n throw new SyncContractError(`envelope type ${JSON.stringify(e.type)} does not start with \"jp.godx.identity.\"`);\n }\n if (typeof e.sequence !== \"number\") throw new SyncContractError(\"envelope has no numeric sequence (the ordering fence)\");\n if (typeof e.subject !== \"string\") throw new SyncContractError(\"envelope has no subject\");\n parseSubject(e.subject);\n if (opts?.expectedSource !== undefined && e.source !== opts.expectedSource) {\n throw new SyncContractError(\n `envelope source is ${JSON.stringify(e.source)}, expected ${JSON.stringify(opts.expectedSource)} — pin the platform origin per environment.`,\n );\n }\n return e as IdentityEnvelope;\n}\n\n/**\n * What a consumer implements to hold a projection — one code path for\n * snapshot rows and stream events, exactly as the contract intends\n * (\"rows[].subject is byte-identical to the CloudEvents subject\").\n */\nexport interface Projector {\n /** Upsert the full mirrored payload for this subject (data is never a diff). */\n apply(subject: ParsedSubject, data: Record<string, unknown>, context: ProjectorContext): Promise<void>;\n}\n\nexport interface ProjectorContext {\n organizationId: string;\n /** The watermark this row is consistent up to. */\n sequence: number;\n}\n\n/** The stream-leg seam (JetStream durable on a Node host). Not implemented here yet. */\nexport interface Transport {\n pull(handler: (envelope: IdentityEnvelope) => Promise<void>): Promise<void>;\n}\n\n/** Dedupe seam keyed on the CloudEvents id — the consumer's job, per contract §4. */\nexport interface Inbox {\n /** true when this id was already applied — skip it. */\n seen(eventId: string): Promise<boolean>;\n record(eventId: string, sequence: number): Promise<void>;\n}\n\n/**\n * Which (source, organization) pairs this consumer accepts — php-sdk's\n * TenantPolicy. Deny is a `terminated` outcome, never an exception: an event\n * for a tenant that is not yours is noise, not an incident.\n */\nexport interface TenantPolicy {\n allows(source: string, organizationId: string | null): boolean;\n}\n\nexport const acceptTenants = (source: string, organizationIds: readonly string[]): TenantPolicy => ({\n allows: (s, org) => s === source && (org === null || organizationIds.includes(org)),\n});\n\n/**\n * What happened to one delivery — mirrors php-sdk's Outcome and the contract's\n * nak-vs-term rule. The HTTP mapping (see handleIngestRequest) is:\n * applied/skipped_* → 2xx (ack) · retry → 5xx (nak, redeliver) ·\n * terminated → 200 with the outcome named (term: redelivery cannot help).\n */\nexport type Outcome =\n | \"applied\"\n | \"skipped_duplicate\"\n | \"skipped_stale\"\n | \"terminated_unknown_type\"\n | \"terminated_tenant_refused\"\n | \"retry\";\n\nexport interface Delivery {\n outcome: Outcome;\n eventId: string;\n /** Present on terminated/retry — the human-readable why. */\n reason?: string;\n}\n\n/**\n * The one pipeline both legs feed: validate → dedupe → tenant gate → project\n * → record. A snapshot pass uses the Projector directly; every EVENT goes\n * through here.\n */\nexport function createProcessor(deps: {\n projector: Projector;\n inbox: Inbox;\n tenantPolicy?: TenantPolicy;\n /** Families this consumer mirrors; events for others are terminated (contract §3 nak-vs-term). */\n families?: readonly IdentityFamily[];\n}) {\n return async (envelope: IdentityEnvelope): Promise<Delivery> => {\n const { id } = envelope;\n let subject: ParsedSubject;\n try {\n subject = parseSubject(envelope.subject);\n } catch (e) {\n return { outcome: \"terminated_unknown_type\", eventId: id, reason: (e as Error).message };\n }\n\n if (deps.families && !deps.families.includes(subject.family as IdentityFamily)) {\n // Unknown family → term, not nak: redelivering cannot make us know it.\n return {\n outcome: \"terminated_unknown_type\",\n eventId: id,\n reason: `family \"${subject.family}\" is not mirrored here (mirrored: ${deps.families.join(\", \")})`,\n };\n }\n\n const organizationId =\n typeof envelope.data[\"console_organization_id\"] === \"string\"\n ? (envelope.data[\"console_organization_id\"] as string)\n : null;\n if (deps.tenantPolicy && !deps.tenantPolicy.allows(envelope.source, organizationId)) {\n return {\n outcome: \"terminated_tenant_refused\",\n eventId: id,\n reason: `source=${envelope.source} organization=${organizationId ?? \"(none)\"} is not accepted by this consumer's tenant policy`,\n };\n }\n\n if (await deps.inbox.seen(id)) {\n return { outcome: \"skipped_duplicate\", eventId: id };\n }\n\n try {\n await deps.projector.apply(subject, envelope.data, {\n organizationId: organizationId ?? \"\",\n sequence: envelope.sequence,\n });\n await deps.inbox.record(id, envelope.sequence);\n return { outcome: \"applied\", eventId: id };\n } catch (e) {\n // Projection failure is transient until proven otherwise → nak/redeliver.\n return { outcome: \"retry\", eventId: id, reason: e instanceof Error ? e.message : String(e) };\n }\n };\n}\n\n// ---------------------------------------------------------------------------\n// Inbox on Cloudflare D1 (or anything with the same prepare/bind surface).\n// ---------------------------------------------------------------------------\n\n/** Structural subset of D1Database, so this module has no CF type dependency. */\ninterface D1Like {\n prepare(sql: string): {\n bind(...values: unknown[]): { first<T = unknown>(): Promise<T | null>; run(): Promise<unknown> };\n };\n}\n\n/** DDL for the inbox table — copy into a migration verbatim. */\nexport const D1_INBOX_DDL = `CREATE TABLE IF NOT EXISTS godx_sync_inbox (\n event_id TEXT PRIMARY KEY,\n sequence INTEGER NOT NULL,\n applied_at INTEGER NOT NULL\n);`;\n\nexport function createD1Inbox(db: D1Like, table = \"godx_sync_inbox\"): Inbox {\n return {\n async seen(eventId) {\n const row = await db.prepare(`SELECT 1 AS x FROM ${table} WHERE event_id = ?`).bind(eventId).first();\n return row !== null;\n },\n async record(eventId, sequence) {\n await db\n .prepare(`INSERT OR IGNORE INTO ${table} (event_id, sequence, applied_at) VALUES (?, ?, ?)`)\n .bind(eventId, sequence, Date.now())\n .run();\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// HTTP ingest — the event leg a Worker CAN run.\n//\n// A JetStream durable needs a long-lived process, so a bridge (Node host, or\n// the platform itself one day) pulls the stream and POSTs each event here.\n// Two international standards, applied as written:\n//\n// - CloudEvents HTTP protocol binding, structured mode:\n// Content-Type application/cloudevents+json (one event) or\n// application/cloudevents-batch+json (an array).\n// - Standard Webhooks (https://www.standardwebhooks.com) authentication:\n// webhook-id / webhook-timestamp / webhook-signature headers, where\n// signature = v1,base64(HMAC-SHA256(secret, \"{id}.{timestamp}.{body}\")),\n// secret encoded as whsec_<base64>, timestamp tolerance ±5 minutes.\n// ---------------------------------------------------------------------------\n\nconst WEBHOOK_TOLERANCE_SECONDS = 5 * 60;\n\nasync function hmacKey(secret: string): Promise<CryptoKey> {\n const raw = secret.startsWith(\"whsec_\")\n ? Uint8Array.from(atob(secret.slice(6)), (ch) => ch.charCodeAt(0))\n : new TextEncoder().encode(secret);\n return crypto.subtle.importKey(\"raw\", raw, { name: \"HMAC\", hash: \"SHA-256\" }, false, [\n \"sign\",\n \"verify\",\n ]);\n}\n\n/** Produce Standard Webhooks headers for a payload — the bridge's half. */\nexport async function signWebhook(\n secret: string,\n body: string,\n opts?: { id?: string; timestamp?: number },\n): Promise<{ \"webhook-id\": string; \"webhook-timestamp\": string; \"webhook-signature\": string }> {\n const id = opts?.id ?? crypto.randomUUID();\n const timestamp = String(opts?.timestamp ?? Math.floor(Date.now() / 1000));\n const key = await hmacKey(secret);\n const mac = await crypto.subtle.sign(\"HMAC\", key, new TextEncoder().encode(`${id}.${timestamp}.${body}`));\n const signature = `v1,${btoa(String.fromCharCode(...new Uint8Array(mac)))}`;\n return { \"webhook-id\": id, \"webhook-timestamp\": timestamp, \"webhook-signature\": signature };\n}\n\n/**\n * Verify Standard Webhooks headers. Returns null when valid, otherwise the\n * human-readable refusal (never throws on bad input).\n */\nexport async function verifyWebhook(\n secret: string,\n body: string,\n headers: { get(name: string): string | null },\n): Promise<string | null> {\n const id = headers.get(\"webhook-id\");\n const timestamp = headers.get(\"webhook-timestamp\");\n const signatureHeader = headers.get(\"webhook-signature\");\n if (!id || !timestamp || !signatureHeader) {\n return \"missing webhook-id / webhook-timestamp / webhook-signature header (Standard Webhooks)\";\n }\n const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));\n if (!Number.isFinite(age) || age > WEBHOOK_TOLERANCE_SECONDS) {\n return `webhook-timestamp is ${timestamp} — outside the ±${WEBHOOK_TOLERANCE_SECONDS}s tolerance (replay guard)`;\n }\n const key = await hmacKey(secret);\n const expected = new TextEncoder().encode(`${id}.${timestamp}.${body}`);\n // The header may carry several space-separated signatures (key rotation).\n for (const part of signatureHeader.split(\" \")) {\n const [version, value] = part.split(\",\", 2);\n if (version !== \"v1\" || !value) continue;\n let mac: Uint8Array<ArrayBuffer>;\n try {\n mac = Uint8Array.from(atob(value), (ch) => ch.charCodeAt(0));\n } catch {\n continue;\n }\n // crypto.subtle.verify is constant-time — never compare digests as strings.\n if (await crypto.subtle.verify(\"HMAC\", key, mac, expected)) return null;\n }\n return \"no webhook-signature entry matched (wrong secret, or the body was altered in transit)\";\n}\n\nexport interface IngestResult {\n deliveries: Delivery[];\n response: Response;\n}\n\n/**\n * Handle one CloudEvents-over-HTTP delivery. Pure fetch API — mount it from\n * any framework in one line. HTTP mapping of outcomes:\n * every outcome except retry → 200 (ack; body lists per-event outcomes)\n * any retry → 500 (nak: the sender must redeliver)\n * bad signature → 401 · wrong content type → 415 · bad JSON/envelope → 400 (term)\n */\nexport async function handleIngestRequest(\n request: Request,\n deps: {\n secret: string;\n process: (envelope: IdentityEnvelope) => Promise<Delivery>;\n expectedSource?: string;\n },\n): Promise<Response> {\n const json = (status: number, body: unknown) =>\n new Response(JSON.stringify(body), { status, headers: { \"Content-Type\": \"application/json\" } });\n\n if (request.method !== \"POST\") return json(405, { error: \"POST only\" });\n\n const contentType = (request.headers.get(\"Content-Type\") ?? \"\").split(\";\")[0].trim();\n const batch = contentType === \"application/cloudevents-batch+json\";\n if (!batch && contentType !== \"application/cloudevents+json\") {\n return json(415, {\n error: `Content-Type \"${contentType}\" is not a CloudEvents structured mode — send application/cloudevents+json (one event) or application/cloudevents-batch+json (array).`,\n });\n }\n\n const body = await request.text();\n const refusal = await verifyWebhook(deps.secret, body, request.headers);\n if (refusal !== null) return json(401, { error: refusal });\n\n let envelopes: IdentityEnvelope[];\n try {\n const parsed: unknown = JSON.parse(body);\n const raw = batch ? parsed : [parsed];\n if (!Array.isArray(raw)) throw new SyncContractError(\"batch content-type but the body is not an array\");\n envelopes = raw.map((e) => parseEnvelope(e, { expectedSource: deps.expectedSource }));\n } catch (e) {\n // Malformed is term, not retry: redelivering the same bytes cannot help.\n return json(400, { error: e instanceof Error ? e.message : String(e) });\n }\n\n const deliveries: Delivery[] = [];\n for (const envelope of envelopes) {\n deliveries.push(await deps.process(envelope));\n }\n const mustRetry = deliveries.some((d) => d.outcome === \"retry\");\n return json(mustRetry ? 500 : 200, { deliveries });\n}\n\nexport interface SnapshotRow {\n subject: string;\n data: Record<string, unknown>;\n}\n\nexport interface SnapshotPage {\n contractVersion: string;\n family: string;\n organizationId: string;\n /** Watermark: resume the stream from here AFTER the pass completes. */\n sequence: number;\n rows: SnapshotRow[];\n nextCursor: string | null;\n}\n\nexport interface ReconcileResult {\n organizationId: string;\n /** The highest watermark of the pass — hand this to the stream leg. */\n sequence: number;\n pages: number;\n rows: number;\n}\n\n/**\n * GET /api/sso/service/snapshot/{family} — cursor-paged keyset read of current\n * state (NOT a replay of history). Auth is the service credential; tenancy is\n * the credential's, never a parameter.\n */\nexport function createSnapshotClient(config: {\n issuer: string;\n serviceSlug: string;\n serviceSecret: string;\n}) {\n const fetchPage = async (\n family: IdentityFamily,\n opts?: { cursor?: string; limit?: number },\n ): Promise<SnapshotPage> => {\n const url = new URL(`/api/sso/service/snapshot/${family}`, config.issuer);\n if (opts?.cursor) url.searchParams.set(\"cursor\", opts.cursor);\n if (opts?.limit) url.searchParams.set(\"limit\", String(opts.limit));\n\n const res = await fetch(url, {\n headers: {\n \"X-Service-Slug\": config.serviceSlug,\n \"X-Service-Secret\": config.serviceSecret,\n Accept: \"application/json\",\n },\n });\n if (!res.ok) {\n const body = await res.text();\n const hint =\n res.status === 410\n ? \"the cursor's watermark aged out of the stream — restart the pass from the beginning (no cursor).\"\n : res.status === 400\n ? \"the cursor is malformed or belongs to another family — cursors are opaque, echo next_cursor verbatim.\"\n : res.status === 403\n ? \"the credential lacks service_api_enabled, or the family is not snapshot-able for it.\"\n : \"check X-Service-Slug/X-Service-Secret and the platform logs (X-Request-Id).\";\n throw new GodxHttpError(res.status, body, `identity snapshot ${family} answered ${res.status} — ${hint}`);\n }\n const b = (await res.json()) as {\n contract_version: string;\n family: string;\n organization_id: string;\n sequence: number;\n rows?: SnapshotRow[];\n next_cursor?: string | null;\n };\n if (!KNOWN_CONTRACT_VERSIONS.includes(b.contract_version as (typeof KNOWN_CONTRACT_VERSIONS)[number])) {\n throw new SyncContractError(\n `snapshot answered contract_version ${JSON.stringify(b.contract_version)}; this SDK knows ${KNOWN_CONTRACT_VERSIONS.join(\", \")} — a consumer that does not recognise it must stop, not guess.`,\n );\n }\n return {\n contractVersion: b.contract_version,\n family: b.family,\n organizationId: b.organization_id,\n sequence: b.sequence,\n rows: b.rows ?? [],\n nextCursor: b.next_cursor ?? null,\n };\n };\n\n /** Page the family to completion, applying rows in order through the Projector. */\n const reconcile = async (\n family: IdentityFamily,\n projector: Projector,\n opts?: { limit?: number },\n ): Promise<ReconcileResult> => {\n let cursor: string | undefined;\n let pages = 0;\n let rows = 0;\n let sequence = 0;\n let organizationId = \"\";\n do {\n const page = await fetchPage(family, { cursor, limit: opts?.limit });\n pages++;\n sequence = Math.max(sequence, page.sequence);\n organizationId = page.organizationId;\n for (const row of page.rows) {\n await projector.apply(parseSubject(row.subject), row.data, {\n organizationId: page.organizationId,\n sequence: page.sequence,\n });\n rows++;\n }\n cursor = page.nextCursor ?? undefined;\n } while (cursor !== undefined);\n return { organizationId, sequence, pages, rows };\n };\n\n return { fetchPage, reconcile };\n}\n"],"mappings":"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","names":["body"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godxjp/server-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.7.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": {