@substrat-run/kernel 0.140.0 → 0.142.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/dist/async-invocation-log.d.ts +1 -1
- package/dist/async-invocation-log.d.ts.map +1 -1
- package/dist/async-invocation-log.js +1 -1
- package/dist/async-invocation-log.js.map +1 -1
- package/dist/capability.d.ts +127 -4
- package/dist/capability.d.ts.map +1 -1
- package/dist/capability.js +279 -20
- package/dist/capability.js.map +1 -1
- package/dist/connector-calls.d.ts +3 -72
- package/dist/connector-calls.d.ts.map +1 -1
- package/dist/connector-calls.js +3 -62
- package/dist/connector-calls.js.map +1 -1
- package/dist/index.d.ts +12 -91
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -58
- package/dist/index.js.map +1 -1
- package/dist/invocation-line.d.ts +211 -0
- package/dist/invocation-line.d.ts.map +1 -0
- package/dist/invocation-line.js +33 -0
- package/dist/invocation-line.js.map +1 -0
- package/dist/meters.d.ts +21 -1
- package/dist/meters.d.ts.map +1 -1
- package/dist/meters.js +0 -0
- package/dist/meters.js.map +1 -1
- package/dist/module-log.d.ts +5 -4
- package/dist/module-log.d.ts.map +1 -1
- package/dist/module-log.js +5 -4
- package/dist/module-log.js.map +1 -1
- package/dist/permission-checker.d.ts +23 -0
- package/dist/permission-checker.d.ts.map +1 -1
- package/dist/permission-checker.js.map +1 -1
- package/dist/permission-eval.d.ts.map +1 -1
- package/dist/permission-eval.js +77 -21
- package/dist/permission-eval.js.map +1 -1
- package/dist/platform-sweep.d.ts +81 -1
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +125 -0
- package/dist/platform-sweep.js.map +1 -1
- package/dist/scope-copy-ledger.d.ts +128 -0
- package/dist/scope-copy-ledger.d.ts.map +1 -0
- package/dist/scope-copy-ledger.js +125 -0
- package/dist/scope-copy-ledger.js.map +1 -0
- package/dist/scope-host.d.ts +157 -13
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js +2 -0
- package/dist/scope-host.js.map +1 -1
- package/dist/storage-gauge.d.ts +125 -0
- package/dist/storage-gauge.d.ts.map +1 -0
- package/dist/storage-gauge.js +234 -0
- package/dist/storage-gauge.js.map +1 -0
- package/package.json +12 -4
- package/dist/invocation-log.d.ts +0 -447
- package/dist/invocation-log.d.ts.map +0 -1
- package/dist/invocation-log.js +0 -410
- package/dist/invocation-log.js.map +0 -1
- package/dist/platform-call.d.ts +0 -45
- package/dist/platform-call.d.ts.map +0 -1
- package/dist/platform-call.js +0 -63
- package/dist/platform-call.js.map +0 -1
- package/dist/provider-error.d.ts +0 -38
- package/dist/provider-error.d.ts.map +0 -1
- package/dist/provider-error.js +0 -65
- package/dist/provider-error.js.map +0 -1
- package/dist/routed-node.d.ts +0 -87
- package/dist/routed-node.d.ts.map +0 -1
- package/dist/routed-node.js +0 -67
- package/dist/routed-node.js.map +0 -1
package/dist/invocation-log.js
DELETED
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One structured log line per invocation, carrying the two dimensions Cloudflare
|
|
3
|
-
* structurally cannot record: which TENANT and which SCOPE a request belonged to
|
|
4
|
-
* (`docs/architecture/observability.md` §4.2/§4.3).
|
|
5
|
-
*
|
|
6
|
-
* ## Why this exists at all
|
|
7
|
-
*
|
|
8
|
-
* Observability is keyed on the deployed unit — a worker script — and one vertical's
|
|
9
|
-
* script serves every tenant that installed it. That makes script-grain data safe for
|
|
10
|
-
* staff and for the vertical's builder, and unsafe for an installer: their own numbers
|
|
11
|
-
* are inseparable from everyone else's (§3). The router already stamps `tenantId` on the
|
|
12
|
-
* datapoints and log lines IT emits, which is why tenant-keyed *metrics* need no code in
|
|
13
|
-
* this package. Logs are the other half, and the router cannot supply it: a trace does
|
|
14
|
-
* not cross the dispatch hop. Verified against production — a router line's `traceId`
|
|
15
|
-
* reaches `substrat-control-plane` (a service binding) but never the dispatched vertical,
|
|
16
|
-
* and every vertical event is a trace of exactly one event. There is nothing to join on,
|
|
17
|
-
* so the line has to be emitted on this side.
|
|
18
|
-
*
|
|
19
|
-
* ## What it makes possible that nothing else does
|
|
20
|
-
*
|
|
21
|
-
* A vertical's successful request emits NO log event whatsoever — the host logs only on a
|
|
22
|
-
* platform fault, and the scope host logs nothing at all. So before this middleware, a
|
|
23
|
-
* vertical's entire log presence was its crashes. The stamped line is therefore not only
|
|
24
|
-
* the correlation key; it is the thing that makes a tenant-facing log view have any rows.
|
|
25
|
-
*
|
|
26
|
-
* ## The correlation contract
|
|
27
|
-
*
|
|
28
|
-
* Every log line emitted during one invocation shares `$metadata.requestId`, so a reader
|
|
29
|
-
* resolves a tenant's lines in two phases: filter on `tenantId` to find the invocations,
|
|
30
|
-
* then fetch every line sharing their request ids. That is what attributes a vertical's
|
|
31
|
-
* OWN `console.log` output — which carries no tenant of its own — to the tenant whose
|
|
32
|
-
* request produced it.
|
|
33
|
-
*
|
|
34
|
-
* The inverse of that contract is a rule, not an optimisation: a line with no `tenantId`
|
|
35
|
-
* is never shown to a tenant (§4.3). An un-routed local invocation has no asserted
|
|
36
|
-
* headers and therefore emits no line — there is no tenant it could be attributed to, and
|
|
37
|
-
* inventing one is the only way this could ever leak.
|
|
38
|
-
*
|
|
39
|
-
* ## The tenant is a VERIFIED assertion, never a header
|
|
40
|
-
*
|
|
41
|
-
* Which is why the line is written from `readRoutedNode`'s answer and not from
|
|
42
|
-
* `headers.get('x-substrat-tenant')`. The two look interchangeable — behind the router
|
|
43
|
-
* they are — and they are not, for exactly the reason #966 made `readRoutedNode` itself
|
|
44
|
-
* fail closed: K-26's boundary is that a vertical's script has no public route, and that
|
|
45
|
-
* is a DEPLOYMENT fact, with `workers.dev` on by default. Reading the header directly
|
|
46
|
-
* meant anyone who could reach the script could name any tenant they liked and have the
|
|
47
|
-
* line filed under it — a request the vertical then refuses with a 400, since `nodeFor`
|
|
48
|
-
* does verify, while this middleware's `finally` had already written the forged line.
|
|
49
|
-
*
|
|
50
|
-
* That is not a cosmetic wrong row. The read path uses the stamped line as PROOF that an
|
|
51
|
-
* invocation was a given tenant's, and admits that invocation's other log lines — which
|
|
52
|
-
* carry no tenant of their own — into that tenant's view on the strength of it. A forged
|
|
53
|
-
* stamp is therefore a way to put chosen text on somebody else's dashboard. So the same
|
|
54
|
-
* verification the vertical does for its own routing happens here, with the same secret
|
|
55
|
-
* and the same dev opt-out, and a failed one writes nothing at all.
|
|
56
|
-
*/
|
|
57
|
-
// The subpath, not the root: this module is bundled into every vertical's upload (the
|
|
58
|
-
// platform entry, #1893), and the root carries every schema in the vocabulary.
|
|
59
|
-
import { encodeInvocationRecord, FIELD_COVERAGE_HEADER, INVOCATION_RECORD_HEADER, invocationLevelOf, } from '@substrat-run/contracts/invocation-record';
|
|
60
|
-
import { ulid } from './ulid.js';
|
|
61
|
-
import { readRoutedNode, ROUTED_ID, RouterAssertionError } from './routed-node.js';
|
|
62
|
-
/**
|
|
63
|
-
* The per-request store key the middleware parks its {@link InvocationRecord} under (#1746).
|
|
64
|
-
* A published name, like `substratInvocationId`: `mountOperations` reads it in another
|
|
65
|
-
* package, and a vertical's own routes may fill it in too.
|
|
66
|
-
*/
|
|
67
|
-
export const INVOCATION_RECORD_KEY = 'substratInvocationRecord';
|
|
68
|
-
// The level and its type moved to contracts in #1904 — the router files its datapoint under
|
|
69
|
-
// the same level — and stay exported from here, where every caller already reads them.
|
|
70
|
-
export { invocationLevelOf } from '@substrat-run/contracts/invocation-record';
|
|
71
|
-
/** Build a line. Unfilled fields are `null` (or empty), as the line's contract says. */
|
|
72
|
-
export function invocationLine(f) {
|
|
73
|
-
const emitted = f.emitted;
|
|
74
|
-
return {
|
|
75
|
-
substrat: 'invocation',
|
|
76
|
-
...(f.kind ? { kind: f.kind } : {}),
|
|
77
|
-
tenantId: f.tenantId,
|
|
78
|
-
scopeId: f.scopeId,
|
|
79
|
-
vertical: f.vertical ?? null,
|
|
80
|
-
surface: f.surface ?? null,
|
|
81
|
-
method: f.method ?? null,
|
|
82
|
-
path: f.path ?? null,
|
|
83
|
-
status: f.status ?? null,
|
|
84
|
-
threw: f.threw,
|
|
85
|
-
durationMs: f.durationMs,
|
|
86
|
-
invocationId: f.invocationId,
|
|
87
|
-
level: f.level,
|
|
88
|
-
operation: f.operation ?? null,
|
|
89
|
-
problemCode: f.problemCode ?? null,
|
|
90
|
-
principalKind: f.principalKind ?? null,
|
|
91
|
-
eventCount: emitted ? emitted.total : null,
|
|
92
|
-
eventTypes: emitted ? distinct(emitted.events.map((e) => e.type)) : [],
|
|
93
|
-
entities: emitted && f.withEntities !== false ? distinct(emitted.events.map((e) => e.entity)) : [],
|
|
94
|
-
versionId: f.versionId ?? null,
|
|
95
|
-
...(f.outputFields ? { outputFields: f.outputFields, ...(f.fieldCoverageId ? { fieldCoverageId: f.fieldCoverageId } : {}) } : {}),
|
|
96
|
-
...(f.async ?? {}),
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
/** Distinct values, first occurrence wins — the order a reader expects to see them in. */
|
|
100
|
-
function distinct(values) {
|
|
101
|
-
return [...new Set(values)];
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* The platform's version binding, read straight off the env.
|
|
105
|
-
*
|
|
106
|
-
* The one exception to "bindings only through the options": `SUBSTRAT_VERSION_ID` is not
|
|
107
|
-
* the vertical's binding to name, the platform attaches it to every pushed script, and a
|
|
108
|
-
* vertical that had to pass it through would be one more line to forget with no way for
|
|
109
|
-
* it to be wrong. Read defensively — the env is typed `unknown` here.
|
|
110
|
-
*/
|
|
111
|
-
function versionIdOf(env) {
|
|
112
|
-
const v = env?.SUBSTRAT_VERSION_ID;
|
|
113
|
-
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* The path, with the query string DISCARDED.
|
|
117
|
-
*
|
|
118
|
-
* Not a tidiness choice. A vertical that speaks OIDC carries `code`, `state` and
|
|
119
|
-
* `id_token_hint` in its query strings, an invite or magic-link flow carries a
|
|
120
|
-
* single-use token, and the platform's own internal calls carry `tenantId`/`scopeId`.
|
|
121
|
-
* An access log that swallowed those would put live credentials into a store that is
|
|
122
|
-
* read by a wider audience than the request ever had, and keep them there for the
|
|
123
|
-
* backend's whole retention window. Logging the path alone loses nothing a reader
|
|
124
|
-
* needs: which route ran is the question, and the identifiers are already dimensions.
|
|
125
|
-
*/
|
|
126
|
-
function pathOf(url) {
|
|
127
|
-
try {
|
|
128
|
-
return new URL(url).pathname;
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
// A URL the runtime accepted but `URL` will not parse is not worth failing over.
|
|
132
|
-
return '/';
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* The registry of stamps, keyed by the incoming `Request`, on `globalThis` under a GLOBAL
|
|
137
|
-
* symbol. Global rather than module state, because two copies of this code meet in one
|
|
138
|
-
* request: the platform's entry (`withInvocationLog`, prebuilt into the upload) and the
|
|
139
|
-
* kernel the vertical bundled — different module instances, one `Symbol.for`. Weak, so a
|
|
140
|
-
* finished request's stamp goes with its `Request`.
|
|
141
|
-
*/
|
|
142
|
-
const STAMPS = Symbol.for('substrat.invocation-stamp');
|
|
143
|
-
function stamps() {
|
|
144
|
-
const g = globalThis;
|
|
145
|
-
return (g[STAMPS] ??= new WeakMap());
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* The stamp a request already carries, if a layer outside this one started it (#1893).
|
|
149
|
-
* `vertical-host` reads the record through this when the Hono context has none, which is
|
|
150
|
-
* the case when the platform's entry stamped the request and the vertical mounts nothing.
|
|
151
|
-
*/
|
|
152
|
-
export function invocationStampOf(request) {
|
|
153
|
-
return stamps().get(request);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* #1923: whether the per-response field walk is armed for this request — the one question the
|
|
157
|
-
* walk's two callers in `vertical-host` (the operation routes and the MCP door) ask.
|
|
158
|
-
*
|
|
159
|
-
* `false` for a request no stamp covers, and for a stamp begun by a layer that predates the
|
|
160
|
-
* switch: every version skew between the platform's entry and a vertical's bundled kernel
|
|
161
|
-
* reads as off, never as on.
|
|
162
|
-
*/
|
|
163
|
-
export function fieldCoverageArmed(request) {
|
|
164
|
-
return typeof stamps().get(request)?.fieldCoverageId === 'string';
|
|
165
|
-
}
|
|
166
|
-
function beginStamp(request, env, options) {
|
|
167
|
-
const stamp = { invocationId: ulid(), record: {} };
|
|
168
|
-
const fieldCoverageId = armedFieldCoverageId(request.headers, env, options);
|
|
169
|
-
if (fieldCoverageId)
|
|
170
|
-
stamp.fieldCoverageId = fieldCoverageId;
|
|
171
|
-
stamps().set(request, stamp);
|
|
172
|
-
return stamp;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* #1923: the dispatch id the ROUTER armed the field walk with for this request, or `undefined`.
|
|
176
|
-
*
|
|
177
|
-
* The header alone is a claim. It is honoured only when its value is a ULID and the request's
|
|
178
|
-
* router assertion verifies, with the same secret and the same dev opt-out as the tenant on
|
|
179
|
-
* the line, because it is trusted for the same reason: the router strips every inbound
|
|
180
|
-
* `x-substrat-*` header, and a caller that reaches the script some other way holds no secret.
|
|
181
|
-
* An unrouted request, or one whose assertion fails, is never armed.
|
|
182
|
-
*
|
|
183
|
-
* The header is read first, so a request the router did not sample — almost all of them —
|
|
184
|
-
* pays one header read and no verification.
|
|
185
|
-
*/
|
|
186
|
-
function armedFieldCoverageId(headers, env, options) {
|
|
187
|
-
let id;
|
|
188
|
-
try {
|
|
189
|
-
id = headers.get(FIELD_COVERAGE_HEADER);
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
return undefined;
|
|
193
|
-
}
|
|
194
|
-
if (id === null || !ROUTED_ID.test(id))
|
|
195
|
-
return undefined;
|
|
196
|
-
return routedNodeOrNull(headers, env, options) !== null ? id : undefined;
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Write the line for a finished request — the one place it is written, whichever layer
|
|
200
|
-
* started the stamp.
|
|
201
|
-
*
|
|
202
|
-
* No VERIFIED tenant ⇒ no line, and the three ways that happens are all silence here: no
|
|
203
|
-
* assertion at all (an un-routed call), an assertion this worker cannot verify or that is
|
|
204
|
-
* not the router's (`RouterAssertionError`), and — defensively — anything else the read
|
|
205
|
-
* throws. Writing a line for any of them would be filing one caller's request under a
|
|
206
|
-
* tenant of their choosing.
|
|
207
|
-
*/
|
|
208
|
-
function writeInvocationLine(stamp, done, options) {
|
|
209
|
-
// Called from a `finally`: a throw here would replace the vertical's answer, or its own
|
|
210
|
-
// error, with a logging failure. The platform wraps every uploaded script in this (#1893),
|
|
211
|
-
// so nothing in it may escape — a line that cannot be written is one missing line.
|
|
212
|
-
try {
|
|
213
|
-
writeLineOrThrow(stamp, done, options);
|
|
214
|
-
}
|
|
215
|
-
catch (e) {
|
|
216
|
-
try {
|
|
217
|
-
console.log(JSON.stringify({ substrat: 'invocation-log-fault', detail: String(e) }));
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
/* the console itself is gone; there is nowhere left to say so */
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
function writeLineOrThrow(stamp, done, options) {
|
|
225
|
-
const node = routedNodeOrNull(done.request.headers, done.env, options);
|
|
226
|
-
if (!node)
|
|
227
|
-
return;
|
|
228
|
-
const { record, invocationId } = stamp;
|
|
229
|
-
const line = invocationLine({
|
|
230
|
-
tenantId: node.tenantId,
|
|
231
|
-
scopeId: node.scopeId,
|
|
232
|
-
vertical: node.verticalSlug,
|
|
233
|
-
surface: node.surface,
|
|
234
|
-
method: done.request.method,
|
|
235
|
-
path: pathOf(done.request.url),
|
|
236
|
-
status: done.status,
|
|
237
|
-
threw: done.threw,
|
|
238
|
-
durationMs: Date.now() - done.started,
|
|
239
|
-
invocationId,
|
|
240
|
-
level: invocationLevelOf(done.status, done.threw, record.problemCode),
|
|
241
|
-
operation: record.operation,
|
|
242
|
-
problemCode: record.problemCode,
|
|
243
|
-
principalKind: record.principalKind,
|
|
244
|
-
...(record.emitted ? { emitted: record.emitted } : {}),
|
|
245
|
-
versionId: versionIdOf(done.env),
|
|
246
|
-
...(record.outputFields ? { outputFields: record.outputFields } : {}),
|
|
247
|
-
...(stamp.fieldCoverageId ? { fieldCoverageId: stamp.fieldCoverageId } : {}),
|
|
248
|
-
});
|
|
249
|
-
console.log(JSON.stringify(line));
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Mount as the FIRST middleware on a vertical's app, with the same two answers the
|
|
253
|
-
* vertical gives `readRoutedNode` in its own `nodeFor`:
|
|
254
|
-
*
|
|
255
|
-
* ```ts
|
|
256
|
-
* const app = new Hono<{ Bindings: Env }>();
|
|
257
|
-
* app.use(
|
|
258
|
-
* '*',
|
|
259
|
-
* invocationLog<Env>({
|
|
260
|
-
* routerSecret: (env) => env.ROUTER_SECRET,
|
|
261
|
-
* allowUnsigned: (env) => env.ALLOW_DEV_NODE === 'true',
|
|
262
|
-
* }),
|
|
263
|
-
* );
|
|
264
|
-
* ```
|
|
265
|
-
*
|
|
266
|
-
* First, because Hono composes handlers in registration order and stops at the one that
|
|
267
|
-
* returns a response — middleware registered after a route does not wrap that route.
|
|
268
|
-
*
|
|
269
|
-
* **Since #1893 the platform stamps every deployed request itself**, by wrapping the
|
|
270
|
-
* uploaded entry with `withInvocationLog`. A vertical that still mounts this middleware
|
|
271
|
-
* then finds the request already stamped and steps aside — it hands the platform's stamp
|
|
272
|
-
* to the handler chain through the context, as it always did, and writes no second line.
|
|
273
|
-
* Mounted where no platform entry runs (a dev server behind a dev router), it stamps as
|
|
274
|
-
* before.
|
|
275
|
-
*
|
|
276
|
-
* Nothing here can fail a request: the line is written in a `finally`, and a throw from
|
|
277
|
-
* the handler is re-thrown untouched for `onError` to map as it always did.
|
|
278
|
-
*/
|
|
279
|
-
export function invocationLog(options = {}) {
|
|
280
|
-
return async (c, next) => {
|
|
281
|
-
// Stamped already by the platform's entry: pass the stamp down, write nothing.
|
|
282
|
-
const existing = invocationStampOf(c.req.raw);
|
|
283
|
-
if (existing) {
|
|
284
|
-
c.set?.('substratInvocationId', existing.invocationId);
|
|
285
|
-
c.set?.(INVOCATION_RECORD_KEY, existing.record);
|
|
286
|
-
await next();
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
// Host code, so a real clock is correct here — `ctx.now()` is the module-code rule,
|
|
290
|
-
// and this middleware runs outside any operation's transaction.
|
|
291
|
-
const started = Date.now();
|
|
292
|
-
// Minted per request, before anything can emit. A ULID so it sorts by time like
|
|
293
|
-
// every other id on the spine.
|
|
294
|
-
const stamp = beginStamp(c.req.raw, c.env, options);
|
|
295
|
-
c.set?.('substratInvocationId', stamp.invocationId);
|
|
296
|
-
c.set?.(INVOCATION_RECORD_KEY, stamp.record);
|
|
297
|
-
let threw = false;
|
|
298
|
-
try {
|
|
299
|
-
await next();
|
|
300
|
-
}
|
|
301
|
-
catch (e) {
|
|
302
|
-
threw = true;
|
|
303
|
-
// Always re-thrown: the envelope `mountPlatformSurface` installs is what answers
|
|
304
|
-
// the caller, and swallowing here would turn a fault into a silent 200.
|
|
305
|
-
throw e;
|
|
306
|
-
}
|
|
307
|
-
finally {
|
|
308
|
-
writeInvocationLine(stamp, { request: { method: c.req.method, url: c.req.raw.url, headers: c.req.raw.headers }, env: c.env, status: threw ? null : (c.res?.status ?? null), threw, started }, options);
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
/** The handlers a module worker may export besides `fetch`, passed through untouched. */
|
|
313
|
-
const OTHER_HANDLERS = ['scheduled', 'queue', 'email', 'tail', 'trace', 'alarm', 'test'];
|
|
314
|
-
/**
|
|
315
|
-
* The platform's half of the stamp (#1893): wrap a module worker's `fetch` so every request
|
|
316
|
-
* it serves is stamped, whatever framework the worker is written in and whether or not it
|
|
317
|
-
* mounts the middleware.
|
|
318
|
-
*
|
|
319
|
-
* The control plane builds this into the entry it uploads in front of every vertical's
|
|
320
|
-
* bundle (`platform-entry.generated.ts`), which is what makes the stamp the platform's
|
|
321
|
-
* rather than a line each vertical has to remember. A request the vertical's own
|
|
322
|
-
* middleware already stamped — possible only if something wrapped this one — is passed
|
|
323
|
-
* straight through. The other handlers (`scheduled`, `queue`, …) are passed through
|
|
324
|
-
* untouched, bound to the worker, since a stamp is about a routed request.
|
|
325
|
-
*
|
|
326
|
-
* Nothing here can fail a request: the line is written in a `finally`, and a throw is
|
|
327
|
-
* re-thrown untouched.
|
|
328
|
-
*/
|
|
329
|
-
export function withInvocationLog(worker, options = {}) {
|
|
330
|
-
// The platform wraps whatever a bundle's default export is (#1893): a class entrypoint or
|
|
331
|
-
// no default at all has no `fetch` to stamp, and is handed back untouched.
|
|
332
|
-
const inner = worker?.fetch;
|
|
333
|
-
if (typeof inner !== 'function')
|
|
334
|
-
return worker;
|
|
335
|
-
const wrapped = {
|
|
336
|
-
async fetch(request, env, ctx) {
|
|
337
|
-
if (invocationStampOf(request))
|
|
338
|
-
return inner.call(worker, request, env, ctx);
|
|
339
|
-
const started = Date.now();
|
|
340
|
-
const stamp = beginStamp(request, env, options);
|
|
341
|
-
let status = null;
|
|
342
|
-
let threw = false;
|
|
343
|
-
try {
|
|
344
|
-
const response = await inner.call(worker, request, env, ctx);
|
|
345
|
-
status = response.status;
|
|
346
|
-
handRecordToRouter(response, stamp.record, request, env, options);
|
|
347
|
-
return response;
|
|
348
|
-
}
|
|
349
|
-
catch (e) {
|
|
350
|
-
threw = true;
|
|
351
|
-
throw e;
|
|
352
|
-
}
|
|
353
|
-
finally {
|
|
354
|
-
writeInvocationLine(stamp, { request, env, status, threw, started }, options);
|
|
355
|
-
}
|
|
356
|
-
},
|
|
357
|
-
};
|
|
358
|
-
for (const name of OTHER_HANDLERS) {
|
|
359
|
-
const h = worker[name];
|
|
360
|
-
if (typeof h === 'function')
|
|
361
|
-
wrapped[name] = h.bind(worker);
|
|
362
|
-
}
|
|
363
|
-
return wrapped;
|
|
364
|
-
}
|
|
365
|
-
/**
|
|
366
|
-
* #1904: hand the record back to the router on the response, which meters the request with
|
|
367
|
-
* it and strips the header before the response leaves (`INVOCATION_RECORD_HEADER`).
|
|
368
|
-
*
|
|
369
|
-
* Only on a request the router vouched for: a direct caller gets no header, and the router
|
|
370
|
-
* is the only reader. Never a throw — a response whose headers are immutable (one passed
|
|
371
|
-
* straight through from a `fetch`) reached no operation and has nothing to hand back, and
|
|
372
|
-
* metering must not fail a request either way.
|
|
373
|
-
*/
|
|
374
|
-
function handRecordToRouter(response, record, request, env, options) {
|
|
375
|
-
try {
|
|
376
|
-
const value = encodeInvocationRecord(record);
|
|
377
|
-
if (value === null || !routedNodeOrNull(request.headers, env, options))
|
|
378
|
-
return;
|
|
379
|
-
response.headers?.set?.(INVOCATION_RECORD_HEADER, value);
|
|
380
|
-
}
|
|
381
|
-
catch {
|
|
382
|
-
/* immutable headers: see above */
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
/**
|
|
386
|
-
* The verified node, or `null` — never a throw.
|
|
387
|
-
*
|
|
388
|
-
* `readRoutedNode` is deliberately loud: present-but-wrong headers are a
|
|
389
|
-
* misconfiguration or an attack and a caller that routes on them must hear about it.
|
|
390
|
-
* This caller does not route on them; it writes a log line in a `finally`, where a throw
|
|
391
|
-
* would replace the vertical's real answer with a logging failure. So the loudness is
|
|
392
|
-
* swallowed HERE and only here, and the request is unaffected either way.
|
|
393
|
-
*/
|
|
394
|
-
function routedNodeOrNull(headers, env, options) {
|
|
395
|
-
try {
|
|
396
|
-
return readRoutedNode(headers, {
|
|
397
|
-
expectedSecret: options.routerSecret?.(env),
|
|
398
|
-
allowUnsigned: options.allowUnsigned?.(env) ?? false,
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
catch (e) {
|
|
402
|
-
// `RouterAssertionError` is the expected shape — a bad, missing or unverifiable
|
|
403
|
-
// secret, or a malformed id. Anything else would be a bug in the read, and a bug in
|
|
404
|
-
// the LOGGER must not become a failed request either.
|
|
405
|
-
if (!(e instanceof RouterAssertionError))
|
|
406
|
-
console.log(JSON.stringify({ substrat: 'invocation-log-fault', detail: String(e) }));
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
//# sourceMappingURL=invocation-log.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"invocation-log.js","sourceRoot":"","sources":["../src/invocation-log.ts"],"names":[],"mappings":"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"}
|
package/dist/platform-call.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { HeaderReader } from './routed-node.js';
|
|
2
|
-
import type { ScopeStubOptions } from './scope-host.js';
|
|
3
|
-
/**
|
|
4
|
-
* Authenticating a call FROM the platform TO a vertical (K-31).
|
|
5
|
-
*
|
|
6
|
-
* Provisioning is control-plane-driven: the platform decides an instance should
|
|
7
|
-
* exist and tells the vertical to create it, because only the vertical can create a
|
|
8
|
-
* usable scope DO. This is the vertical's side of that call.
|
|
9
|
-
*
|
|
10
|
-
* It is shared code for the same reason `readRoutedNode` is — five verticals each
|
|
11
|
-
* re-deriving how to trust a header is five chances to get it wrong, and the one that
|
|
12
|
-
* gets it wrong is not obviously broken. Both are moving from the kernel to
|
|
13
|
-
* `vertical-host` (#1978), which already re-exports them.
|
|
14
|
-
*
|
|
15
|
-
* Note the direction. `readRoutedNode` answers "which tenant is this request for",
|
|
16
|
-
* and a request with no assertion is legitimate (a standalone deploy). This answers
|
|
17
|
-
* "is the platform itself calling", and there is no legitimate unauthenticated case:
|
|
18
|
-
* an open provisioning endpoint lets a stranger mint tenants inside the vertical.
|
|
19
|
-
* So this one **fails closed with no configuration at all**.
|
|
20
|
-
*/
|
|
21
|
-
/** Thrown when a call does not prove it came from the platform. */
|
|
22
|
-
export declare class PlatformCallError extends Error {
|
|
23
|
-
}
|
|
24
|
-
/** Constant-time compare, so a wrong secret leaks nothing through timing. */
|
|
25
|
-
export declare function secretMatches(presented: string | null, expected: string): boolean;
|
|
26
|
-
/**
|
|
27
|
-
* Both kick flags as the stub options that raise them (#1705 PR 2): spread into `getScope`'s
|
|
28
|
-
* options with the handler's header setter. One call per worker rather than one line per flag,
|
|
29
|
-
* so a flag added later reaches every vertical that uses this, and not only the ones that
|
|
30
|
-
* remembered the line. A vertical that skips it still works, and waits for the sweep.
|
|
31
|
-
*/
|
|
32
|
-
export declare function kickFlags(setHeader: (name: string, value: string) => void): Pick<ScopeStubOptions, 'onPlatformRequests' | 'onExportedEvents'>;
|
|
33
|
-
/**
|
|
34
|
-
* Throw unless this request proves it came from the platform.
|
|
35
|
-
*
|
|
36
|
-
* **An unset secret is a failure, not a bypass.** That is the opposite of how the
|
|
37
|
-
* router secret behaves, and deliberately so: there, an unset secret means "no router
|
|
38
|
-
* is configured", which a standalone deploy legitimately wants. Here it would mean
|
|
39
|
-
* "anyone may provision", which nothing legitimately wants. A template copied without
|
|
40
|
-
* the secret configured must refuse to provision rather than provision for strangers.
|
|
41
|
-
*/
|
|
42
|
-
export declare function assertPlatformCall(headers: HeaderReader, options?: {
|
|
43
|
-
expectedSecret?: string;
|
|
44
|
-
}): void;
|
|
45
|
-
//# sourceMappingURL=platform-call.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"platform-call.d.ts","sourceRoot":"","sources":["../src/platform-call.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAOxD;;;;;;;;;;;;;;;;;GAiBG;AAEH,mEAAmE;AACnE,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAE/C,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAOjF;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAC/C,IAAI,CAAC,gBAAgB,EAAE,oBAAoB,GAAG,kBAAkB,CAAC,CAKnE;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,YAAY,EACrB,OAAO,GAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAO,GACxC,IAAI,CAQN"}
|
package/dist/platform-call.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { EXPORTED_EVENTS_HEADER, PLATFORM_REQUEST_HEADER, PLATFORM_SECRET_HEADER, } from '@substrat-run/contracts/wire-headers';
|
|
2
|
-
/**
|
|
3
|
-
* Authenticating a call FROM the platform TO a vertical (K-31).
|
|
4
|
-
*
|
|
5
|
-
* Provisioning is control-plane-driven: the platform decides an instance should
|
|
6
|
-
* exist and tells the vertical to create it, because only the vertical can create a
|
|
7
|
-
* usable scope DO. This is the vertical's side of that call.
|
|
8
|
-
*
|
|
9
|
-
* It is shared code for the same reason `readRoutedNode` is — five verticals each
|
|
10
|
-
* re-deriving how to trust a header is five chances to get it wrong, and the one that
|
|
11
|
-
* gets it wrong is not obviously broken. Both are moving from the kernel to
|
|
12
|
-
* `vertical-host` (#1978), which already re-exports them.
|
|
13
|
-
*
|
|
14
|
-
* Note the direction. `readRoutedNode` answers "which tenant is this request for",
|
|
15
|
-
* and a request with no assertion is legitimate (a standalone deploy). This answers
|
|
16
|
-
* "is the platform itself calling", and there is no legitimate unauthenticated case:
|
|
17
|
-
* an open provisioning endpoint lets a stranger mint tenants inside the vertical.
|
|
18
|
-
* So this one **fails closed with no configuration at all**.
|
|
19
|
-
*/
|
|
20
|
-
/** Thrown when a call does not prove it came from the platform. */
|
|
21
|
-
export class PlatformCallError extends Error {
|
|
22
|
-
}
|
|
23
|
-
/** Constant-time compare, so a wrong secret leaks nothing through timing. */
|
|
24
|
-
export function secretMatches(presented, expected) {
|
|
25
|
-
if (!presented || presented.length !== expected.length)
|
|
26
|
-
return false;
|
|
27
|
-
let diff = 0;
|
|
28
|
-
for (let i = 0; i < expected.length; i++) {
|
|
29
|
-
diff |= presented.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
30
|
-
}
|
|
31
|
-
return diff === 0;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Both kick flags as the stub options that raise them (#1705 PR 2): spread into `getScope`'s
|
|
35
|
-
* options with the handler's header setter. One call per worker rather than one line per flag,
|
|
36
|
-
* so a flag added later reaches every vertical that uses this, and not only the ones that
|
|
37
|
-
* remembered the line. A vertical that skips it still works, and waits for the sweep.
|
|
38
|
-
*/
|
|
39
|
-
export function kickFlags(setHeader) {
|
|
40
|
-
return {
|
|
41
|
-
onPlatformRequests: () => setHeader(PLATFORM_REQUEST_HEADER, '1'),
|
|
42
|
-
onExportedEvents: () => setHeader(EXPORTED_EVENTS_HEADER, '1'),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Throw unless this request proves it came from the platform.
|
|
47
|
-
*
|
|
48
|
-
* **An unset secret is a failure, not a bypass.** That is the opposite of how the
|
|
49
|
-
* router secret behaves, and deliberately so: there, an unset secret means "no router
|
|
50
|
-
* is configured", which a standalone deploy legitimately wants. Here it would mean
|
|
51
|
-
* "anyone may provision", which nothing legitimately wants. A template copied without
|
|
52
|
-
* the secret configured must refuse to provision rather than provision for strangers.
|
|
53
|
-
*/
|
|
54
|
-
export function assertPlatformCall(headers, options = {}) {
|
|
55
|
-
const { expectedSecret } = options;
|
|
56
|
-
if (!expectedSecret) {
|
|
57
|
-
throw new PlatformCallError('platform calls are not configured on this deployment');
|
|
58
|
-
}
|
|
59
|
-
if (!secretMatches(headers.get(PLATFORM_SECRET_HEADER), expectedSecret)) {
|
|
60
|
-
throw new PlatformCallError('not a platform call');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
//# sourceMappingURL=platform-call.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"platform-call.js","sourceRoot":"","sources":["../src/platform-call.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,sCAAsC,CAAC;AAE9C;;;;;;;;;;;;;;;;;GAiBG;AAEH,mEAAmE;AACnE,MAAM,OAAO,iBAAkB,SAAQ,KAAK;CAAG;AAE/C,6EAA6E;AAC7E,MAAM,UAAU,aAAa,CAAC,SAAwB,EAAE,QAAgB;IACtE,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACrE,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,IAAI,IAAI,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CACvB,SAAgD;IAEhD,OAAO;QACL,kBAAkB,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,uBAAuB,EAAE,GAAG,CAAC;QACjE,gBAAgB,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,sBAAsB,EAAE,GAAG,CAAC;KAC/D,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAqB,EACrB,OAAO,GAAgC,EAAE;IAEzC,MAAM,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IACnC,IAAI,CAAC,cAAc,EAAE,CAAC;QACpB,MAAM,IAAI,iBAAiB,CAAC,sDAAsD,CAAC,CAAC;IACtF,CAAC;IACD,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,EAAE,cAAc,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,iBAAiB,CAAC,qBAAqB,CAAC,CAAC;IACrD,CAAC;AACH,CAAC"}
|
package/dist/provider-error.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 4xx statuses that are nevertheless worth retrying: the provider is refusing this attempt,
|
|
3
|
-
* not the request. Timeouts and locks clear on their own; a rate limit is an instruction to
|
|
4
|
-
* come back later, which is exactly what the next drain pass does.
|
|
5
|
-
*/
|
|
6
|
-
export declare const RETRYABLE_CLIENT_STATUSES: ReadonlySet<number>;
|
|
7
|
-
/**
|
|
8
|
-
* The HTTP status an error carries, if it carries one. Structural on purpose (see above):
|
|
9
|
-
* `ScriveApiError` and any future connector's error type both satisfy it without a shared base
|
|
10
|
-
* class, and an error that carries no status — a network failure, a bug — reads as `undefined`,
|
|
11
|
-
* which is the honest answer and keeps it retryable.
|
|
12
|
-
*/
|
|
13
|
-
export declare function providerErrorStatus(error: unknown): number | undefined;
|
|
14
|
-
/**
|
|
15
|
-
* Is this delivery over? — the DRAIN's question, and the only one that decides retries.
|
|
16
|
-
*
|
|
17
|
-
* A non-retryable 4xx, whoever raised it: attempt 101 carries the identical bytes past the
|
|
18
|
-
* identical check. Deliberately blind to WHO refused, because terminality does not depend on
|
|
19
|
-
* it — our own `validation_failed` is as final as the provider's 409, and both statuses come
|
|
20
|
-
* from the same structural read (`SubstratError` carries its catalog status; a connector's
|
|
21
|
-
* error carries the provider's).
|
|
22
|
-
*/
|
|
23
|
-
export declare function isTerminalDispatchFailure(error: unknown): boolean;
|
|
24
|
-
/**
|
|
25
|
-
* The PROVIDER refused the request — a 4xx that came back over the wire, not one we raised.
|
|
26
|
-
*
|
|
27
|
-
* The exclusion is the whole point (#841). This predicate reads a bare numeric `status`, and
|
|
28
|
-
* every `SubstratError` carries one from the problem catalog — so a `permission denied:
|
|
29
|
-
* protocol:read` raised on our own side of egress answered `true` here, and the drain
|
|
30
|
-
* captioned it "a client error the provider will refuse identically on retry". Scrive never
|
|
31
|
-
* saw that request. An operator who reads that sentence goes and audits their Scrive account.
|
|
32
|
-
*
|
|
33
|
-
* Terminality was never the part that was wrong, so it did not move: see
|
|
34
|
-
* {@link isTerminalDispatchFailure}, which is what the drain now asks. This one answers only
|
|
35
|
-
* "may we quote this as the provider's words", and one of ours never may.
|
|
36
|
-
*/
|
|
37
|
-
export declare function isTerminalProviderError(error: unknown): boolean;
|
|
38
|
-
//# sourceMappingURL=provider-error.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"provider-error.d.ts","sourceRoot":"","sources":["../src/provider-error.ts"],"names":[],"mappings":"AAkBA;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAAiC,CAAC;AAE5F;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAItE;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGjE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D"}
|
package/dist/provider-error.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Is a failed outbound call worth trying again? (#618)
|
|
3
|
-
*
|
|
4
|
-
* The platform retries a connector delivery by re-draining its `connector:<provider>` intent,
|
|
5
|
-
* and until now every failure looked the same to that loop: a throw meant "transient", so a
|
|
6
|
-
* provider answering `409 requires valid personal number field` was retried a hundred times
|
|
7
|
-
* over two days. That answer is not a fault to wait out — it is the provider telling the
|
|
8
|
-
* CALLER its request is wrong, and attempt 101 carries the identical bytes.
|
|
9
|
-
*
|
|
10
|
-
* The rule lives in one place rather than in any one connector because both ends need it
|
|
11
|
-
* (in the kernel, moving to `control-plane-api` beside the drain, #1978):
|
|
12
|
-
* a connector decides what status to raise, the drain decides whether to keep the intent
|
|
13
|
-
* drainable, and they must not disagree. It is deliberately STRUCTURAL — any error carrying a
|
|
14
|
-
* numeric `status` — so the drain never has to import a provider's error class to classify it,
|
|
15
|
-
* and a connector gets the behaviour by raising the status it already knows.
|
|
16
|
-
*/
|
|
17
|
-
import { isSubstratError } from '@substrat-run/contracts';
|
|
18
|
-
/**
|
|
19
|
-
* 4xx statuses that are nevertheless worth retrying: the provider is refusing this attempt,
|
|
20
|
-
* not the request. Timeouts and locks clear on their own; a rate limit is an instruction to
|
|
21
|
-
* come back later, which is exactly what the next drain pass does.
|
|
22
|
-
*/
|
|
23
|
-
export const RETRYABLE_CLIENT_STATUSES = new Set([408, 423, 425, 429]);
|
|
24
|
-
/**
|
|
25
|
-
* The HTTP status an error carries, if it carries one. Structural on purpose (see above):
|
|
26
|
-
* `ScriveApiError` and any future connector's error type both satisfy it without a shared base
|
|
27
|
-
* class, and an error that carries no status — a network failure, a bug — reads as `undefined`,
|
|
28
|
-
* which is the honest answer and keeps it retryable.
|
|
29
|
-
*/
|
|
30
|
-
export function providerErrorStatus(error) {
|
|
31
|
-
if (typeof error !== 'object' || error === null)
|
|
32
|
-
return undefined;
|
|
33
|
-
const status = error.status;
|
|
34
|
-
return typeof status === 'number' && Number.isFinite(status) ? status : undefined;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Is this delivery over? — the DRAIN's question, and the only one that decides retries.
|
|
38
|
-
*
|
|
39
|
-
* A non-retryable 4xx, whoever raised it: attempt 101 carries the identical bytes past the
|
|
40
|
-
* identical check. Deliberately blind to WHO refused, because terminality does not depend on
|
|
41
|
-
* it — our own `validation_failed` is as final as the provider's 409, and both statuses come
|
|
42
|
-
* from the same structural read (`SubstratError` carries its catalog status; a connector's
|
|
43
|
-
* error carries the provider's).
|
|
44
|
-
*/
|
|
45
|
-
export function isTerminalDispatchFailure(error) {
|
|
46
|
-
const status = providerErrorStatus(error);
|
|
47
|
-
return status !== undefined && status >= 400 && status < 500 && !RETRYABLE_CLIENT_STATUSES.has(status);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* The PROVIDER refused the request — a 4xx that came back over the wire, not one we raised.
|
|
51
|
-
*
|
|
52
|
-
* The exclusion is the whole point (#841). This predicate reads a bare numeric `status`, and
|
|
53
|
-
* every `SubstratError` carries one from the problem catalog — so a `permission denied:
|
|
54
|
-
* protocol:read` raised on our own side of egress answered `true` here, and the drain
|
|
55
|
-
* captioned it "a client error the provider will refuse identically on retry". Scrive never
|
|
56
|
-
* saw that request. An operator who reads that sentence goes and audits their Scrive account.
|
|
57
|
-
*
|
|
58
|
-
* Terminality was never the part that was wrong, so it did not move: see
|
|
59
|
-
* {@link isTerminalDispatchFailure}, which is what the drain now asks. This one answers only
|
|
60
|
-
* "may we quote this as the provider's words", and one of ours never may.
|
|
61
|
-
*/
|
|
62
|
-
export function isTerminalProviderError(error) {
|
|
63
|
-
return !isSubstratError(error) && isTerminalDispatchFailure(error);
|
|
64
|
-
}
|
|
65
|
-
//# sourceMappingURL=provider-error.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"provider-error.js","sourceRoot":"","sources":["../src/provider-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAwB,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAE5F;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAc;IAChD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAClE,MAAM,MAAM,GAAI,KAA8B,CAAC,MAAM,CAAC;IACtD,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AACpF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAc;IACtD,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAC1C,OAAO,MAAM,KAAK,SAAS,IAAI,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,IAAI,CAAC,yBAAyB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACzG,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAc;IACpD,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,yBAAyB,CAAC,KAAK,CAAC,CAAC;AACrE,CAAC"}
|