@godxjp/server-sdk 0.7.0 → 0.8.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 +1 -1
- package/dist/sync.d.ts +13 -3
- package/dist/sync.js +8 -1
- package/dist/sync.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -179,7 +179,7 @@ Every export per subpath, checked against `src/*.ts`.
|
|
|
179
179
|
| `IngestResult` | `{ deliveries, response }` | Exported pair type for callers that want both halves. |
|
|
180
180
|
| `handleIngestRequest` | `(request: Request, {secret, process, expectedSource?}) => Promise<Response>` | The whole HTTP ingest endpoint — pure fetch API, one line to mount from any framework. |
|
|
181
181
|
| `SnapshotRow` / `SnapshotPage` / `ReconcileResult` | interfaces | Snapshot wire shapes and the pass summary (`sequence` watermark, pages, rows). |
|
|
182
|
-
| `createSnapshotClient` | `({issuer, serviceSlug, serviceSecret}) => { fetchPage(family, {cursor?, limit?}), reconcile(family, projector, {limit?}) }` | Keyset-paged `GET /api/sso/service/snapshot/{family}` to completion; per-status hints on failure (410 → restart the pass, 403 → `service_api_enabled`, …). |
|
|
182
|
+
| `createSnapshotClient` | `({issuer, serviceSlug, serviceSecret}) => { fetchPage(family, {cursor?, limit?, organizationId?, organizationSlug?}), reconcile(family, projector, {limit?, organizationId?, organizationSlug?}) }` | Keyset-paged `GET /api/sso/service/snapshot/{family}` to completion; per-status hints on failure (410 → restart the pass, 403 → `service_api_enabled`, …). |
|
|
183
183
|
|
|
184
184
|
### `./org`
|
|
185
185
|
|
package/dist/sync.d.ts
CHANGED
|
@@ -157,18 +157,28 @@ interface ReconcileResult {
|
|
|
157
157
|
* state (NOT a replay of history). Auth is the service credential; tenancy is
|
|
158
158
|
* the credential's, never a parameter.
|
|
159
159
|
*/
|
|
160
|
+
/**
|
|
161
|
+
* Which tenant a snapshot pass covers. Without either, Platform serves the service instance's
|
|
162
|
+
* OWNING organization — for a pool deployment only one of its tenants. Prefer `organizationId`
|
|
163
|
+
* (the console organization id: `console_organization_id` on every payload and the response's
|
|
164
|
+
* `organization_id`); `organizationSlug` is the directory API's spelling.
|
|
165
|
+
*/
|
|
166
|
+
interface SnapshotTenantOptions {
|
|
167
|
+
organizationId?: string;
|
|
168
|
+
organizationSlug?: string;
|
|
169
|
+
}
|
|
160
170
|
declare function createSnapshotClient(config: {
|
|
161
171
|
issuer: string;
|
|
162
172
|
serviceSlug: string;
|
|
163
173
|
serviceSecret: string;
|
|
164
174
|
}): {
|
|
165
|
-
fetchPage: (family: IdentityFamily, opts?: {
|
|
175
|
+
fetchPage: (family: IdentityFamily, opts?: SnapshotTenantOptions & {
|
|
166
176
|
cursor?: string;
|
|
167
177
|
limit?: number;
|
|
168
178
|
}) => Promise<SnapshotPage>;
|
|
169
|
-
reconcile: (family: IdentityFamily, projector: Projector, opts?: {
|
|
179
|
+
reconcile: (family: IdentityFamily, projector: Projector, opts?: SnapshotTenantOptions & {
|
|
170
180
|
limit?: number;
|
|
171
181
|
}) => Promise<ReconcileResult>;
|
|
172
182
|
};
|
|
173
183
|
|
|
174
|
-
export { D1_INBOX_DDL, type Delivery, type IdentityEnvelope, type IdentityFamily, type Inbox, type IngestResult, KNOWN_CONTRACT_VERSIONS, type Outcome, type ParsedSubject, type Projector, type ProjectorContext, type ReconcileResult, type SnapshotPage, type SnapshotRow, SyncContractError, type TenantPolicy, type Transport, acceptTenants, createD1Inbox, createProcessor, createSnapshotClient, handleIngestRequest, parseEnvelope, parseSubject, signWebhook, verifyWebhook };
|
|
184
|
+
export { D1_INBOX_DDL, type Delivery, type IdentityEnvelope, type IdentityFamily, type Inbox, type IngestResult, KNOWN_CONTRACT_VERSIONS, type Outcome, type ParsedSubject, type Projector, type ProjectorContext, type ReconcileResult, type SnapshotPage, type SnapshotRow, type SnapshotTenantOptions, SyncContractError, type TenantPolicy, type Transport, acceptTenants, createD1Inbox, createProcessor, createSnapshotClient, handleIngestRequest, parseEnvelope, parseSubject, signWebhook, verifyWebhook };
|
package/dist/sync.js
CHANGED
|
@@ -173,6 +173,8 @@ function createSnapshotClient(config) {
|
|
|
173
173
|
const url = new URL(`/api/sso/service/snapshot/${family}`, config.issuer);
|
|
174
174
|
if (opts?.cursor) url.searchParams.set("cursor", opts.cursor);
|
|
175
175
|
if (opts?.limit) url.searchParams.set("limit", String(opts.limit));
|
|
176
|
+
if (opts?.organizationId) url.searchParams.set("organization_id", opts.organizationId);
|
|
177
|
+
if (opts?.organizationSlug) url.searchParams.set("organization_slug", opts.organizationSlug);
|
|
176
178
|
const res = await fetch(url, {
|
|
177
179
|
headers: {
|
|
178
180
|
"X-Service-Slug": config.serviceSlug,
|
|
@@ -207,7 +209,12 @@ function createSnapshotClient(config) {
|
|
|
207
209
|
let sequence = 0;
|
|
208
210
|
let organizationId = "";
|
|
209
211
|
do {
|
|
210
|
-
const page = await fetchPage(family, {
|
|
212
|
+
const page = await fetchPage(family, {
|
|
213
|
+
cursor,
|
|
214
|
+
limit: opts?.limit,
|
|
215
|
+
organizationId: opts?.organizationId,
|
|
216
|
+
organizationSlug: opts?.organizationSlug
|
|
217
|
+
});
|
|
211
218
|
pages++;
|
|
212
219
|
sequence = Math.max(sequence, page.sequence);
|
|
213
220
|
organizationId = page.organizationId;
|
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 | \"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"]}
|
|
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 */\n/**\n * Which tenant a snapshot pass covers. Without either, Platform serves the service instance's\n * OWNING organization — for a pool deployment only one of its tenants. Prefer `organizationId`\n * (the console organization id: `console_organization_id` on every payload and the response's\n * `organization_id`); `organizationSlug` is the directory API's spelling.\n */\nexport interface SnapshotTenantOptions {\n organizationId?: string;\n organizationSlug?: string;\n}\n\nexport function createSnapshotClient(config: {\n issuer: string;\n serviceSlug: string;\n serviceSecret: string;\n}) {\n const fetchPage = async (\n family: IdentityFamily,\n opts?: SnapshotTenantOptions & { 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 // A pool consumer names the tenant (godx-jp/server-sdk#12): by console organization id —\n // the value every payload carries (godx-jp/id#2156) — or by slug (godx-jp/id#2007).\n // Platform refuses a pair that disagrees with 400, so passing both never picks silently.\n if (opts?.organizationId) url.searchParams.set(\"organization_id\", opts.organizationId);\n if (opts?.organizationSlug) url.searchParams.set(\"organization_slug\", opts.organizationSlug);\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?: SnapshotTenantOptions & { 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, {\n cursor,\n limit: opts?.limit,\n organizationId: opts?.organizationId,\n organizationSlug: opts?.organizationSlug,\n });\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.
|
|
3
|
+
"version": "0.8.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": {
|