@foir/sdk 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +719 -27
- package/README.md +121 -2
- package/dist/auth/auth.d.ts +72 -1
- package/dist/auth/hosted.d.ts +71 -4
- package/dist/auth/index.d.ts +3 -3
- package/dist/auth/operations.d.ts +84 -0
- package/dist/auth/ops.d.ts +18 -0
- package/dist/auth.js +209 -47
- package/dist/client.js +101 -13
- package/dist/codegen/block-registry.d.ts +1 -1
- package/dist/codegen/block-scaffold.d.ts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/item-registry.d.ts +1 -1
- package/dist/codegen/item-scaffold.d.ts +1 -1
- package/dist/codegen/preset.d.ts +3 -2
- package/dist/content/content-renderer.d.ts +4 -5
- package/dist/content/index.d.ts +1 -1
- package/dist/content/richtext.d.ts +1 -1
- package/dist/data/builtins.d.ts +81 -13
- package/dist/data/client.d.ts +92 -9
- package/dist/data/decimal.d.ts +212 -0
- package/dist/data/document.d.ts +14 -1
- package/dist/data/index.d.ts +22 -10
- package/dist/data/manifest.d.ts +40 -4
- package/dist/data/schema.d.ts +18 -11
- package/dist/data/selection.d.ts +15 -0
- package/dist/data/subscriptions.d.ts +36 -10
- package/dist/data/transport.d.ts +363 -24
- package/dist/data/ws-transport.d.ts +8 -2
- package/dist/data.js +591 -64
- package/dist/graphql/cdn-route.d.ts +1 -1
- package/dist/graphql/foir-request.d.ts +1 -1
- package/dist/index.js +0 -9
- package/dist/live/coalesce.d.ts +57 -0
- package/dist/live/drift.d.ts +58 -0
- package/dist/live/events.d.ts +13 -0
- package/dist/live/headers.d.ts +53 -0
- package/dist/live/index.d.ts +54 -0
- package/dist/live/keys.d.ts +72 -0
- package/dist/live/manager.d.ts +77 -0
- package/dist/live/react/index.d.ts +143 -0
- package/dist/live/reconcile.d.ts +82 -0
- package/dist/live/resync.d.ts +59 -0
- package/dist/live/store.d.ts +134 -0
- package/dist/live/tagIndex.d.ts +29 -0
- package/dist/live/tanstack.d.ts +127 -0
- package/dist/live/writetoken.d.ts +86 -0
- package/dist/live-react.js +395 -0
- package/dist/live.js +845 -0
- package/dist/next/cacheTags.d.ts +245 -0
- package/dist/next/index.d.ts +7 -0
- package/dist/next.js +260 -34
- package/dist/push.js +163 -36
- package/dist/react.js +157 -33
- package/dist/realtime/index.d.ts +4 -5
- package/dist/server/events-client.d.ts +54 -4
- package/dist/server/index.d.ts +4 -2
- package/dist/server/operation-types.d.ts +86 -58
- package/dist/server/read-dispatch.d.ts +53 -0
- package/dist/server/require-scoped-token.d.ts +20 -1
- package/dist/server.js +602 -10
- package/dist/transport/foir-http.d.ts +168 -15
- package/package.json +10 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@foir/sdk/server`'s event-log client — cursor-resumable consumption of a
|
|
3
|
-
* project's durable event history
|
|
3
|
+
* project's durable event history.
|
|
4
4
|
*
|
|
5
5
|
* ```ts
|
|
6
6
|
* import { createEventsClient } from '@foir/sdk/server';
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* OPERATOR SURFACE. This is not a customer-facing capability.
|
|
22
22
|
* ---------------------------------------------------------------------------
|
|
23
23
|
*
|
|
24
|
-
* DECIDED 2026-08-04
|
|
24
|
+
* DECIDED 2026-08-04. The event log is reachable by an `sk_` api key
|
|
25
25
|
* and by an admin session. It is NOT reachable by a customer token, there is no
|
|
26
26
|
* customer lane for it on api-public, and a `pk_` key can never be minted
|
|
27
27
|
* holding `events:list.project` at all.
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
* needs. It also keeps this module `graphql`-free, like `@foir/sdk/data`.
|
|
74
74
|
*/
|
|
75
75
|
import { FoirRequestError } from '../transport/foir-http';
|
|
76
|
+
import { type FoirReconnectPolicy, type FoirSubscription, type FoirSubscriptionHandlers, type FoirWebSocketFactory } from '../data/ws-transport';
|
|
76
77
|
/** The path `GET /v1/events` is mounted on, inside api-public's api-key group. */
|
|
77
78
|
export declare const FOIR_EVENTS_PATH = "/v1/events";
|
|
78
79
|
/**
|
|
@@ -109,7 +110,10 @@ export interface FoirEvent {
|
|
|
109
110
|
* comparing, never for counting.
|
|
110
111
|
*/
|
|
111
112
|
seq: number;
|
|
112
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* The registry name, e.g. `"RECORD_CREATED"`. SCREAMING_SNAKE, never the
|
|
115
|
+
* dotted form.
|
|
116
|
+
*/
|
|
113
117
|
event: string;
|
|
114
118
|
/**
|
|
115
119
|
* The event's own flat keys, plus `state` and `stateSchema`.
|
|
@@ -171,7 +175,9 @@ export interface FoirEventsListParams {
|
|
|
171
175
|
/** Page size. The server defaults to 100 and caps at 1000. */
|
|
172
176
|
limit?: number;
|
|
173
177
|
/**
|
|
174
|
-
* Filter by event name, e.g. `['
|
|
178
|
+
* Filter by event name, e.g. `['RECORD_CREATED', 'RECORD_UPDATED']`.
|
|
179
|
+
* SCREAMING_SNAKE: a name in any other casing matches nothing and comes back
|
|
180
|
+
* as an empty page rather than an error, so a wrong spelling is silent. Omit for
|
|
175
181
|
* every event.
|
|
176
182
|
*/
|
|
177
183
|
events?: readonly string[];
|
|
@@ -213,6 +219,21 @@ export interface FoirEventsClientConfig {
|
|
|
213
219
|
* cannot be clobbered by accident.
|
|
214
220
|
*/
|
|
215
221
|
requestInit?: Omit<RequestInit, 'method' | 'headers' | 'body'>;
|
|
222
|
+
/**
|
|
223
|
+
* Override the WebSocket endpoint `subscribe()` connects to. Normally derived
|
|
224
|
+
* from `apiUrl`; set it when the socket is not on the API origin.
|
|
225
|
+
*/
|
|
226
|
+
wsUrl?: string;
|
|
227
|
+
/**
|
|
228
|
+
* WebSocket constructor, called as `(url, protocols)`. Defaults to
|
|
229
|
+
* `globalThis.WebSocket`. Node 18/20 have no global one, so a server process
|
|
230
|
+
* on those versions passes the `ws` package's.
|
|
231
|
+
*
|
|
232
|
+
* Only `subscribe()` uses it; `list()` and `stream()` are plain fetch.
|
|
233
|
+
*/
|
|
234
|
+
webSocketImpl?: FoirWebSocketFactory;
|
|
235
|
+
/** How the `subscribe()` socket behaves when it drops. */
|
|
236
|
+
reconnect?: FoirReconnectPolicy;
|
|
216
237
|
}
|
|
217
238
|
export interface FoirEventsClient {
|
|
218
239
|
/** One page of the project's event history. */
|
|
@@ -227,6 +248,35 @@ export interface FoirEventsClient {
|
|
|
227
248
|
* including mid-page after a crash.
|
|
228
249
|
*/
|
|
229
250
|
stream(params?: FoirEventsStreamParams): AsyncGenerator<FoirEvent, void, void>;
|
|
251
|
+
/**
|
|
252
|
+
* The same cursor, pushed instead of pulled.
|
|
253
|
+
*
|
|
254
|
+
* The server backfills from `after` and then continues live on ONE stream, so
|
|
255
|
+
* there is no gap between catching up and keeping up and no second code path
|
|
256
|
+
* to write. `stream()` remains the right choice for a batch job that wants to
|
|
257
|
+
* drain and exit; this is for a process that stays open.
|
|
258
|
+
*
|
|
259
|
+
* The cursor advances as events arrive, so a dropped socket resumes from the
|
|
260
|
+
* last event delivered rather than replaying from where the subscription
|
|
261
|
+
* originally started.
|
|
262
|
+
*
|
|
263
|
+
* A cursor older than the retention window ends the subscription with a
|
|
264
|
+
* {@link FoirEventCursorExpiredError} carrying `resumeFrom`, exactly as the
|
|
265
|
+
* HTTP lane answers 410 — a gap is always something a consumer is told about,
|
|
266
|
+
* never something it has to notice.
|
|
267
|
+
*/
|
|
268
|
+
subscribe(params: FoirEventsSubscribeParams): FoirSubscription;
|
|
269
|
+
}
|
|
270
|
+
export interface FoirEventsSubscribeParams extends FoirSubscriptionHandlers<FoirEvent> {
|
|
271
|
+
/**
|
|
272
|
+
* Exclusive cursor: begin with events having `seq > after`. Omit to start
|
|
273
|
+
* from the live edge rather than the oldest retained event — a long-running
|
|
274
|
+
* consumer that has no checkpoint usually wants what happens next, not the
|
|
275
|
+
* whole window.
|
|
276
|
+
*/
|
|
277
|
+
after?: number;
|
|
278
|
+
/** Filter by event name. Omit for every event. */
|
|
279
|
+
events?: readonly string[];
|
|
230
280
|
}
|
|
231
281
|
/**
|
|
232
282
|
* The cursor named a position older than the retention window, so events the
|
package/dist/server/index.d.ts
CHANGED
|
@@ -2,11 +2,13 @@ export { createServerClient, ServerClientError } from './server-client';
|
|
|
2
2
|
export { verifyWebhookSignature } from './webhook-helpers';
|
|
3
3
|
export { verifyScopedToken, parseContextHeader, parseSubject, recordsReadModels, recordsWriteModels, type ScopedTokenClaims, type ScopedTokenSubject, type VerifyScopedTokenOptions, } from './verify-scoped-token';
|
|
4
4
|
export { requireScopedToken, ScopedTokenError, type RequireScopedTokenOptions, type ScopedTokenIngress, } from './require-scoped-token';
|
|
5
|
+
export { readDispatch, OperationPayloadError, type BodyCarrier, } from './read-dispatch';
|
|
5
6
|
export type { ServerClient, ServerClientOptions, GraphQLResponse, EditorPlacement, } from './types';
|
|
6
7
|
export type { OperationPayload, OperationTrigger, OperationRecordContext, OperationContext, OperationResult, OperationCallback, } from './operation-types';
|
|
7
8
|
export { createCallbackClient } from './callback-client';
|
|
8
9
|
export type { CallbackClient, CallbackResult } from './callback-client';
|
|
9
10
|
export { createEventsClient, FoirEventCursorExpiredError, isFoirEventCursorExpired, FOIR_EVENTS_PATH, FOIR_EVENTS_SCOPE, FOIR_EVENTS_RETENTION_DAYS, } from './events-client';
|
|
10
|
-
export type { FoirEvent, FoirEventActor, FoirEventPage, FoirEventRetention, FoirEventsClient, FoirEventsClientConfig, FoirEventsListParams, FoirEventsStreamParams, } from './events-client';
|
|
11
|
+
export type { FoirEvent, FoirEventActor, FoirEventPage, FoirEventRetention, FoirEventsClient, FoirEventsClientConfig, FoirEventsListParams, FoirEventsStreamParams, FoirEventsSubscribeParams, } from './events-client';
|
|
12
|
+
export type { FoirSubscription, FoirSubscriptionHandlers } from '../data/ws-transport';
|
|
11
13
|
export { FoirRequestError, isFoirForbidden, isFoirNotFound, isFoirRateLimited, isFoirUnauthenticated, } from '../transport/foir-http';
|
|
12
|
-
export type { FoirErrorCode, FoirErrorExtensions, FoirGraphQLErrorItem } from '../transport/foir-http';
|
|
14
|
+
export type { FoirErrorCode, FoirErrorExtensions, FoirGraphQLErrorItem, FoirAuthDowngrade, } from '../transport/foir-http';
|
|
@@ -1,41 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type definitions for platform operations.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* `OperationPayload` is the body the platform POSTs to an operation endpoint,
|
|
5
|
+
* field for field. It is written against the worker that builds it
|
|
6
|
+
* (`buildOperationPayload` in the platform's operation processor), not against
|
|
7
|
+
* what an endpoint might like to receive: every field here is one the platform
|
|
8
|
+
* sends, present exactly when the platform sends it. A field the platform does
|
|
9
|
+
* not send is not here, because a type that promises one is how an endpoint
|
|
10
|
+
* comes to read `undefined` and refuse every call.
|
|
11
|
+
*
|
|
12
|
+
* Read it with `readDispatch(request)`, which parses and checks the body into
|
|
13
|
+
* this type.
|
|
5
14
|
*/
|
|
6
15
|
/**
|
|
7
|
-
*
|
|
16
|
+
* The body the platform sends an operation endpoint.
|
|
17
|
+
*
|
|
18
|
+
* The same shape however the run was started (a person in the console, an
|
|
19
|
+
* `execute<Op>` call, a lifecycle hook, a schedule) and whichever way it is
|
|
20
|
+
* delivered (to the registered endpoint, or down `foir operations dev` to one
|
|
21
|
+
* on your machine).
|
|
8
22
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
23
|
+
* `TInput` types `input`. It is an assertion, not a check: the platform
|
|
24
|
+
* validates `input` against the operation's input schema before dispatching,
|
|
25
|
+
* and this parameter lets the endpoint say what that schema is.
|
|
11
26
|
*/
|
|
12
|
-
export interface OperationPayload {
|
|
13
|
-
/**
|
|
14
|
-
executionId
|
|
15
|
-
/**
|
|
27
|
+
export interface OperationPayload<TInput extends object = Record<string, unknown>> {
|
|
28
|
+
/** The run. Always present; quote it in a callback and in your own logs. */
|
|
29
|
+
executionId: string;
|
|
30
|
+
/** The key of the operation being run, e.g. `send_quote`. */
|
|
16
31
|
operationKey: string;
|
|
17
|
-
/** How
|
|
32
|
+
/** How the run was started. */
|
|
18
33
|
trigger: OperationTrigger;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* What the caller passed as the operation's input. Always an object:
|
|
36
|
+
* `readDispatch` reads a run started with no input (a schedule that declares
|
|
37
|
+
* none, which the platform serialises as `null`) as `{}`.
|
|
38
|
+
*/
|
|
39
|
+
input: TInput;
|
|
40
|
+
/** The tenant, project and principal the run belongs to. */
|
|
26
41
|
context: OperationContext;
|
|
27
|
-
/** Extension credentials (auto-injected by platform) */
|
|
28
|
-
credentials?: Record<string, unknown>;
|
|
29
|
-
/** Extension config (auto-injected by platform) */
|
|
30
|
-
config?: Record<string, unknown>;
|
|
31
42
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
|
|
43
|
+
* The content block the caller attached when it started the run, forwarded
|
|
44
|
+
* as the caller wrote it. Absent when there was none, which is every run
|
|
45
|
+
* started by a hook, a schedule or `execute<Op>`.
|
|
46
|
+
*/
|
|
47
|
+
content?: Record<string, unknown>;
|
|
48
|
+
/**
|
|
49
|
+
* The record block the caller attached when it started the run, forwarded
|
|
50
|
+
* as the caller wrote it. Absent when there was none. For a record event,
|
|
51
|
+
* the record's id arrives in `input` (for example `input.recordId`), not
|
|
52
|
+
* here: read the record back as the operation when you need its content.
|
|
53
|
+
*/
|
|
54
|
+
record?: OperationRecordContext;
|
|
55
|
+
/**
|
|
56
|
+
* Caller-supplied context about where the run was started from, forwarded as
|
|
57
|
+
* written. Absent when there was none.
|
|
58
|
+
*/
|
|
59
|
+
triggerContext?: Record<string, unknown>;
|
|
60
|
+
/**
|
|
61
|
+
* Where and how to report back. Present only when the run was dispatched in
|
|
62
|
+
* ASYNC mode: the endpoint answers `202 Accepted` and reports the outcome
|
|
63
|
+
* later through this block (see `createCallbackClient`).
|
|
36
64
|
*
|
|
37
|
-
* Absent
|
|
38
|
-
*
|
|
65
|
+
* Absent in the blocking (SYNC) mode. A blocking run has no callback path:
|
|
66
|
+
* the endpoint's response body is the result. Branch on this field being
|
|
67
|
+
* present to tell the two modes apart.
|
|
39
68
|
*/
|
|
40
69
|
callback?: OperationCallback;
|
|
41
70
|
}
|
|
@@ -61,53 +90,52 @@ export interface OperationCallback {
|
|
|
61
90
|
progress: string;
|
|
62
91
|
cancel: string;
|
|
63
92
|
};
|
|
93
|
+
/**
|
|
94
|
+
* The operation keys that led to this run, its own last. Present when the
|
|
95
|
+
* run was caused by another operation's work.
|
|
96
|
+
*/
|
|
97
|
+
causationChain?: string[];
|
|
64
98
|
}
|
|
65
99
|
/**
|
|
66
|
-
*
|
|
100
|
+
* How a run was started. The four values are the platform's own; a caller that
|
|
101
|
+
* names none is recorded as `api`.
|
|
102
|
+
*
|
|
103
|
+
* - `ui`: a console surface that names itself as the trigger, such as an app's
|
|
104
|
+
* configure page.
|
|
105
|
+
* - `api`: an `execute<Op>` call, or any caller that named no trigger.
|
|
106
|
+
* - `lifecycle`: a hook on a platform event (a record published, an operation
|
|
107
|
+
* completed).
|
|
108
|
+
* - `schedule`: a cron schedule firing.
|
|
67
109
|
*/
|
|
68
110
|
export interface OperationTrigger {
|
|
69
|
-
|
|
70
|
-
type: 'ui' | 'api' | 'lifecycle' | 'field';
|
|
71
|
-
/** Field key (for field-triggered operations) */
|
|
72
|
-
fieldKey?: string;
|
|
73
|
-
/** Field type (for field-triggered operations) */
|
|
74
|
-
fieldType?: string;
|
|
111
|
+
type: 'ui' | 'api' | 'lifecycle' | 'schedule';
|
|
75
112
|
}
|
|
76
113
|
/**
|
|
77
|
-
*
|
|
114
|
+
* The record block a caller attached to a run, forwarded as the caller wrote
|
|
115
|
+
* it. The platform does not shape it, so neither does this type.
|
|
78
116
|
*/
|
|
79
|
-
export
|
|
80
|
-
/** Record ID */
|
|
81
|
-
id: string;
|
|
82
|
-
/** Model key (e.g., 'tilly_note') */
|
|
83
|
-
modelKey: string;
|
|
84
|
-
/** Version ID (if versioned record) */
|
|
85
|
-
versionId?: string;
|
|
86
|
-
/** Record content/data */
|
|
87
|
-
data: Record<string, unknown>;
|
|
88
|
-
/** Record metadata */
|
|
89
|
-
metadata: {
|
|
90
|
-
/** Natural key (slug, handle, etc.) */
|
|
91
|
-
naturalKey?: string;
|
|
92
|
-
/** Additional metadata fields */
|
|
93
|
-
[key: string]: unknown;
|
|
94
|
-
};
|
|
95
|
-
}
|
|
117
|
+
export type OperationRecordContext = Record<string, unknown>;
|
|
96
118
|
/**
|
|
97
|
-
*
|
|
119
|
+
* The tenant, project and principal a run belongs to.
|
|
98
120
|
*/
|
|
99
121
|
export interface OperationContext {
|
|
100
122
|
/** Tenant ID */
|
|
101
123
|
tenantId: string;
|
|
102
124
|
/** Project ID */
|
|
103
125
|
projectId: string;
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
/**
|
|
127
|
+
* The principal that started the run. Always present, and the empty string
|
|
128
|
+
* when the run has none, as a hook's run does.
|
|
129
|
+
*/
|
|
130
|
+
userId: string;
|
|
131
|
+
/**
|
|
132
|
+
* The customer the run acts for, when it acts for one: the signed-in
|
|
133
|
+
* customer who ran it, or the owner of the record whose event fired the
|
|
134
|
+
* hook. The dispatch token carries the same customer, so a client built from
|
|
135
|
+
* it acts for them too.
|
|
136
|
+
*/
|
|
107
137
|
customerId?: string;
|
|
108
|
-
/**
|
|
109
|
-
locale?: string;
|
|
110
|
-
/** Timestamp when operation was triggered */
|
|
138
|
+
/** When the platform built this body, RFC 3339 in UTC to the second. */
|
|
111
139
|
timestamp: string;
|
|
112
140
|
}
|
|
113
141
|
/**
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* readDispatch: the body of an operation dispatch, parsed and checked.
|
|
3
|
+
*
|
|
4
|
+
* An operation endpoint is handed two things. The headers carry the dispatch
|
|
5
|
+
* token, which `requireScopedToken` verifies. The body carries the run: its id,
|
|
6
|
+
* the operation's key, how it was started, the context it runs in, and the
|
|
7
|
+
* caller's input under `input`. Reading the body as though it WERE the input is
|
|
8
|
+
* the mistake this exists to prevent, because it fails quietly: every field the
|
|
9
|
+
* endpoint looks for is absent, and the endpoint refuses every call on a
|
|
10
|
+
* missing field it was in fact sent.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const { dispatch } = await requireScopedToken(request, { expectedProjectId });
|
|
14
|
+
* const { input, context } = await readDispatch<{ email: string }>(request);
|
|
15
|
+
* const foir = createFoirClient({ apiUrl, dispatch, generated });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* The shape is the platform's `buildOperationPayload`, field for field; see
|
|
19
|
+
* `OperationPayload`. A body that is not that shape is refused with
|
|
20
|
+
* {@link OperationPayloadError}, naming what is wrong, rather than handed on
|
|
21
|
+
* half-read.
|
|
22
|
+
*
|
|
23
|
+
* What it does not do is drop fields. A key the platform adds later is kept on
|
|
24
|
+
* the returned object, untyped, because a reader that re-built the body from
|
|
25
|
+
* the fields it knows would delete whatever it did not, which is the exact way
|
|
26
|
+
* blocks of this payload have gone missing on their way to an endpoint before.
|
|
27
|
+
*/
|
|
28
|
+
import type { OperationPayload } from './operation-types';
|
|
29
|
+
/**
|
|
30
|
+
* The dispatch body was not an operation payload. `status` is the HTTP status
|
|
31
|
+
* the route should answer with: 400, since the request is malformed rather than
|
|
32
|
+
* unauthorised (the token has already been verified by the time the body is
|
|
33
|
+
* read).
|
|
34
|
+
*/
|
|
35
|
+
export declare class OperationPayloadError extends Error {
|
|
36
|
+
readonly status: 400;
|
|
37
|
+
constructor(message: string);
|
|
38
|
+
}
|
|
39
|
+
/** The minimal request shape needed: satisfied by any fetch-API Request. */
|
|
40
|
+
export interface BodyCarrier {
|
|
41
|
+
text(): Promise<string>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Read the dispatch body off an operation request.
|
|
45
|
+
*
|
|
46
|
+
* `TInput` types `input`, as an assertion: the platform has already checked the
|
|
47
|
+
* input against the operation's input schema, and this is where the endpoint
|
|
48
|
+
* says what that schema is.
|
|
49
|
+
*
|
|
50
|
+
* Reads the body, so call it once per request and after nothing else has
|
|
51
|
+
* consumed it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readDispatch<TInput extends object = Record<string, unknown>>(request: BodyCarrier): Promise<OperationPayload<TInput>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* requireScopedToken — the one-line ingress guard for BFF routes the
|
|
3
|
-
* platform dispatches operations to
|
|
3
|
+
* platform dispatches operations to.
|
|
4
4
|
*
|
|
5
5
|
* Operation endpoints must be publicly reachable (the platform's dispatcher
|
|
6
6
|
* refuses private networks), so "the BFF is never public-facing" is achieved
|
|
@@ -16,6 +16,17 @@
|
|
|
16
16
|
* });
|
|
17
17
|
* ```
|
|
18
18
|
*
|
|
19
|
+
* The verified token is also the endpoint's credential for calling Foir back.
|
|
20
|
+
* `dispatch` on the result is the token as a data-client credential, so the
|
|
21
|
+
* endpoint acts as the operation, with the capabilities the operation
|
|
22
|
+
* declared, and holds no key of its own. `generated` is the module
|
|
23
|
+
* `foir types --operation <key>` wrote for the operation:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* const { dispatch } = await requireScopedToken(request, { expectedProjectId });
|
|
27
|
+
* const foir = createFoirClient({ apiUrl: env.PLATFORM_API_URL, dispatch, generated });
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
19
30
|
* Throws {@link ScopedTokenError} with `status` 401 (missing/invalid token)
|
|
20
31
|
* or 403 (valid token for the wrong tenant/project) — map it to a response
|
|
21
32
|
* in one place:
|
|
@@ -30,6 +41,7 @@
|
|
|
30
41
|
* ```
|
|
31
42
|
*/
|
|
32
43
|
import type { ScopedTokenClaims, ScopedTokenSubject, VerifyScopedTokenOptions } from './verify-scoped-token';
|
|
44
|
+
import type { FoirDispatchCredential } from '../data/transport';
|
|
33
45
|
/**
|
|
34
46
|
* Verification failure with the HTTP status the route should answer with:
|
|
35
47
|
* 401 for a missing or invalid token, 403 for a valid token bound to the
|
|
@@ -66,6 +78,13 @@ export interface ScopedTokenIngress {
|
|
|
66
78
|
subject: ScopedTokenSubject;
|
|
67
79
|
/** Parsed `X-Foir-Context` correlation metadata (empty when absent). */
|
|
68
80
|
context: Record<string, string>;
|
|
81
|
+
/**
|
|
82
|
+
* The token this call verified, as the credential for a data client that
|
|
83
|
+
* calls Foir AS the operation:
|
|
84
|
+
* `createFoirClient({ apiUrl, dispatch, generated })`. It sends the token as
|
|
85
|
+
* `X-Foir-Token` with the dispatch's `X-Foir-Context`, and never an api key.
|
|
86
|
+
*/
|
|
87
|
+
dispatch: FoirDispatchCredential;
|
|
69
88
|
}
|
|
70
89
|
/**
|
|
71
90
|
* Verify the platform-minted scoped token on an inbound dispatch request.
|