@godxjp/server-sdk 0.1.0 → 0.2.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 +112 -6
- package/dist/org.d.ts +44 -1
- package/dist/org.js +4 -0
- package/dist/org.js.map +1 -1
- package/dist/producer.d.ts +109 -0
- package/dist/producer.js +79 -0
- package/dist/producer.js.map +1 -0
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -19,7 +19,8 @@ Naming is by **side**, not by language (`web-sdk` / `server-sdk` / `php-sdk`)
|
|
|
19
19
|
| `./http` | OIDC discovery (cached per isolate; endpoints are read, never hardcoded), `GodxSsoClient` (authorize URL, code exchange, refresh, revoke, userinfo), the server-to-server service API (`X-Service-Slug`/`X-Service-Secret`). |
|
|
20
20
|
| `./hono` | Thin adapter: `godxSso()` mounts `/auth/{login,callback,logout}`, `requireAuth`, `requireWritable` (read-only organizations may not write), `dockProxy` (the GoDX Dock's `apiBase` lane — no CORS, no token in the page). Every SSO failure lands back on the app as `/?sso_error=…&sso_error_description=…` with a human-readable description. |
|
|
21
21
|
| `./sync` | The identity feed, both consumer legs — see below. |
|
|
22
|
-
| `./org` | Typed client for the **write-back** surface (`/api/org/v1/*`): me, employees, departments, branches, teams (list / paginate-all / create). `Idempotency-Key` on every write (IETF draft), RFC 6750 `WWW-Authenticate` surfaced on 401/403, 422 field errors and 429 `Retry-After` exposed on `OrgApiError`. Tenancy is the token's — the client never sends an organization id. Since godx-jp/id#1960, a downstream **service-launch token carries the `organization-api` audience whenever org.\* scopes are granted** (i.e. survive RBAC narrowing) — see [`./org`](#org--the-write-back-client) below. |
|
|
22
|
+
| `./org` | Typed client for the **write-back** surface (`/api/org/v1/*`): me, employees, departments, branches, teams (list / paginate-all / create), and **notifications.create** (godx-jp/id#1962). `Idempotency-Key` on every write (IETF draft), RFC 6750 `WWW-Authenticate` surfaced on 401/403, 422 field errors and 429 `Retry-After` exposed on `OrgApiError`. Tenancy is the token's — the client never sends an organization id. Since godx-jp/id#1960, a downstream **service-launch token carries the `organization-api` audience whenever org.\* scopes are granted** (i.e. survive RBAC narrowing) — see [`./org`](#org--the-write-back-client) below. |
|
|
23
|
+
| `./producer` | The **outbox producer kit** (godx-jp/id#1963 phase 4): `OUTBOX_TABLE_SQL` (D1 DDL), `recordDomainEvent` (write the event next to the state change), `drainOutbox` (relay unpublished rows in sequence order through an injected `publish`). CloudEvents 1.0 envelope mirroring `./sync`'s shape — see [`./producer`](#producer--the-outbox-producer-kit) below. |
|
|
23
24
|
|
|
24
25
|
Core modules depend on `fetch` + WebCrypto only; `hono` is the only framework-shaped subpath.
|
|
25
26
|
|
|
@@ -167,7 +168,22 @@ and the scope→permission mapping.
|
|
|
167
168
|
| `PageMeta` / `Page<T>` | interfaces | Laravel-style page envelope (`data` + `meta`). |
|
|
168
169
|
| `OrgEmployee` / `OrgDepartment` / `OrgBranch` / `OrgTeam` / `OrgMe` | interfaces | Row shapes (open-ended: `[key: string]: unknown`). |
|
|
169
170
|
| `OrgApiError` | `class extends GodxHttpError { wwwAuthenticate, retryAfterSeconds, validationErrors }` | Every non-OK org/v1 answer, with the standard headers/bodies surfaced as fields. |
|
|
170
|
-
| `
|
|
171
|
+
| `NotificationCategory` | union type | `organization_invitation \| access_request \| service_provisioning \| billing \| security \| operator \| service` — the platform's category vocabulary, verbatim. |
|
|
172
|
+
| `LocalizedText` | `{ ja, en?, vi? }` | `ja` is the required base locale; title ≤200 chars, body ≤2000. |
|
|
173
|
+
| `CreateNotificationInput` | `{ recipients, category, source_ref, action_url, title, body }` | 1..100 recipients; `source_ref` ≤191 (also the per-instance dedup key); `action_url` ≤2000, must start with the service's base URL. |
|
|
174
|
+
| `CreateNotificationResult` | `{ created[], updated[], skipped[] }` | `created`/`updated` carry `{id, user_id}`; `skipped` carries `{user_id, reason}` (e.g. `preference_off`). |
|
|
175
|
+
| `createOrgApi` | `(config) => { me, employees, departments, branches, teams, notifications }` | The typed client; method table below. |
|
|
176
|
+
|
|
177
|
+
### `./producer`
|
|
178
|
+
|
|
179
|
+
| Export | Signature | One line |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `OUTBOX_TABLE_SQL` | `string` | DDL for the outbox table + the `(published_at, sequence)` drain index — copy into a migration verbatim (below). |
|
|
182
|
+
| `ProducerContractError` | `class extends Error` | Every contract refusal (bad subject token). |
|
|
183
|
+
| `DomainEnvelope` | interface | CloudEvents 1.0 envelope as `drainOutbox` hands it to `publish` — `./sync`'s shape, no `tenantid` (the tenant is the subject). |
|
|
184
|
+
| `DomainEventInput` / `RecordedDomainEvent` | interfaces | What `recordDomainEvent` takes / returns (`{id, sequence, subject, type}`). |
|
|
185
|
+
| `recordDomainEvent` | `(db, {service, organizationId, aggregate, action, data, id?}) => Promise<RecordedDomainEvent>` | Insert one outbox row next to the caller's own write; composes subject/type, validates every token. |
|
|
186
|
+
| `drainOutbox` | `(db, publish, {batch = 100}?) => Promise<{published, remaining}>` | Publish unpublished rows in sequence order; marks `published_at` only after `publish` resolves; stops at the first failure. |
|
|
171
187
|
|
|
172
188
|
## `./sync` — the identity feed
|
|
173
189
|
|
|
@@ -385,6 +401,28 @@ page 2.
|
|
|
385
401
|
| `departments.list(page?)` / `.all()` / `.create(...)` | `GET`/`POST /departments` | `org.departments:read` / `:write` |
|
|
386
402
|
| `branches.list(page?)` / `.all()` / `.create(...)` | `GET`/`POST /branches` | `org.branches:read` / `:write` |
|
|
387
403
|
| `teams.list(page?)` / `.all()` / `.create(...)` | `GET`/`POST /teams` | `org.teams:read` / `:write` |
|
|
404
|
+
| `notifications.create(input, {idempotencyKey?}?)` | `POST /notifications` | `org.notifications:write` |
|
|
405
|
+
|
|
406
|
+
`notifications.create` (godx-jp/id#1962) sends in-app notifications to organization members who
|
|
407
|
+
use this service — 1..100 `recipients` per call, every one of whom must belong to the token's
|
|
408
|
+
organization AND hold access to this service (422 otherwise). `action_url` must be absolute and
|
|
409
|
+
start with the service's base URL. `source_ref` doubles as the per-instance dedup key: sending the
|
|
410
|
+
same `source_ref` again lands in `updated` rather than duplicating. The platform answers **201
|
|
411
|
+
when anything was created, 200 otherwise**; the body is always
|
|
412
|
+
`{ data: { created, updated, skipped } }`, with each skip carrying its reason (e.g.
|
|
413
|
+
`preference_off` — the recipient turned the category off).
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
const { data } = await orgApi.notifications.create({
|
|
417
|
+
recipients: ["user-uuid-1", "user-uuid-2"],
|
|
418
|
+
category: "service",
|
|
419
|
+
source_ref: "task:42", // dedup key per service instance
|
|
420
|
+
action_url: "https://boilerplate.example/tasks/42", // must start with the service base URL
|
|
421
|
+
title: { ja: "承認依頼", en: "Approval requested" }, // ja required; en/vi optional
|
|
422
|
+
body: { ja: "タスク42の承認をお願いします。" },
|
|
423
|
+
});
|
|
424
|
+
// data.created / data.updated: {id, user_id}[] · data.skipped: {user_id, reason}[]
|
|
425
|
+
```
|
|
388
426
|
|
|
389
427
|
### Scope → permission mapping
|
|
390
428
|
|
|
@@ -401,6 +439,7 @@ the Console refuses):
|
|
|
401
439
|
| `org.departments:write` | `organization.departments.manage`, `settings.manage` |
|
|
402
440
|
| `org.branches:write` | `organization.branches.manage`, `settings.manage` |
|
|
403
441
|
| `org.teams:write` | `organization.teams.manage` |
|
|
442
|
+
| `org.notifications:write` | `organization.members.invite` |
|
|
404
443
|
|
|
405
444
|
### Handling `OrgApiError`
|
|
406
445
|
|
|
@@ -436,21 +475,88 @@ The error `message` already names the likely cause per status ("missing/expired,
|
|
|
436
475
|
minted for the organization-api audience", "lacks the route's org.* scope", …) so a bare log is
|
|
437
476
|
still actionable.
|
|
438
477
|
|
|
478
|
+
## `./producer` — the outbox producer kit
|
|
479
|
+
|
|
480
|
+
`./sync` is the CONSUMER half of the event discipline; this is the PRODUCER half (godx-jp/id#1963
|
|
481
|
+
phase 4). A service that publishes its own domain events writes them to a D1 **outbox** row next
|
|
482
|
+
to the state change they describe, and a relay drains the table in sequence order — so an event
|
|
483
|
+
can never outlive a rolled-back write, and never overtake an earlier one.
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
import { recordDomainEvent, drainOutbox } from "@godxjp/server-sdk/producer";
|
|
487
|
+
|
|
488
|
+
// In the request handler, next to your own write:
|
|
489
|
+
await recordDomainEvent(env.DB, {
|
|
490
|
+
service: "boilerplate", // every token: /^[A-Za-z0-9_-]+$/ — refused by name otherwise
|
|
491
|
+
organizationId: principal.organizationId,
|
|
492
|
+
aggregate: "task",
|
|
493
|
+
action: "completed",
|
|
494
|
+
data: { task_id: task.id }, // full snapshot, not a diff — same rule as the identity feed
|
|
495
|
+
});
|
|
496
|
+
// → subject godx.svc.boilerplate.{orgId}.task.completed · type jp.godx.boilerplate.task.completed
|
|
497
|
+
|
|
498
|
+
// In the relay (Cron Trigger, or a host-side loop):
|
|
499
|
+
const { published, remaining } = await drainOutbox(env.DB, async ({ subject, envelope }) => {
|
|
500
|
+
await myTransport.publish(subject, JSON.stringify(envelope)); // CloudEvents 1.0, ./sync's shape
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
The envelope is exactly `./sync`'s CloudEvents shape (`specversion`, `id`, `source`, `type`,
|
|
505
|
+
`subject`, `time`, `datacontenttype`, `sequence`, `data`) with **no `tenantid`** — the tenant is
|
|
506
|
+
the subject's `{organizationId}` token. `id` is the consumer's idempotency key, `sequence` the
|
|
507
|
+
per-producer ordering fence; `published_at` is marked only after `publish` resolves, so delivery
|
|
508
|
+
is at-least-once and consumers dedupe on `id` (which `./sync`'s `Inbox` already does).
|
|
509
|
+
|
|
510
|
+
**The transport is injected because a Worker cannot dial NATS over TCP.** Two honest shapes:
|
|
511
|
+
|
|
512
|
+
| Relay | Publish lag | How |
|
|
513
|
+
|---|---|---|
|
|
514
|
+
| Workers-resident (Cron Trigger) | **≥1 minute** — Cloudflare floors cron at 1 min. Do not promise consumers less from this shape | `"triggers": { "crons": ["* * * * *"] }` + `scheduled()` calling `drainOutbox` |
|
|
515
|
+
| Host-side loop (Node + any NATS client) | sub-second | poll `drainOutbox` in a long-lived process; same function, different clock |
|
|
516
|
+
|
|
517
|
+
**Sequence caveat, stated honestly**: `recordDomainEvent` computes `MAX(sequence) + 1` with a
|
|
518
|
+
read-then-insert, which is racy on paper. On D1 it is safe in practice because D1 serializes
|
|
519
|
+
writes per database — two invocations cannot interleave between the SELECT and the INSERT. On any
|
|
520
|
+
other SQLite with genuinely concurrent writers, run it inside the caller's transaction (where it
|
|
521
|
+
belongs anyway).
|
|
522
|
+
|
|
523
|
+
### Outbox DDL (copy-paste)
|
|
524
|
+
|
|
525
|
+
`OUTBOX_TABLE_SQL` exports exactly this — paste into a migration verbatim and keep any ORM-side
|
|
526
|
+
schema (e.g. Drizzle) in lockstep:
|
|
527
|
+
|
|
528
|
+
```sql
|
|
529
|
+
CREATE TABLE IF NOT EXISTS outbox (
|
|
530
|
+
id TEXT PRIMARY KEY,
|
|
531
|
+
sequence INTEGER NOT NULL,
|
|
532
|
+
subject TEXT NOT NULL,
|
|
533
|
+
type TEXT NOT NULL,
|
|
534
|
+
organization_id TEXT NOT NULL,
|
|
535
|
+
data TEXT NOT NULL,
|
|
536
|
+
created_at TEXT NOT NULL,
|
|
537
|
+
published_at TEXT NULL
|
|
538
|
+
);
|
|
539
|
+
CREATE INDEX IF NOT EXISTS outbox_unpublished_idx ON outbox (published_at, sequence);
|
|
540
|
+
```
|
|
541
|
+
|
|
439
542
|
## Tests
|
|
440
543
|
|
|
441
544
|
```bash
|
|
442
545
|
npx vitest run # from packages/server-sdk
|
|
443
546
|
```
|
|
444
547
|
|
|
445
|
-
|
|
548
|
+
40 tests, doubling as executable behaviour docs:
|
|
446
549
|
|
|
447
550
|
- `sync.test.ts` (23): envelope validation (specversion/subject/source pinning), webhook
|
|
448
551
|
sign/verify (tamper, replay, rotation, raw-secret fallback), processor outcomes (duplicate,
|
|
449
552
|
unknown family, tenant refused, retry-not-recorded), ingest HTTP mapping (single, batch, mixed
|
|
450
553
|
outcomes, batch-retry, 401/415/400/405), snapshot pagination/contract-stop/410.
|
|
451
|
-
- `org.test.ts` (
|
|
554
|
+
- `org.test.ts` (9): Bearer + Accept headers, Idempotency-Key (generated and caller-supplied),
|
|
452
555
|
pagination to the last page, 403 `WWW-Authenticate` surfaced, 401 audience hint, 422 per-field
|
|
453
|
-
validation, 429 `Retry-After
|
|
556
|
+
validation, 429 `Retry-After`, notifications.create mapping + Idempotency-Key.
|
|
557
|
+
- `producer.test.ts` (8): subject/type composition, token charset refusal naming the offender,
|
|
558
|
+
sequence monotonicity, drain order + envelope shape (no tenantid), stop-on-first-failure,
|
|
559
|
+
published_at only after publish resolves, batch limit, DDL ships the drain index.
|
|
454
560
|
|
|
455
561
|
The php-sdk mirrors the sync suite in `tests/Sync/Http/`.
|
|
456
562
|
|
|
@@ -483,4 +589,4 @@ Short version — the boilerplate's root `README.md` has the full symptom → ca
|
|
|
483
589
|
- [ ] `"private": true` → off; add `files`, `license`, `repository`; version from `0.1.0`.
|
|
484
590
|
- [ ] Move `hono` to `peerDependencies` review (already `>=4`) and keep `jose` as the only
|
|
485
591
|
runtime dependency.
|
|
486
|
-
- [ ] CI: `vitest run` (
|
|
592
|
+
- [ ] CI: `vitest run` (40) + `tsc --noEmit` + a pack-and-import smoke test on Workers and Node.
|
package/dist/org.d.ts
CHANGED
|
@@ -76,6 +76,41 @@ interface OrgTeam {
|
|
|
76
76
|
interface OrgMe {
|
|
77
77
|
[key: string]: unknown;
|
|
78
78
|
}
|
|
79
|
+
/** Notification categories — the platform's `NotificationCenter::CATEGORIES`, verbatim. */
|
|
80
|
+
type NotificationCategory = "organization_invitation" | "access_request" | "service_provisioning" | "billing" | "security" | "operator" | "service";
|
|
81
|
+
/** `ja` is the required base locale; `en`/`vi` optional. Title ≤200 chars, body ≤2000. */
|
|
82
|
+
interface LocalizedText {
|
|
83
|
+
ja: string;
|
|
84
|
+
en?: string;
|
|
85
|
+
vi?: string;
|
|
86
|
+
}
|
|
87
|
+
interface CreateNotificationInput {
|
|
88
|
+
/** Console user ids — 1..100 per call. Every recipient must belong to the org AND hold access to this service. */
|
|
89
|
+
recipients: string[];
|
|
90
|
+
category: NotificationCategory;
|
|
91
|
+
/** Producer-side reference (≤191) — also the platform's per-instance dedup key: same source_ref updates instead of duplicating. */
|
|
92
|
+
source_ref: string;
|
|
93
|
+
/** Absolute URL (≤2000) that must start with the service's base URL — the platform refuses anything else. */
|
|
94
|
+
action_url: string;
|
|
95
|
+
title: LocalizedText;
|
|
96
|
+
body: LocalizedText;
|
|
97
|
+
}
|
|
98
|
+
/** The platform answers 201 when anything was created, 200 otherwise (updated/skipped only). */
|
|
99
|
+
interface CreateNotificationResult {
|
|
100
|
+
created: {
|
|
101
|
+
id: string;
|
|
102
|
+
user_id: string;
|
|
103
|
+
}[];
|
|
104
|
+
updated: {
|
|
105
|
+
id: string;
|
|
106
|
+
user_id: string;
|
|
107
|
+
}[];
|
|
108
|
+
/** e.g. reason "preference_off" — the recipient turned this category off. */
|
|
109
|
+
skipped: {
|
|
110
|
+
user_id: string;
|
|
111
|
+
reason: string;
|
|
112
|
+
}[];
|
|
113
|
+
}
|
|
79
114
|
declare class OrgApiError extends GodxHttpError {
|
|
80
115
|
/** RFC 6750 challenge, when the platform sent one (401/403). */
|
|
81
116
|
readonly wwwAuthenticate: string | null;
|
|
@@ -121,6 +156,14 @@ declare function createOrgApi(config: OrgApiConfig): {
|
|
|
121
156
|
data: OrgTeam;
|
|
122
157
|
}>;
|
|
123
158
|
};
|
|
159
|
+
notifications: {
|
|
160
|
+
/** scope org.notifications:write — send in-app notifications to members who use this service (godx-jp/id#1962). */
|
|
161
|
+
create: (input: CreateNotificationInput, options?: {
|
|
162
|
+
idempotencyKey?: string;
|
|
163
|
+
}) => Promise<{
|
|
164
|
+
data: CreateNotificationResult;
|
|
165
|
+
}>;
|
|
166
|
+
};
|
|
124
167
|
};
|
|
125
168
|
|
|
126
|
-
export { type OrgApiConfig, OrgApiError, type OrgBranch, type OrgDepartment, type OrgEmployee, type OrgMe, type OrgTeam, type Page, type PageMeta, createOrgApi };
|
|
169
|
+
export { type CreateNotificationInput, type CreateNotificationResult, type LocalizedText, type NotificationCategory, type OrgApiConfig, OrgApiError, type OrgBranch, type OrgDepartment, type OrgEmployee, type OrgMe, type OrgTeam, type Page, type PageMeta, createOrgApi };
|
package/dist/org.js
CHANGED
|
@@ -91,6 +91,10 @@ function createOrgApi(config) {
|
|
|
91
91
|
list: (page) => get("/teams", page ? { page: String(page) } : void 0),
|
|
92
92
|
all: () => paginate("/teams"),
|
|
93
93
|
create: (payload, idempotencyKey) => post("/teams", payload, idempotencyKey)
|
|
94
|
+
},
|
|
95
|
+
notifications: {
|
|
96
|
+
/** scope org.notifications:write — send in-app notifications to members who use this service (godx-jp/id#1962). */
|
|
97
|
+
create: (input, options) => post("/notifications", { ...input }, options?.idempotencyKey)
|
|
94
98
|
}
|
|
95
99
|
};
|
|
96
100
|
}
|
package/dist/org.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/org.ts"],"sourcesContent":["/**\n * ./org — typed client for the Delegated Organization API (`/api/org/v1/*`),\n * the platform's write-back surface: the ONLY sanctioned way a service changes\n * platform-owned directory data. The identity feed then carries the change to\n * every consumer (the feed itself is one-way; see ./sync).\n *\n * Auth model, exactly as the platform enforces it (routes/api/org.php):\n * - Bearer access token minted FOR the `organization-api` recipient resource\n * (`aud` contains `{issuer}/api/org/v1`) by a `user`-lane principal.\n * - Tenancy is the token's organization, re-derived server-side per request —\n * this client never sends an organization id.\n * - Each route needs its `org.{resource}:{read|write}` scope; a missing scope\n * answers 403 `insufficient_scope` with a WWW-Authenticate header (RFC 6750),\n * which this client surfaces verbatim.\n *\n * NOTE (2026-09): the platform grants that audience on the MINI-APP lane only\n * (`TokenIssuer::miniAppResourceAudiences`). A downstream service-launch token\n * does not carry it yet — godx-jp/id has the proposal to widen that lane.\n *\n * International-standard behaviours built in:\n * - `Idempotency-Key` (IETF draft-ietf-httpapi-idempotency-key-header) is sent\n * on every write — generated when not supplied. The org/v1 surface does not\n * *honor* it yet (the admin surface does); sending it is forward-compatible\n * and costs nothing.\n * - 401/403 surface the `WWW-Authenticate` challenge; 429 surfaces\n * `Retry-After`; validation errors (422) surface the per-field messages.\n * - Page-based pagination is wrapped in an async iterator so callers cannot\n * forget page 2.\n */\nimport { GodxHttpError } from \"./http\";\n\nexport interface OrgApiConfig {\n issuer: string;\n /** Bearer access token whose aud includes {issuer}/api/org/v1. */\n accessToken: string;\n}\n\nexport interface PageMeta {\n current_page: number;\n last_page: number;\n per_page: number;\n total: number;\n}\n\nexport interface Page<T> {\n data: T[];\n meta: PageMeta;\n}\n\nexport interface OrgEmployee {\n id: string;\n employee_code: string;\n full_name: string;\n email?: string | null;\n department_id?: string | null;\n branch_id?: string | null;\n user_id?: string | null;\n [key: string]: unknown;\n}\n\nexport interface OrgDepartment {\n id: string;\n code?: string | null;\n name: string;\n [key: string]: unknown;\n}\n\nexport interface OrgBranch {\n id: string;\n code?: string | null;\n name: string;\n [key: string]: unknown;\n}\n\nexport interface OrgTeam {\n id: string;\n name: string;\n [key: string]: unknown;\n}\n\nexport interface OrgMe {\n [key: string]: unknown;\n}\n\nexport class OrgApiError extends GodxHttpError {\n /** RFC 6750 challenge, when the platform sent one (401/403). */\n readonly wwwAuthenticate: string | null;\n /** Seconds to wait, when the platform answered 429 with Retry-After. */\n readonly retryAfterSeconds: number | null;\n /** Laravel validation messages per field, when the answer was 422. */\n readonly validationErrors: Record<string, string[]> | null;\n\n constructor(status: number, body: string, message: string, headers: Headers) {\n super(status, body, message);\n this.name = \"OrgApiError\";\n this.wwwAuthenticate = headers.get(\"WWW-Authenticate\");\n const retryAfter = headers.get(\"Retry-After\");\n this.retryAfterSeconds = retryAfter !== null && /^\\d+$/.test(retryAfter) ? Number(retryAfter) : null;\n let validation: Record<string, string[]> | null = null;\n try {\n const parsed = JSON.parse(body) as { errors?: Record<string, string[]> };\n if (parsed && typeof parsed === \"object\" && parsed.errors) validation = parsed.errors;\n } catch {\n /* body was not JSON — nothing more to surface */\n }\n this.validationErrors = validation;\n }\n}\n\nasync function unwrap<T>(response: Response, what: string): Promise<T> {\n if (!response.ok) {\n const body = await response.text();\n const challenge = response.headers.get(\"WWW-Authenticate\");\n const hint =\n response.status === 401\n ? `the token is missing, expired, or was not minted for the organization-api audience${challenge ? ` — ${challenge}` : \"\"}`\n : response.status === 403\n ? `the token lacks the route's org.* scope${challenge ? ` — ${challenge}` : \"\"}`\n : response.status === 422\n ? \"validation refused the payload (see validationErrors)\"\n : response.status === 429\n ? \"rate limited — honor retryAfterSeconds\"\n : \"see the response body\";\n throw new OrgApiError(response.status, body, `${what} answered ${response.status}: ${hint}`, response.headers);\n }\n return (await response.json()) as T;\n}\n\nexport function createOrgApi(config: OrgApiConfig) {\n const base = config.issuer.replace(/\\/$/, \"\") + \"/api/org/v1\";\n\n const get = async <T>(path: string, query?: Record<string, string>): Promise<T> => {\n const url = new URL(base + path);\n if (query) url.search = new URLSearchParams(query).toString();\n const response = await fetch(url, {\n headers: { Authorization: `Bearer ${config.accessToken}`, Accept: \"application/json\" },\n });\n return unwrap<T>(response, `GET ${path}`);\n };\n\n const post = async <T>(path: string, payload: Record<string, unknown>, idempotencyKey?: string): Promise<T> => {\n const response = await fetch(base + path, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${config.accessToken}`,\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n \"Idempotency-Key\": idempotencyKey ?? crypto.randomUUID(),\n },\n body: JSON.stringify(payload),\n });\n return unwrap<T>(response, `POST ${path}`);\n };\n\n /** Follow page-based pagination to the end, yielding every row once. */\n async function* paginate<T>(path: string): AsyncGenerator<T, void, void> {\n let page = 1;\n for (;;) {\n const result = await get<Page<T>>(path, { page: String(page) });\n for (const row of result.data) yield row;\n if (result.meta.current_page >= result.meta.last_page) return;\n page = result.meta.current_page + 1;\n }\n }\n\n return {\n /** GET /me — scope org:profile. */\n me: () => get<OrgMe>(\"/me\"),\n\n employees: {\n list: (page?: number) => get<Page<OrgEmployee>>(\"/employees\", page ? { page: String(page) } : undefined),\n all: () => paginate<OrgEmployee>(\"/employees\"),\n /** scope org.employees:write */\n create: (payload: Record<string, unknown>, idempotencyKey?: string) =>\n post<{ data: OrgEmployee }>(\"/employees\", payload, idempotencyKey),\n /** Who this user reports to — rides org.employees:read. */\n managerOf: (consoleUserId: string) => get<{ data: unknown }>(`/users/${encodeURIComponent(consoleUserId)}/manager`),\n },\n\n departments: {\n list: (page?: number) => get<Page<OrgDepartment>>(\"/departments\", page ? { page: String(page) } : undefined),\n all: () => paginate<OrgDepartment>(\"/departments\"),\n create: (payload: Record<string, unknown>, idempotencyKey?: string) =>\n post<{ data: OrgDepartment }>(\"/departments\", payload, idempotencyKey),\n },\n\n branches: {\n list: (page?: number) => get<Page<OrgBranch>>(\"/branches\", page ? { page: String(page) } : undefined),\n all: () => paginate<OrgBranch>(\"/branches\"),\n create: (payload: Record<string, unknown>, idempotencyKey?: string) =>\n post<{ data: OrgBranch }>(\"/branches\", payload, idempotencyKey),\n },\n\n teams: {\n list: (page?: number) => get<Page<OrgTeam>>(\"/teams\", page ? { page: String(page) } : undefined),\n all: () => paginate<OrgTeam>(\"/teams\"),\n create: (payload: Record<string, unknown>, idempotencyKey?: string) =>\n post<{ data: OrgTeam }>(\"/teams\", payload, idempotencyKey),\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/org.ts"],"sourcesContent":["/**\n * ./org — typed client for the Delegated Organization API (`/api/org/v1/*`),\n * the platform's write-back surface: the ONLY sanctioned way a service changes\n * platform-owned directory data. The identity feed then carries the change to\n * every consumer (the feed itself is one-way; see ./sync).\n *\n * Auth model, exactly as the platform enforces it (routes/api/org.php):\n * - Bearer access token minted FOR the `organization-api` recipient resource\n * (`aud` contains `{issuer}/api/org/v1`) by a `user`-lane principal.\n * - Tenancy is the token's organization, re-derived server-side per request —\n * this client never sends an organization id.\n * - Each route needs its `org.{resource}:{read|write}` scope; a missing scope\n * answers 403 `insufficient_scope` with a WWW-Authenticate header (RFC 6750),\n * which this client surfaces verbatim.\n *\n * NOTE (2026-09): the platform grants that audience on the MINI-APP lane only\n * (`TokenIssuer::miniAppResourceAudiences`). A downstream service-launch token\n * does not carry it yet — godx-jp/id has the proposal to widen that lane.\n *\n * International-standard behaviours built in:\n * - `Idempotency-Key` (IETF draft-ietf-httpapi-idempotency-key-header) is sent\n * on every write — generated when not supplied. The org/v1 surface does not\n * *honor* it yet (the admin surface does); sending it is forward-compatible\n * and costs nothing.\n * - 401/403 surface the `WWW-Authenticate` challenge; 429 surfaces\n * `Retry-After`; validation errors (422) surface the per-field messages.\n * - Page-based pagination is wrapped in an async iterator so callers cannot\n * forget page 2.\n */\nimport { GodxHttpError } from \"./http\";\n\nexport interface OrgApiConfig {\n issuer: string;\n /** Bearer access token whose aud includes {issuer}/api/org/v1. */\n accessToken: string;\n}\n\nexport interface PageMeta {\n current_page: number;\n last_page: number;\n per_page: number;\n total: number;\n}\n\nexport interface Page<T> {\n data: T[];\n meta: PageMeta;\n}\n\nexport interface OrgEmployee {\n id: string;\n employee_code: string;\n full_name: string;\n email?: string | null;\n department_id?: string | null;\n branch_id?: string | null;\n user_id?: string | null;\n [key: string]: unknown;\n}\n\nexport interface OrgDepartment {\n id: string;\n code?: string | null;\n name: string;\n [key: string]: unknown;\n}\n\nexport interface OrgBranch {\n id: string;\n code?: string | null;\n name: string;\n [key: string]: unknown;\n}\n\nexport interface OrgTeam {\n id: string;\n name: string;\n [key: string]: unknown;\n}\n\nexport interface OrgMe {\n [key: string]: unknown;\n}\n\n/** Notification categories — the platform's `NotificationCenter::CATEGORIES`, verbatim. */\nexport type NotificationCategory =\n | \"organization_invitation\"\n | \"access_request\"\n | \"service_provisioning\"\n | \"billing\"\n | \"security\"\n | \"operator\"\n | \"service\";\n\n/** `ja` is the required base locale; `en`/`vi` optional. Title ≤200 chars, body ≤2000. */\nexport interface LocalizedText {\n ja: string;\n en?: string;\n vi?: string;\n}\n\nexport interface CreateNotificationInput {\n /** Console user ids — 1..100 per call. Every recipient must belong to the org AND hold access to this service. */\n recipients: string[];\n category: NotificationCategory;\n /** Producer-side reference (≤191) — also the platform's per-instance dedup key: same source_ref updates instead of duplicating. */\n source_ref: string;\n /** Absolute URL (≤2000) that must start with the service's base URL — the platform refuses anything else. */\n action_url: string;\n title: LocalizedText;\n body: LocalizedText;\n}\n\n/** The platform answers 201 when anything was created, 200 otherwise (updated/skipped only). */\nexport interface CreateNotificationResult {\n created: { id: string; user_id: string }[];\n updated: { id: string; user_id: string }[];\n /** e.g. reason \"preference_off\" — the recipient turned this category off. */\n skipped: { user_id: string; reason: string }[];\n}\n\nexport class OrgApiError extends GodxHttpError {\n /** RFC 6750 challenge, when the platform sent one (401/403). */\n readonly wwwAuthenticate: string | null;\n /** Seconds to wait, when the platform answered 429 with Retry-After. */\n readonly retryAfterSeconds: number | null;\n /** Laravel validation messages per field, when the answer was 422. */\n readonly validationErrors: Record<string, string[]> | null;\n\n constructor(status: number, body: string, message: string, headers: Headers) {\n super(status, body, message);\n this.name = \"OrgApiError\";\n this.wwwAuthenticate = headers.get(\"WWW-Authenticate\");\n const retryAfter = headers.get(\"Retry-After\");\n this.retryAfterSeconds = retryAfter !== null && /^\\d+$/.test(retryAfter) ? Number(retryAfter) : null;\n let validation: Record<string, string[]> | null = null;\n try {\n const parsed = JSON.parse(body) as { errors?: Record<string, string[]> };\n if (parsed && typeof parsed === \"object\" && parsed.errors) validation = parsed.errors;\n } catch {\n /* body was not JSON — nothing more to surface */\n }\n this.validationErrors = validation;\n }\n}\n\nasync function unwrap<T>(response: Response, what: string): Promise<T> {\n if (!response.ok) {\n const body = await response.text();\n const challenge = response.headers.get(\"WWW-Authenticate\");\n const hint =\n response.status === 401\n ? `the token is missing, expired, or was not minted for the organization-api audience${challenge ? ` — ${challenge}` : \"\"}`\n : response.status === 403\n ? `the token lacks the route's org.* scope${challenge ? ` — ${challenge}` : \"\"}`\n : response.status === 422\n ? \"validation refused the payload (see validationErrors)\"\n : response.status === 429\n ? \"rate limited — honor retryAfterSeconds\"\n : \"see the response body\";\n throw new OrgApiError(response.status, body, `${what} answered ${response.status}: ${hint}`, response.headers);\n }\n return (await response.json()) as T;\n}\n\nexport function createOrgApi(config: OrgApiConfig) {\n const base = config.issuer.replace(/\\/$/, \"\") + \"/api/org/v1\";\n\n const get = async <T>(path: string, query?: Record<string, string>): Promise<T> => {\n const url = new URL(base + path);\n if (query) url.search = new URLSearchParams(query).toString();\n const response = await fetch(url, {\n headers: { Authorization: `Bearer ${config.accessToken}`, Accept: \"application/json\" },\n });\n return unwrap<T>(response, `GET ${path}`);\n };\n\n const post = async <T>(path: string, payload: Record<string, unknown>, idempotencyKey?: string): Promise<T> => {\n const response = await fetch(base + path, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${config.accessToken}`,\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n \"Idempotency-Key\": idempotencyKey ?? crypto.randomUUID(),\n },\n body: JSON.stringify(payload),\n });\n return unwrap<T>(response, `POST ${path}`);\n };\n\n /** Follow page-based pagination to the end, yielding every row once. */\n async function* paginate<T>(path: string): AsyncGenerator<T, void, void> {\n let page = 1;\n for (;;) {\n const result = await get<Page<T>>(path, { page: String(page) });\n for (const row of result.data) yield row;\n if (result.meta.current_page >= result.meta.last_page) return;\n page = result.meta.current_page + 1;\n }\n }\n\n return {\n /** GET /me — scope org:profile. */\n me: () => get<OrgMe>(\"/me\"),\n\n employees: {\n list: (page?: number) => get<Page<OrgEmployee>>(\"/employees\", page ? { page: String(page) } : undefined),\n all: () => paginate<OrgEmployee>(\"/employees\"),\n /** scope org.employees:write */\n create: (payload: Record<string, unknown>, idempotencyKey?: string) =>\n post<{ data: OrgEmployee }>(\"/employees\", payload, idempotencyKey),\n /** Who this user reports to — rides org.employees:read. */\n managerOf: (consoleUserId: string) => get<{ data: unknown }>(`/users/${encodeURIComponent(consoleUserId)}/manager`),\n },\n\n departments: {\n list: (page?: number) => get<Page<OrgDepartment>>(\"/departments\", page ? { page: String(page) } : undefined),\n all: () => paginate<OrgDepartment>(\"/departments\"),\n create: (payload: Record<string, unknown>, idempotencyKey?: string) =>\n post<{ data: OrgDepartment }>(\"/departments\", payload, idempotencyKey),\n },\n\n branches: {\n list: (page?: number) => get<Page<OrgBranch>>(\"/branches\", page ? { page: String(page) } : undefined),\n all: () => paginate<OrgBranch>(\"/branches\"),\n create: (payload: Record<string, unknown>, idempotencyKey?: string) =>\n post<{ data: OrgBranch }>(\"/branches\", payload, idempotencyKey),\n },\n\n teams: {\n list: (page?: number) => get<Page<OrgTeam>>(\"/teams\", page ? { page: String(page) } : undefined),\n all: () => paginate<OrgTeam>(\"/teams\"),\n create: (payload: Record<string, unknown>, idempotencyKey?: string) =>\n post<{ data: OrgTeam }>(\"/teams\", payload, idempotencyKey),\n },\n\n notifications: {\n /** scope org.notifications:write — send in-app notifications to members who use this service (godx-jp/id#1962). */\n create: (input: CreateNotificationInput, options?: { idempotencyKey?: string }) =>\n post<{ data: CreateNotificationResult }>(\"/notifications\", { ...input }, options?.idempotencyKey),\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ./producer — the outbox half of the event discipline (godx-jp/id#1963,
|
|
3
|
+
* phase 4): a service that PUBLISHES its own domain events writes them to a
|
|
4
|
+
* D1 outbox row alongside its state change, and a relay drains the table in
|
|
5
|
+
* sequence order. The envelope mirrors ./sync's CloudEvents 1.0 shape so
|
|
6
|
+
* consumers reuse one parser; there is NO tenantid attribute — the tenant
|
|
7
|
+
* rides in the subject, exactly like the identity feed.
|
|
8
|
+
*
|
|
9
|
+
* The transport is INJECTED (the `publish` callback) because a Cloudflare
|
|
10
|
+
* Worker cannot dial NATS over TCP. Two deployment shapes, stated honestly:
|
|
11
|
+
*
|
|
12
|
+
* - **Workers-resident relay**: a Cron Trigger calling `drainOutbox`.
|
|
13
|
+
* Cloudflare floors cron at 1 minute, so publish lag is ≥1 minute.
|
|
14
|
+
* Do not promise consumers anything faster from this shape.
|
|
15
|
+
* - **Host-side loop**: any long-lived process with a NATS client (nats.js on
|
|
16
|
+
* Node, or the platform's own relay) polling `drainOutbox` gets sub-second
|
|
17
|
+
* lag. Same function, different clock.
|
|
18
|
+
*/
|
|
19
|
+
/** Structural subset of D1Database, so this module has no CF type dependency. */
|
|
20
|
+
interface D1Like {
|
|
21
|
+
prepare(sql: string): {
|
|
22
|
+
bind(...values: unknown[]): {
|
|
23
|
+
first<T = unknown>(): Promise<T | null>;
|
|
24
|
+
run(): Promise<unknown>;
|
|
25
|
+
all<T = unknown>(): Promise<{
|
|
26
|
+
results: T[];
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
declare class ProducerContractError extends Error {
|
|
32
|
+
constructor(message: string);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* DDL for the outbox table — copy into a migration verbatim (drizzle users:
|
|
36
|
+
* keep the ORM schema in lockstep). The index serves `drainOutbox`'s
|
|
37
|
+
* "unpublished, in sequence order" read.
|
|
38
|
+
*/
|
|
39
|
+
declare const OUTBOX_TABLE_SQL = "CREATE TABLE IF NOT EXISTS outbox (\n id TEXT PRIMARY KEY,\n sequence INTEGER NOT NULL,\n subject TEXT NOT NULL,\n type TEXT NOT NULL,\n organization_id TEXT NOT NULL,\n data TEXT NOT NULL,\n created_at TEXT NOT NULL,\n published_at TEXT NULL\n);\nCREATE INDEX IF NOT EXISTS outbox_unpublished_idx ON outbox (published_at, sequence);";
|
|
40
|
+
/**
|
|
41
|
+
* CloudEvents 1.0 envelope as `drainOutbox` hands it to `publish` — the same
|
|
42
|
+
* shape ./sync's IdentityEnvelope reads, and deliberately WITHOUT a tenantid
|
|
43
|
+
* attribute: the tenant is the subject's `{organizationId}` token.
|
|
44
|
+
*/
|
|
45
|
+
interface DomainEnvelope {
|
|
46
|
+
specversion: "1.0";
|
|
47
|
+
/** UUIDv4 — THE idempotency key for consumers. */
|
|
48
|
+
id: string;
|
|
49
|
+
/** Producer identity: `godx.svc.{service}` (the subject's first three tokens). */
|
|
50
|
+
source: string;
|
|
51
|
+
/** jp.godx.{service}.{aggregate}.{action} */
|
|
52
|
+
type: string;
|
|
53
|
+
/** godx.svc.{service}.{organizationId}.{aggregate}.{action} */
|
|
54
|
+
subject: string;
|
|
55
|
+
time: string;
|
|
56
|
+
datacontenttype: "application/json";
|
|
57
|
+
/** Per-producer monotonic ordering fence (outbox.sequence). */
|
|
58
|
+
sequence: number;
|
|
59
|
+
data: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
interface DomainEventInput {
|
|
62
|
+
service: string;
|
|
63
|
+
organizationId: string;
|
|
64
|
+
aggregate: string;
|
|
65
|
+
action: string;
|
|
66
|
+
data: Record<string, unknown>;
|
|
67
|
+
/** CloudEvents id — generated (crypto.randomUUID) when not supplied. */
|
|
68
|
+
id?: string;
|
|
69
|
+
}
|
|
70
|
+
interface RecordedDomainEvent {
|
|
71
|
+
id: string;
|
|
72
|
+
sequence: number;
|
|
73
|
+
subject: string;
|
|
74
|
+
type: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Record one domain event in the outbox, next to the state change it
|
|
78
|
+
* describes. Composes:
|
|
79
|
+
* - subject = `godx.svc.{service}.{organizationId}.{aggregate}.{action}`
|
|
80
|
+
* - type = `jp.godx.{service}.{aggregate}.{action}`
|
|
81
|
+
*
|
|
82
|
+
* Sequence is per-producer monotonic via `MAX(sequence) + 1`. That
|
|
83
|
+
* read-then-insert is racy on paper; on D1 it is safe in practice because
|
|
84
|
+
* D1 serializes writes per database — two invocations cannot interleave
|
|
85
|
+
* between the SELECT and the INSERT. On any other SQLite with genuinely
|
|
86
|
+
* concurrent writers, run this inside the caller's transaction (which is
|
|
87
|
+
* where it belongs anyway: the event must not outlive a rolled-back write).
|
|
88
|
+
*/
|
|
89
|
+
declare function recordDomainEvent(db: D1Like, input: DomainEventInput): Promise<RecordedDomainEvent>;
|
|
90
|
+
/**
|
|
91
|
+
* Publish unpublished rows in sequence order through the injected transport.
|
|
92
|
+
* `published_at` is marked only AFTER `publish` resolves — a crash between
|
|
93
|
+
* the two re-publishes the row, so consumers dedupe on the CloudEvents `id`
|
|
94
|
+
* (at-least-once, exactly like the identity feed). The drain STOPS at the
|
|
95
|
+
* first failure so no later event overtakes an earlier one; the failed row
|
|
96
|
+
* stays unpublished and the next drain retries it, so inspect `remaining`
|
|
97
|
+
* rather than expecting a throw.
|
|
98
|
+
*/
|
|
99
|
+
declare function drainOutbox(db: D1Like, publish: (event: {
|
|
100
|
+
subject: string;
|
|
101
|
+
envelope: DomainEnvelope;
|
|
102
|
+
}) => Promise<void>, { batch }?: {
|
|
103
|
+
batch?: number;
|
|
104
|
+
}): Promise<{
|
|
105
|
+
published: number;
|
|
106
|
+
remaining: number;
|
|
107
|
+
}>;
|
|
108
|
+
|
|
109
|
+
export { type DomainEnvelope, type DomainEventInput, OUTBOX_TABLE_SQL, ProducerContractError, type RecordedDomainEvent, drainOutbox, recordDomainEvent };
|
package/dist/producer.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/producer.ts
|
|
2
|
+
var ProducerContractError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "ProducerContractError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var OUTBOX_TABLE_SQL = `CREATE TABLE IF NOT EXISTS outbox (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
sequence INTEGER NOT NULL,
|
|
11
|
+
subject TEXT NOT NULL,
|
|
12
|
+
type TEXT NOT NULL,
|
|
13
|
+
organization_id TEXT NOT NULL,
|
|
14
|
+
data TEXT NOT NULL,
|
|
15
|
+
created_at TEXT NOT NULL,
|
|
16
|
+
published_at TEXT NULL
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS outbox_unpublished_idx ON outbox (published_at, sequence);`;
|
|
19
|
+
var TOKEN = /^[A-Za-z0-9_-]+$/;
|
|
20
|
+
function requireToken(name, value) {
|
|
21
|
+
if (!TOKEN.test(value)) {
|
|
22
|
+
throw new ProducerContractError(
|
|
23
|
+
`${name} ${JSON.stringify(value)} is not a valid subject token \u2014 expected /^[A-Za-z0-9_-]+$/ ("." separates tokens and may not appear inside one).`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
async function recordDomainEvent(db, input) {
|
|
29
|
+
const service = requireToken("service", input.service);
|
|
30
|
+
const organizationId = requireToken("organizationId", input.organizationId);
|
|
31
|
+
const aggregate = requireToken("aggregate", input.aggregate);
|
|
32
|
+
const action = requireToken("action", input.action);
|
|
33
|
+
const subject = `godx.svc.${service}.${organizationId}.${aggregate}.${action}`;
|
|
34
|
+
const type = `jp.godx.${service}.${aggregate}.${action}`;
|
|
35
|
+
const id = input.id ?? crypto.randomUUID();
|
|
36
|
+
const next = await db.prepare("SELECT COALESCE(MAX(sequence), 0) + 1 AS next FROM outbox").bind().first();
|
|
37
|
+
const sequence = Number(next?.next ?? 1);
|
|
38
|
+
await db.prepare(
|
|
39
|
+
"INSERT INTO outbox (id, sequence, subject, type, organization_id, data, created_at, published_at) VALUES (?, ?, ?, ?, ?, ?, ?, NULL)"
|
|
40
|
+
).bind(id, sequence, subject, type, organizationId, JSON.stringify(input.data), (/* @__PURE__ */ new Date()).toISOString()).run();
|
|
41
|
+
return { id, sequence, subject, type };
|
|
42
|
+
}
|
|
43
|
+
async function drainOutbox(db, publish, { batch = 100 } = {}) {
|
|
44
|
+
const { results } = await db.prepare(
|
|
45
|
+
"SELECT id, sequence, subject, type, data, created_at FROM outbox WHERE published_at IS NULL ORDER BY sequence LIMIT ?"
|
|
46
|
+
).bind(batch).all();
|
|
47
|
+
let published = 0;
|
|
48
|
+
for (const row of results) {
|
|
49
|
+
const envelope = {
|
|
50
|
+
specversion: "1.0",
|
|
51
|
+
id: row.id,
|
|
52
|
+
// Tokens cannot contain "." (validated on record), so the first three
|
|
53
|
+
// subject tokens are exactly `godx.svc.{service}` — the producer.
|
|
54
|
+
source: row.subject.split(".").slice(0, 3).join("."),
|
|
55
|
+
type: row.type,
|
|
56
|
+
subject: row.subject,
|
|
57
|
+
time: row.created_at,
|
|
58
|
+
datacontenttype: "application/json",
|
|
59
|
+
sequence: row.sequence,
|
|
60
|
+
data: JSON.parse(row.data)
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
await publish({ subject: row.subject, envelope });
|
|
64
|
+
} catch {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
await db.prepare("UPDATE outbox SET published_at = ? WHERE id = ?").bind((/* @__PURE__ */ new Date()).toISOString(), row.id).run();
|
|
68
|
+
published++;
|
|
69
|
+
}
|
|
70
|
+
const count = await db.prepare("SELECT COUNT(*) AS n FROM outbox WHERE published_at IS NULL").bind().first();
|
|
71
|
+
return { published, remaining: Number(count?.n ?? 0) };
|
|
72
|
+
}
|
|
73
|
+
export {
|
|
74
|
+
OUTBOX_TABLE_SQL,
|
|
75
|
+
ProducerContractError,
|
|
76
|
+
drainOutbox,
|
|
77
|
+
recordDomainEvent
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=producer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/producer.ts"],"sourcesContent":["/**\n * ./producer — the outbox half of the event discipline (godx-jp/id#1963,\n * phase 4): a service that PUBLISHES its own domain events writes them to a\n * D1 outbox row alongside its state change, and a relay drains the table in\n * sequence order. The envelope mirrors ./sync's CloudEvents 1.0 shape so\n * consumers reuse one parser; there is NO tenantid attribute — the tenant\n * rides in the subject, exactly like the identity feed.\n *\n * The transport is INJECTED (the `publish` callback) because a Cloudflare\n * Worker cannot dial NATS over TCP. Two deployment shapes, stated honestly:\n *\n * - **Workers-resident relay**: a Cron Trigger calling `drainOutbox`.\n * Cloudflare floors cron at 1 minute, so publish lag is ≥1 minute.\n * Do not promise consumers anything faster from this shape.\n * - **Host-side loop**: any long-lived process with a NATS client (nats.js on\n * Node, or the platform's own relay) polling `drainOutbox` gets sub-second\n * lag. Same function, different clock.\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[]): {\n first<T = unknown>(): Promise<T | null>;\n run(): Promise<unknown>;\n all<T = unknown>(): Promise<{ results: T[] }>;\n };\n };\n}\n\nexport class ProducerContractError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ProducerContractError\";\n }\n}\n\n/**\n * DDL for the outbox table — copy into a migration verbatim (drizzle users:\n * keep the ORM schema in lockstep). The index serves `drainOutbox`'s\n * \"unpublished, in sequence order\" read.\n */\nexport const OUTBOX_TABLE_SQL = `CREATE TABLE IF NOT EXISTS outbox (\n id TEXT PRIMARY KEY,\n sequence INTEGER NOT NULL,\n subject TEXT NOT NULL,\n type TEXT NOT NULL,\n organization_id TEXT NOT NULL,\n data TEXT NOT NULL,\n created_at TEXT NOT NULL,\n published_at TEXT NULL\n);\nCREATE INDEX IF NOT EXISTS outbox_unpublished_idx ON outbox (published_at, sequence);`;\n\n/**\n * CloudEvents 1.0 envelope as `drainOutbox` hands it to `publish` — the same\n * shape ./sync's IdentityEnvelope reads, and deliberately WITHOUT a tenantid\n * attribute: the tenant is the subject's `{organizationId}` token.\n */\nexport interface DomainEnvelope {\n specversion: \"1.0\";\n /** UUIDv4 — THE idempotency key for consumers. */\n id: string;\n /** Producer identity: `godx.svc.{service}` (the subject's first three tokens). */\n source: string;\n /** jp.godx.{service}.{aggregate}.{action} */\n type: string;\n /** godx.svc.{service}.{organizationId}.{aggregate}.{action} */\n subject: string;\n time: string;\n datacontenttype: \"application/json\";\n /** Per-producer monotonic ordering fence (outbox.sequence). */\n sequence: number;\n data: Record<string, unknown>;\n}\n\nexport interface DomainEventInput {\n service: string;\n organizationId: string;\n aggregate: string;\n action: string;\n data: Record<string, unknown>;\n /** CloudEvents id — generated (crypto.randomUUID) when not supplied. */\n id?: string;\n}\n\nexport interface RecordedDomainEvent {\n id: string;\n sequence: number;\n subject: string;\n type: string;\n}\n\n/** Every subject/type token: letters, digits, `_`, `-` — never `.`, which separates tokens. */\nconst TOKEN = /^[A-Za-z0-9_-]+$/;\n\nfunction requireToken(name: string, value: string): string {\n if (!TOKEN.test(value)) {\n throw new ProducerContractError(\n `${name} ${JSON.stringify(value)} is not a valid subject token — expected /^[A-Za-z0-9_-]+$/ (\".\" separates tokens and may not appear inside one).`,\n );\n }\n return value;\n}\n\n/**\n * Record one domain event in the outbox, next to the state change it\n * describes. Composes:\n * - subject = `godx.svc.{service}.{organizationId}.{aggregate}.{action}`\n * - type = `jp.godx.{service}.{aggregate}.{action}`\n *\n * Sequence is per-producer monotonic via `MAX(sequence) + 1`. That\n * read-then-insert is racy on paper; on D1 it is safe in practice because\n * D1 serializes writes per database — two invocations cannot interleave\n * between the SELECT and the INSERT. On any other SQLite with genuinely\n * concurrent writers, run this inside the caller's transaction (which is\n * where it belongs anyway: the event must not outlive a rolled-back write).\n */\nexport async function recordDomainEvent(db: D1Like, input: DomainEventInput): Promise<RecordedDomainEvent> {\n const service = requireToken(\"service\", input.service);\n const organizationId = requireToken(\"organizationId\", input.organizationId);\n const aggregate = requireToken(\"aggregate\", input.aggregate);\n const action = requireToken(\"action\", input.action);\n\n const subject = `godx.svc.${service}.${organizationId}.${aggregate}.${action}`;\n const type = `jp.godx.${service}.${aggregate}.${action}`;\n const id = input.id ?? crypto.randomUUID();\n\n const next = await db\n .prepare(\"SELECT COALESCE(MAX(sequence), 0) + 1 AS next FROM outbox\")\n .bind()\n .first<{ next: number }>();\n const sequence = Number(next?.next ?? 1);\n\n await db\n .prepare(\n \"INSERT INTO outbox (id, sequence, subject, type, organization_id, data, created_at, published_at) VALUES (?, ?, ?, ?, ?, ?, ?, NULL)\",\n )\n .bind(id, sequence, subject, type, organizationId, JSON.stringify(input.data), new Date().toISOString())\n .run();\n\n return { id, sequence, subject, type };\n}\n\n/**\n * Publish unpublished rows in sequence order through the injected transport.\n * `published_at` is marked only AFTER `publish` resolves — a crash between\n * the two re-publishes the row, so consumers dedupe on the CloudEvents `id`\n * (at-least-once, exactly like the identity feed). The drain STOPS at the\n * first failure so no later event overtakes an earlier one; the failed row\n * stays unpublished and the next drain retries it, so inspect `remaining`\n * rather than expecting a throw.\n */\nexport async function drainOutbox(\n db: D1Like,\n publish: (event: { subject: string; envelope: DomainEnvelope }) => Promise<void>,\n { batch = 100 }: { batch?: number } = {},\n): Promise<{ published: number; remaining: number }> {\n interface Row {\n id: string;\n sequence: number;\n subject: string;\n type: string;\n data: string;\n created_at: string;\n }\n const { results } = await db\n .prepare(\n \"SELECT id, sequence, subject, type, data, created_at FROM outbox WHERE published_at IS NULL ORDER BY sequence LIMIT ?\",\n )\n .bind(batch)\n .all<Row>();\n\n let published = 0;\n for (const row of results) {\n const envelope: DomainEnvelope = {\n specversion: \"1.0\",\n id: row.id,\n // Tokens cannot contain \".\" (validated on record), so the first three\n // subject tokens are exactly `godx.svc.{service}` — the producer.\n source: row.subject.split(\".\").slice(0, 3).join(\".\"),\n type: row.type,\n subject: row.subject,\n time: row.created_at,\n datacontenttype: \"application/json\",\n sequence: row.sequence,\n data: JSON.parse(row.data) as Record<string, unknown>,\n };\n try {\n await publish({ subject: row.subject, envelope });\n } catch {\n break; // order preserved: nothing after the failed row may go out first\n }\n await db.prepare(\"UPDATE outbox SET published_at = ? WHERE id = ?\").bind(new Date().toISOString(), row.id).run();\n published++;\n }\n\n const count = await db\n .prepare(\"SELECT COUNT(*) AS n FROM outbox WHERE published_at IS NULL\")\n .bind()\n .first<{ n: number }>();\n return { published, remaining: Number(count?.n ?? 0) };\n}\n"],"mappings":";AA8BO,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC/C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAOO,IAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoDhC,IAAM,QAAQ;AAEd,SAAS,aAAa,MAAc,OAAuB;AACzD,MAAI,CAAC,MAAM,KAAK,KAAK,GAAG;AACtB,UAAM,IAAI;AAAA,MACR,GAAG,IAAI,IAAI,KAAK,UAAU,KAAK,CAAC;AAAA,IAClC;AAAA,EACF;AACA,SAAO;AACT;AAeA,eAAsB,kBAAkB,IAAY,OAAuD;AACzG,QAAM,UAAU,aAAa,WAAW,MAAM,OAAO;AACrD,QAAM,iBAAiB,aAAa,kBAAkB,MAAM,cAAc;AAC1E,QAAM,YAAY,aAAa,aAAa,MAAM,SAAS;AAC3D,QAAM,SAAS,aAAa,UAAU,MAAM,MAAM;AAElD,QAAM,UAAU,YAAY,OAAO,IAAI,cAAc,IAAI,SAAS,IAAI,MAAM;AAC5E,QAAM,OAAO,WAAW,OAAO,IAAI,SAAS,IAAI,MAAM;AACtD,QAAM,KAAK,MAAM,MAAM,OAAO,WAAW;AAEzC,QAAM,OAAO,MAAM,GAChB,QAAQ,2DAA2D,EACnE,KAAK,EACL,MAAwB;AAC3B,QAAM,WAAW,OAAO,MAAM,QAAQ,CAAC;AAEvC,QAAM,GACH;AAAA,IACC;AAAA,EACF,EACC,KAAK,IAAI,UAAU,SAAS,MAAM,gBAAgB,KAAK,UAAU,MAAM,IAAI,IAAG,oBAAI,KAAK,GAAE,YAAY,CAAC,EACtG,IAAI;AAEP,SAAO,EAAE,IAAI,UAAU,SAAS,KAAK;AACvC;AAWA,eAAsB,YACpB,IACA,SACA,EAAE,QAAQ,IAAI,IAAwB,CAAC,GACY;AASnD,QAAM,EAAE,QAAQ,IAAI,MAAM,GACvB;AAAA,IACC;AAAA,EACF,EACC,KAAK,KAAK,EACV,IAAS;AAEZ,MAAI,YAAY;AAChB,aAAW,OAAO,SAAS;AACzB,UAAM,WAA2B;AAAA,MAC/B,aAAa;AAAA,MACb,IAAI,IAAI;AAAA;AAAA;AAAA,MAGR,QAAQ,IAAI,QAAQ,MAAM,GAAG,EAAE,MAAM,GAAG,CAAC,EAAE,KAAK,GAAG;AAAA,MACnD,MAAM,IAAI;AAAA,MACV,SAAS,IAAI;AAAA,MACb,MAAM,IAAI;AAAA,MACV,iBAAiB;AAAA,MACjB,UAAU,IAAI;AAAA,MACd,MAAM,KAAK,MAAM,IAAI,IAAI;AAAA,IAC3B;AACA,QAAI;AACF,YAAM,QAAQ,EAAE,SAAS,IAAI,SAAS,SAAS,CAAC;AAAA,IAClD,QAAQ;AACN;AAAA,IACF;AACA,UAAM,GAAG,QAAQ,iDAAiD,EAAE,MAAK,oBAAI,KAAK,GAAE,YAAY,GAAG,IAAI,EAAE,EAAE,IAAI;AAC/G;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM,GACjB,QAAQ,6DAA6D,EACrE,KAAK,EACL,MAAqB;AACxB,SAAO,EAAE,WAAW,WAAW,OAAO,OAAO,KAAK,CAAC,EAAE;AACvD;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/server-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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": {
|
|
@@ -55,6 +55,13 @@
|
|
|
55
55
|
"worker": "./dist/org.js",
|
|
56
56
|
"browser": "./dist/browser-guard.js",
|
|
57
57
|
"default": "./dist/org.js"
|
|
58
|
+
},
|
|
59
|
+
"./producer": {
|
|
60
|
+
"types": "./dist/producer.d.ts",
|
|
61
|
+
"workerd": "./dist/producer.js",
|
|
62
|
+
"worker": "./dist/producer.js",
|
|
63
|
+
"browser": "./dist/browser-guard.js",
|
|
64
|
+
"default": "./dist/producer.js"
|
|
58
65
|
}
|
|
59
66
|
},
|
|
60
67
|
"scripts": {
|