|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/runtime": patch |
| 4 | +"@objectstack/service-i18n": patch |
| 5 | +--- |
| 6 | + |
| 7 | +fix(spec,runtime,service-i18n): the dispatcher domains and their service contracts describe the same surface (#4127) |
| 8 | + |
| 9 | +#4087 retired a `/storage` bridge that called `upload(key, data, options?)` as |
| 10 | +`upload(file, { request })` — a shape no implementation has. Sweeping the other |
| 11 | +dispatcher domains against `packages/spec/src/contracts/*` found the mirror-image |
| 12 | +gap in three places: the call site and the implementation agreed, and the |
| 13 | +**contract** was the thing that had never been written down. Each one was worked |
| 14 | +around at the call site with `typeof x.foo === 'function'` — a duck-type is what |
| 15 | +"the contract does not cover this" looks like when nobody fixes the contract. |
| 16 | + |
| 17 | +Fixed at the contract, per Prime Directive #12. |
| 18 | + |
| 19 | +**`INotificationService` — the inbox half.** `listInbox` / `markRead` / |
| 20 | +`markAllRead` now exist, with `InboxQuery` / `InboxNotification` / |
| 21 | +`InboxListResult` / `MarkReadResult`. Three SDK-expressed routes |
| 22 | +(`notifications.list` / `.markRead` / `.markAllRead`) have rested on them all |
| 23 | +along, implemented by `service-messaging`, while this contract described only |
| 24 | +`send`. The cost was not theoretical: the dev notification stub implements |
| 25 | +exactly `send` and `sendBatch` **because it followed the contract**, so the one |
| 26 | +implementation written to spec was the one the dispatcher had to duck-type past. |
| 27 | + |
| 28 | +They are optional, and the probe stays: an inbox needs a durable store, and a |
| 29 | +send-only provider (SMTP, Twilio, a Slack webhook) fills the slot legitimately |
| 30 | +without one. `handlerReady` cannot express that — the slot is serveable, one |
| 31 | +capability of it is absent. The `/notifications` domain now takes |
| 32 | +`INotificationService` instead of `as any`, and each write route probes its own |
| 33 | +method rather than riding the entry `listInbox` check (they are separately |
| 34 | +optional, so "has an inbox to read" never implied "has read-state to write"). |
| 35 | + |
| 36 | +**`II18nService.getFieldLabels`.** Both serving surfaces — the dispatcher's |
| 37 | +`/i18n/labels/:object/:locale` and service-i18n's own mount — probed for it and |
| 38 | +both documented it as "optional on `II18nService`", which was not true. It is |
| 39 | +now. service-i18n's probe loses two casts with it (one through |
| 40 | +`Record<string, unknown>`, one re-declaring the signature inline). |
| 41 | + |
| 42 | +**`IAutomationService.getFlowRuntimeStates`** + the `FlowRuntimeState` type. |
| 43 | +`GET /automation/_status` (and the CLI boot summary, and the |
| 44 | +`kernel:bootstrapped` audit) already called it while the contract stopped at |
| 45 | +`listFlows(): string[]`. The dispatcher's inline cast declared it as |
| 46 | +`{ name, enabled, bound }` — a third copy of the shape and a narrower one than |
| 47 | +the engine returns, dropping the `status` / `triggerType` / `object` fields that |
| 48 | +say WHY a flow is unbound. |
| 49 | + |
| 50 | +Two runtime fixes fell out of the same sweep: |
| 51 | + |
| 52 | +- **`POST /automation/trigger/:name` now builds a real `AutomationContext`.** |
| 53 | + It passed the raw HTTP body to `execute(name, body)`, so the |
| 54 | + `{ recordId, objectName, params }` translation never ran and — the sharper |
| 55 | + half — no caller identity was forwarded. A flow's default `runAs` is `'user'`, |
| 56 | + and a `runAs:'user'` run whose trigger resolved no user has its data |
| 57 | + operations REFUSED (#3760, fail-closed), so `client.automation.trigger()` |
| 58 | + could not run a data-touching flow at all while `POST /:name/trigger` could. |
| 59 | + service-automation's own comment claims "most trigger surfaces (REST action / |
| 60 | + trigger endpoint) already resolve the full envelope"; for this endpoint it was |
| 61 | + not true. Both routes share one context builder now. |
| 62 | +- **The dead `automationService.trigger(...)` probe is gone.** Nothing in the |
| 63 | + repo has ever implemented `trigger` on the automation slot and the contract |
| 64 | + never declared it, so the branch was unreachable on every deployment and its |
| 65 | + `execute` "fallback" was the route. Declaring `trigger?` would have blessed a |
| 66 | + second name for `execute`; the dead branch is deleted instead. |
| 67 | + |
| 68 | +No migration. Every added contract member is optional, so existing |
| 69 | +implementations stay valid; the two runtime fixes only make routes that were |
| 70 | +failing or degraded behave like their working twins. |
0 commit comments