@substrat-run/control-plane-api 0.135.0 → 0.137.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/aggregate-reads.d.ts.map +1 -1
- package/dist/aggregate-reads.js +4 -1
- package/dist/aggregate-reads.js.map +1 -1
- package/dist/aggregate-source.d.ts +17 -1
- package/dist/aggregate-source.d.ts.map +1 -1
- package/dist/aggregate-source.js +16 -10
- package/dist/aggregate-source.js.map +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1030 -68
- package/dist/api.js.map +1 -1
- package/dist/cf-observability.d.ts +21 -0
- package/dist/cf-observability.d.ts.map +1 -1
- package/dist/cf-observability.js +150 -45
- package/dist/cf-observability.js.map +1 -1
- package/dist/connect-url.d.ts +2 -2
- package/dist/connect-url.d.ts.map +1 -1
- package/dist/connect-url.js +7 -1
- package/dist/connect-url.js.map +1 -1
- package/dist/connection-relay.d.ts +4 -2
- package/dist/connection-relay.d.ts.map +1 -1
- package/dist/connection-relay.js +10 -1
- package/dist/connection-relay.js.map +1 -1
- package/dist/deploy.d.ts +3 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +3 -4
- package/dist/deploy.js.map +1 -1
- package/dist/failure-attribution.js +1 -1
- package/dist/failure-attribution.js.map +1 -1
- package/dist/field-coverage-tally.d.ts +58 -0
- package/dist/field-coverage-tally.d.ts.map +1 -0
- package/dist/field-coverage-tally.js +234 -0
- package/dist/field-coverage-tally.js.map +1 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/observability.d.ts +20 -1
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +11 -1
- package/dist/observability.js.map +1 -1
- package/dist/platform-drain.d.ts +24 -2
- package/dist/platform-drain.d.ts.map +1 -1
- package/dist/platform-drain.js +36 -3
- package/dist/platform-drain.js.map +1 -1
- package/dist/platform-entry.d.ts +62 -6
- package/dist/platform-entry.d.ts.map +1 -1
- package/dist/platform-entry.generated.d.ts +8 -1
- package/dist/platform-entry.generated.d.ts.map +1 -1
- package/dist/platform-entry.generated.js +10 -3
- package/dist/platform-entry.generated.js.map +1 -1
- package/dist/platform-entry.js +151 -9
- package/dist/platform-entry.js.map +1 -1
- package/dist/provider-error.d.ts +9 -0
- package/dist/provider-error.d.ts.map +1 -0
- package/dist/provider-error.js +9 -0
- package/dist/provider-error.js.map +1 -0
- package/dist/reconcile.d.ts +10 -3
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/reconcile.js +7 -2
- package/dist/reconcile.js.map +1 -1
- package/dist/service-family.d.ts +19 -0
- package/dist/service-family.d.ts.map +1 -0
- package/dist/service-family.js +27 -0
- package/dist/service-family.js.map +1 -0
- package/dist/tenant-request-query.d.ts.map +1 -1
- package/dist/tenant-request-query.js +3 -1
- package/dist/tenant-request-query.js.map +1 -1
- package/dist/vertical-client.d.ts +191 -40
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js +351 -171
- package/dist/vertical-client.js.map +1 -1
- package/package.json +7 -7
package/dist/vertical-client.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
import { PREVIEW_CLIENT_PATH, attachmentRecord, denialFilterParams, capabilityFilterParams, capabilityPage, mintedPreviewClient, ownerSeat, ownerClaimLink, ownerTransferResult, previewClientClaim, retiredPreviewClients, peerGrantsEntry, peerSwitchOutcome, systemSwitchOutcome, systemScheduleEntry, switchedOffInUnit, exportedBatch, importResult, importCursorMoved, importState, substratError, } from '@substrat-run/contracts';
|
|
2
|
-
import {
|
|
1
|
+
import { PREVIEW_CLIENT_PATH, attachmentRecord, denialFilterParams, capabilityFilterParams, capabilityPage, mintedPreviewClient, ownerSeat, ownerClaimLink, ownerTransferResult, previewClientClaim, retiredPreviewClients, peerGrantsEntry, peerSwitchOutcome, systemSwitchOutcome, lifecycleDelivery, systemScheduleEntry, switchedOffInUnit, exportedBatch, importResult, importCursorMoved, importState, substratError, CONNECTOR_ATTACHMENT_RECORD_HEADER, LOAD_STAMP_HEADER, PLATFORM_SECRET_HEADER, WRITE_REVISION_HEADER, } from '@substrat-run/contracts';
|
|
2
|
+
import { undrainedEventsOf } from '@substrat-run/kernel';
|
|
3
3
|
import { ControlPlaneError } from '@substrat-run/control-plane-client';
|
|
4
|
+
/** `routeAnswer`'s word for a deployment that predates the route it was asked (#1722, #2010). */
|
|
5
|
+
const PREDATES = Symbol('predates');
|
|
6
|
+
/** #1722's kept-copy and fenced-wipe verbs: a host without the method answers a 501. */
|
|
7
|
+
const FENCED = { legacy501: true, lost: 'it may or may not have acted' };
|
|
8
|
+
/**
|
|
9
|
+
* What a 200 whose body is not JSON says, when it is an HTML page (#2010, Codex #2014 r1). An old
|
|
10
|
+
* deployment's SPA fallback answers its own app's `index.html` for a path it does not route, and a
|
|
11
|
+
* proxy or error page in between answers HTML too. Nothing the platform controls marks the first
|
|
12
|
+
* (the page is the vertical's, served by the asset layer before any platform code runs, and the
|
|
13
|
+
* deployments in question predate any marker that could be added now), so the two cannot be told
|
|
14
|
+
* apart, and an HTML page is never read as "predates the route".
|
|
15
|
+
*/
|
|
16
|
+
function htmlNote(res) {
|
|
17
|
+
return (res.headers.get('content-type') ?? '').toLowerCase().includes('text/html')
|
|
18
|
+
? " (an HTML page: an old deployment's app shell cannot be told from an error page in between)"
|
|
19
|
+
: '';
|
|
20
|
+
}
|
|
4
21
|
/**
|
|
5
22
|
* The query string for both internal denial reads. The filter's own fields come from
|
|
6
23
|
* the ONE encoder in contracts (#971) — this branch answers the same two routes as
|
|
@@ -319,6 +336,30 @@ export class VerticalClient {
|
|
|
319
336
|
async connectorGrant(input) {
|
|
320
337
|
await this.postInternal('/internal/connector-grant', input, 'connector-grant');
|
|
321
338
|
}
|
|
339
|
+
/**
|
|
340
|
+
* Deliver one scope's lifecycle to the deployment serving it (#1713). Throws on anything but
|
|
341
|
+
* a parsed answer, a deployment built before the route (404/501) included, so the caller
|
|
342
|
+
* records no receipt and the heal sweep asks again.
|
|
343
|
+
*/
|
|
344
|
+
async setLifecycle(input) {
|
|
345
|
+
const verb = 'lifecycle';
|
|
346
|
+
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
347
|
+
const res = await this.reach(verb, () => this.options.fetch(`${base}/internal/lifecycle`, {
|
|
348
|
+
method: 'POST',
|
|
349
|
+
headers: { 'content-type': 'application/json', [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
350
|
+
body: JSON.stringify(input),
|
|
351
|
+
}));
|
|
352
|
+
if (res.status === 404) {
|
|
353
|
+
throw new ControlPlaneError(501, `the deployment serving scope ${input.scopeId} predates the lifecycle delivery (#1713) — redeploy the vertical`);
|
|
354
|
+
}
|
|
355
|
+
if (!res.ok)
|
|
356
|
+
throw await this.refusal(verb, res);
|
|
357
|
+
const parsed = lifecycleDelivery.safeParse(await res.json().catch(() => null));
|
|
358
|
+
if (!parsed.success) {
|
|
359
|
+
throw new ControlPlaneError(502, `vertical answered ${verb} for scope ${input.scopeId} with an unexpected shape`);
|
|
360
|
+
}
|
|
361
|
+
return parsed.data;
|
|
362
|
+
}
|
|
322
363
|
/**
|
|
323
364
|
* Move one module's schedule kill switch in the deployment serving the scope (#1666) —
|
|
324
365
|
* the far end of `revokeFromSystem` / `restoreToSystem` for a hosted scope, whose
|
|
@@ -327,114 +368,83 @@ export class VerticalClient {
|
|
|
327
368
|
* ONE shape is normalized to "redeploy the vertical", and only because it is the
|
|
328
369
|
* deployment's own proof that it cannot have acted: a script built before the route
|
|
329
370
|
* answers a **404** (the path does not exist — the route itself answers a module it holds
|
|
330
|
-
* nothing for with a 200 `held: false`, never a 404)
|
|
331
|
-
*
|
|
371
|
+
* nothing for with a 200 `held: false`, never a 404). "Nothing was switched" is true of exactly
|
|
372
|
+
* that. An HTML 200 is NOT that proof (`routeAnswer`): an old deployment's app shell cannot be
|
|
373
|
+
* told from an error page after a switch that moved.
|
|
332
374
|
*
|
|
333
375
|
* Everything else surfaces as the failure it is, and never claims that: a transport
|
|
334
|
-
* failure (`reach` → 502 "unreachable"), a genuine 5xx from the far end,
|
|
335
|
-
*
|
|
336
|
-
* answer was lost, so the only honest
|
|
376
|
+
* failure (`reach` → 502 "unreachable"), a genuine 5xx from the far end, a 200 whose body
|
|
377
|
+
* is truncated or fails to read (#2010), and a 200 JSON of the wrong shape. The request may
|
|
378
|
+
* have landed and the switch may have moved before the answer was lost, so the only honest
|
|
379
|
+
* instruction is to confirm the position first.
|
|
337
380
|
*/
|
|
338
381
|
async systemSwitch(input) {
|
|
339
382
|
const verb = 'system-switch';
|
|
340
|
-
const
|
|
341
|
-
`
|
|
383
|
+
const lost = `the switch on scope ${input.scopeId} may or may not have moved. ` +
|
|
384
|
+
`Confirm its position (read the scope's schedule status) before retrying.`;
|
|
342
385
|
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
343
|
-
const
|
|
386
|
+
const rule = {
|
|
387
|
+
legacy501: false,
|
|
388
|
+
lost,
|
|
389
|
+
predates: `the deployment serving scope ${input.scopeId} predates the schedule switch (#1666) — ` +
|
|
390
|
+
`redeploy the vertical, then retry. Nothing was switched.`,
|
|
391
|
+
shape: `vertical answered ${verb} with an unexpected shape — ${lost}`,
|
|
392
|
+
};
|
|
393
|
+
return this.parsedAnswer(verb, rule, systemSwitchOutcome, () => this.options.fetch(`${base}/internal/system-switch`, {
|
|
344
394
|
method: 'POST',
|
|
345
395
|
headers: { 'content-type': 'application/json', [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
346
396
|
body: JSON.stringify(input),
|
|
347
397
|
}));
|
|
348
|
-
if (res.status === 404)
|
|
349
|
-
throw predates();
|
|
350
|
-
if (!res.ok)
|
|
351
|
-
throw await this.refusal(verb, res);
|
|
352
|
-
const text = await res.text();
|
|
353
|
-
let raw;
|
|
354
|
-
try {
|
|
355
|
-
raw = JSON.parse(text);
|
|
356
|
-
}
|
|
357
|
-
catch {
|
|
358
|
-
throw predates();
|
|
359
|
-
}
|
|
360
|
-
const parsed = systemSwitchOutcome.safeParse(raw);
|
|
361
|
-
if (!parsed.success) {
|
|
362
|
-
throw new ControlPlaneError(502, `vertical answered ${verb} with an unexpected shape — the switch on scope ${input.scopeId} may ` +
|
|
363
|
-
`or may not have moved. Confirm its position (the scope's SQL console) before retrying.`);
|
|
364
|
-
}
|
|
365
|
-
return parsed.data;
|
|
366
398
|
}
|
|
367
399
|
/**
|
|
368
400
|
* The read half of the schedule kill switch's status (#1674): every module the
|
|
369
401
|
* deployment serving this scope holds or has held system authority for, and where each
|
|
370
|
-
* stands. Mirrors `systemSwitch`'s seam and its skew rule exactly (
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* passes through `refusal` verbatim, unchanged either way.
|
|
402
|
+
* stands. Mirrors `systemSwitch`'s seam and its skew rule exactly (`routeAnswer`): a 404
|
|
403
|
+
* is the deployment's own proof it predates this read, and becomes a 501 that says so. A
|
|
404
|
+
* truncated, unreadable or HTML 200 (#2010) and a 200 of the wrong-shaped JSON are NOT that
|
|
405
|
+
* proof — they are a failed read, a 502, rather than a guess at
|
|
406
|
+
* "redeploy". The far end's own 501 (the route exists, the method doesn't) passes through
|
|
407
|
+
* `refusal` verbatim, unchanged either way.
|
|
377
408
|
*/
|
|
378
409
|
async systemGrantsStatus(input) {
|
|
379
410
|
const verb = 'system-grants';
|
|
380
|
-
const
|
|
381
|
-
`redeploy the vertical, then retry.`);
|
|
411
|
+
const lost = `the status of scope ${input.scopeId} could not be read. Nothing was changed; retry the read.`;
|
|
382
412
|
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
383
|
-
const
|
|
413
|
+
const rule = {
|
|
414
|
+
legacy501: false,
|
|
415
|
+
lost,
|
|
416
|
+
predates: `the deployment serving scope ${input.scopeId} predates the schedule switch's status read (#1674) — ` +
|
|
417
|
+
`redeploy the vertical, then retry.`,
|
|
418
|
+
shape: `vertical answered ${verb} with an unexpected shape for scope ${input.scopeId}.`,
|
|
419
|
+
};
|
|
420
|
+
return this.parsedAnswer(verb, rule, systemScheduleEntry.array(), () => this.options.fetch(`${base}/internal/system-grants?scopeId=${encodeURIComponent(input.scopeId)}`, {
|
|
384
421
|
method: 'GET',
|
|
385
422
|
headers: { [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
386
423
|
}));
|
|
387
|
-
if (res.status === 404)
|
|
388
|
-
throw predates();
|
|
389
|
-
if (!res.ok)
|
|
390
|
-
throw await this.refusal(verb, res);
|
|
391
|
-
const text = await res.text();
|
|
392
|
-
let raw;
|
|
393
|
-
try {
|
|
394
|
-
raw = JSON.parse(text);
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
throw predates();
|
|
398
|
-
}
|
|
399
|
-
const parsed = systemScheduleEntry.array().safeParse(raw);
|
|
400
|
-
if (!parsed.success) {
|
|
401
|
-
throw new ControlPlaneError(502, `vertical answered ${verb} with an unexpected shape for scope ${input.scopeId}.`);
|
|
402
|
-
}
|
|
403
|
-
return parsed.data;
|
|
404
424
|
}
|
|
405
425
|
/**
|
|
406
426
|
* The read half of the peer kill switch's status (#1706): every peer the deployment
|
|
407
427
|
* serving this scope holds or has held grants for, and where each stands. Mirrors
|
|
408
|
-
* `peerSwitch`'s seam and `systemGrantsStatus`'s skew rule exactly — a 404
|
|
409
|
-
*
|
|
410
|
-
*
|
|
428
|
+
* `peerSwitch`'s seam and `systemGrantsStatus`'s skew rule exactly — a 404 is the
|
|
429
|
+
* deployment's own proof it predates this read and becomes a 501 that says to redeploy; a
|
|
430
|
+
* truncated, unreadable, HTML or wrong-shaped 200 is not that proof and surfaces as the 502
|
|
431
|
+
* it is.
|
|
411
432
|
*/
|
|
412
433
|
async peerGrantsStatus(input) {
|
|
413
434
|
const verb = 'peer-grants';
|
|
414
|
-
const
|
|
415
|
-
`redeploy the vertical, then retry.`);
|
|
435
|
+
const lost = `the peer status of scope ${input.scopeId} could not be read. Nothing was changed; retry the read.`;
|
|
416
436
|
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
417
|
-
const
|
|
437
|
+
const rule = {
|
|
438
|
+
legacy501: false,
|
|
439
|
+
lost,
|
|
440
|
+
predates: `the deployment serving scope ${input.scopeId} predates the peer switch's status read (#1706) — ` +
|
|
441
|
+
`redeploy the vertical, then retry.`,
|
|
442
|
+
shape: `vertical answered ${verb} with an unexpected shape for scope ${input.scopeId}.`,
|
|
443
|
+
};
|
|
444
|
+
return this.parsedAnswer(verb, rule, peerGrantsEntry.array(), () => this.options.fetch(`${base}/internal/peer-grants?scopeId=${encodeURIComponent(input.scopeId)}`, {
|
|
418
445
|
method: 'GET',
|
|
419
446
|
headers: { [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
420
447
|
}));
|
|
421
|
-
if (res.status === 404)
|
|
422
|
-
throw predates();
|
|
423
|
-
if (!res.ok)
|
|
424
|
-
throw await this.refusal(verb, res);
|
|
425
|
-
const text = await res.text();
|
|
426
|
-
let raw;
|
|
427
|
-
try {
|
|
428
|
-
raw = JSON.parse(text);
|
|
429
|
-
}
|
|
430
|
-
catch {
|
|
431
|
-
throw predates();
|
|
432
|
-
}
|
|
433
|
-
const parsed = peerGrantsEntry.array().safeParse(raw);
|
|
434
|
-
if (!parsed.success) {
|
|
435
|
-
throw new ControlPlaneError(502, `vertical answered ${verb} with an unexpected shape for scope ${input.scopeId}.`);
|
|
436
|
-
}
|
|
437
|
-
return parsed.data;
|
|
438
448
|
}
|
|
439
449
|
/**
|
|
440
450
|
* Move one PEER's kill switch in the deployment serving the scope (#1706) — the far end
|
|
@@ -442,40 +452,30 @@ export class VerticalClient {
|
|
|
442
452
|
* live there and nowhere the shared control plane can reach.
|
|
443
453
|
*
|
|
444
454
|
* The seam, the skew rule and the honesty rule are `systemSwitch`'s, for the same reasons:
|
|
445
|
-
* a **404** (no such route)
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
* request may have landed and the switch may have moved before the
|
|
455
|
+
* a **404** (no such route) is the deployment's own proof that it
|
|
456
|
+
* predates this route and therefore cannot have switched anything — the route itself
|
|
457
|
+
* answers a peer the scope holds nothing for with a 200 `held: false`, never a 404.
|
|
458
|
+
* Everything else, a truncated or unreadable 200 included (#2010), surfaces as the failure
|
|
459
|
+
* it is, because the request may have landed and the switch may have moved before the
|
|
460
|
+
* answer was lost.
|
|
450
461
|
*/
|
|
451
462
|
async peerSwitch(input) {
|
|
452
463
|
const verb = 'peer-switch';
|
|
453
|
-
const
|
|
454
|
-
`
|
|
464
|
+
const lost = `the switch for peer '${input.vertical}' on scope ${input.scopeId} may or may not have moved. ` +
|
|
465
|
+
`Confirm its position (read the scope's peer status) before retrying.`;
|
|
455
466
|
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
456
|
-
const
|
|
467
|
+
const rule = {
|
|
468
|
+
legacy501: false,
|
|
469
|
+
lost,
|
|
470
|
+
predates: `the deployment serving scope ${input.scopeId} predates the peer kill switch (#1706) — ` +
|
|
471
|
+
`redeploy the vertical, then retry. Nothing was switched.`,
|
|
472
|
+
shape: `vertical answered ${verb} with an unexpected shape — ${lost}`,
|
|
473
|
+
};
|
|
474
|
+
return this.parsedAnswer(verb, rule, peerSwitchOutcome, () => this.options.fetch(`${base}/internal/peer-switch`, {
|
|
457
475
|
method: 'POST',
|
|
458
476
|
headers: { 'content-type': 'application/json', [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
459
477
|
body: JSON.stringify(input),
|
|
460
478
|
}));
|
|
461
|
-
if (res.status === 404)
|
|
462
|
-
throw predates();
|
|
463
|
-
if (!res.ok)
|
|
464
|
-
throw await this.refusal(verb, res);
|
|
465
|
-
const text = await res.text();
|
|
466
|
-
let raw;
|
|
467
|
-
try {
|
|
468
|
-
raw = JSON.parse(text);
|
|
469
|
-
}
|
|
470
|
-
catch {
|
|
471
|
-
throw predates();
|
|
472
|
-
}
|
|
473
|
-
const parsed = peerSwitchOutcome.safeParse(raw);
|
|
474
|
-
if (!parsed.success) {
|
|
475
|
-
throw new ControlPlaneError(502, `vertical answered ${verb} with an unexpected shape — the switch for peer '${input.vertical}' on ` +
|
|
476
|
-
`scope ${input.scopeId} may or may not have moved. Confirm its position before retrying.`);
|
|
477
|
-
}
|
|
478
|
-
return parsed.data;
|
|
479
479
|
}
|
|
480
480
|
/**
|
|
481
481
|
* A preview's own client at a team auth-server (#1704, `@substrat-run/contracts`'
|
|
@@ -483,13 +483,14 @@ export class VerticalClient {
|
|
|
483
483
|
* here, delete the preview's clients. Addressed to the ISSUER's deployment (this client),
|
|
484
484
|
* never the preview's.
|
|
485
485
|
*
|
|
486
|
-
* The skew rule is `
|
|
486
|
+
* The skew rule is `routeAnswer`'s with `legacy501`: a deployment built before these
|
|
487
487
|
* routes answers its `/internal/*` fallback — a JSON **501** on the auth-server — or a
|
|
488
|
-
* **404
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
* shape
|
|
492
|
-
* 409 a parent it does not claim) passes through
|
|
488
|
+
* **404**. Both are the deployment's own proof it cannot have acted,
|
|
489
|
+
* and become a 501 that says to redeploy the auth server. The caller must never read that
|
|
490
|
+
* as "the parent does not sign in here". A truncated or unreadable 200 (#2010) and a 200 of
|
|
491
|
+
* the wrong shape are not that proof and surface as a 502: a mint or a retire may have run.
|
|
492
|
+
* A refusal (403 another tenant's issuer, 409 a parent it does not claim) passes through
|
|
493
|
+
* verbatim.
|
|
493
494
|
*
|
|
494
495
|
* The mint's answer carries a client SECRET. It is returned to the caller and nothing here
|
|
495
496
|
* keeps, logs or quotes it: no error message on this path includes a response body that
|
|
@@ -505,31 +506,19 @@ export class VerticalClient {
|
|
|
505
506
|
return this.previewClientCall('DELETE', PREVIEW_CLIENT_PATH, input, retiredPreviewClients, 'preview-client retire', input.scopeId);
|
|
506
507
|
}
|
|
507
508
|
async previewClientCall(method, path, body, schema, verb, issuerScopeId) {
|
|
508
|
-
const predates = () => new ControlPlaneError(501, `the auth server serving scope ${issuerScopeId} predates preview clients (#1704) — redeploy it, ` +
|
|
509
|
-
`then re-run. Nothing was minted or deleted there.`);
|
|
510
509
|
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
511
|
-
const
|
|
510
|
+
const rule = {
|
|
511
|
+
legacy501: true,
|
|
512
|
+
lost: `the auth server serving scope ${issuerScopeId} may or may not have acted on it.`,
|
|
513
|
+
predates: `the auth server serving scope ${issuerScopeId} predates preview clients (#1704) — redeploy it, ` +
|
|
514
|
+
`then re-run. Nothing was minted or deleted there.`,
|
|
515
|
+
shape: `auth server answered ${verb} with an unexpected shape for scope ${issuerScopeId}.`,
|
|
516
|
+
};
|
|
517
|
+
return this.parsedAnswer(verb, rule, schema, () => this.options.fetch(`${base}${path}`, {
|
|
512
518
|
method,
|
|
513
519
|
headers: { 'content-type': 'application/json', [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
514
520
|
body: JSON.stringify(body),
|
|
515
521
|
}));
|
|
516
|
-
if (res.status === 404 || res.status === 501)
|
|
517
|
-
throw predates();
|
|
518
|
-
if (!res.ok)
|
|
519
|
-
throw await this.refusal(verb, res);
|
|
520
|
-
const text = await res.text();
|
|
521
|
-
let raw;
|
|
522
|
-
try {
|
|
523
|
-
raw = JSON.parse(text);
|
|
524
|
-
}
|
|
525
|
-
catch {
|
|
526
|
-
throw predates();
|
|
527
|
-
}
|
|
528
|
-
const parsed = schema.safeParse(raw);
|
|
529
|
-
if (!parsed.success) {
|
|
530
|
-
throw new ControlPlaneError(502, `auth server answered ${verb} with an unexpected shape for scope ${issuerScopeId}.`);
|
|
531
|
-
}
|
|
532
|
-
return parsed.data;
|
|
533
522
|
}
|
|
534
523
|
/** Journal a platform-request outcome back in the vertical after the platform ran it. */
|
|
535
524
|
async settlePlatformRequest(tenantId, scopeId, id, outcome) {
|
|
@@ -562,6 +551,19 @@ export class VerticalClient {
|
|
|
562
551
|
async exportScope(scopeId) {
|
|
563
552
|
return this.getInternal(`/internal/export?scopeId=${encodeURIComponent(scopeId)}`);
|
|
564
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* `exportScope` with the store's load stamp (#1722), read by the vertical in the same call as
|
|
556
|
+
* the dump and handed over in a header: what a carry's fenced wipe of the copy it leaves
|
|
557
|
+
* expects. Null from a deployment that predates the stamp, which cannot fence a wipe either.
|
|
558
|
+
* A store nothing has stamped is stamped by this read.
|
|
559
|
+
*/
|
|
560
|
+
async exportScopeStamped(scopeId) {
|
|
561
|
+
const path = `/internal/export?scopeId=${encodeURIComponent(scopeId)}&stamp=1`;
|
|
562
|
+
const res = await this.getInternalResponse(path);
|
|
563
|
+
const loadStamp = res.headers.get(LOAD_STAMP_HEADER) || null;
|
|
564
|
+
const revision = res.headers.get(WRITE_REVISION_HEADER) || null;
|
|
565
|
+
return { tables: await this.parseInternal('introspection', path, res), loadStamp, revision };
|
|
566
|
+
}
|
|
565
567
|
/**
|
|
566
568
|
* The write half of `exportScope` — load a dump into one existing scope in this
|
|
567
569
|
* deployment (drop-then-replay), for the governed restore/backout. The control-plane
|
|
@@ -576,7 +578,12 @@ export class VerticalClient {
|
|
|
576
578
|
/** #1742: the recorded-off modules, switched off on THIS scope in the restore's own event.
|
|
577
579
|
* #1869: `sourceScopeId`, the scope the tables were captured from, so the vertical re-points
|
|
578
580
|
* exactly that scope's grants, and `exact` when the platform exported them itself. A
|
|
579
|
-
* vertical that predates the fields ignores them.
|
|
581
|
+
* vertical that predates the fields ignores them. #1722: `loadStamp`, the stamp a carry
|
|
582
|
+
* leaves on the copy it lands, which a later fenced wipe of that copy expects, and `expect`,
|
|
583
|
+
* the marker the carry read here (`loadMarker`): the load is refused if the store moved since.
|
|
584
|
+
* #2005: `markCopy` — the directory's classification of this scope, sent when it is not
|
|
585
|
+
* primary, so the vertical marks it a copy in its own storage (and refuses a primary one).
|
|
586
|
+
* A vertical that predates the field ignores it. */
|
|
580
587
|
opts) {
|
|
581
588
|
// `exact` vouches for a named source; the vertical refuses it without one, so say so here.
|
|
582
589
|
if (opts?.exact && !opts.sourceScopeId) {
|
|
@@ -587,11 +594,194 @@ export class VerticalClient {
|
|
|
587
594
|
scopeId,
|
|
588
595
|
tables,
|
|
589
596
|
...(opts?.switchedOff ? { switchedOff: opts.switchedOff } : {}),
|
|
597
|
+
...(opts?.tenantHeld ? { tenantHeld: opts.tenantHeld } : {}),
|
|
590
598
|
...(opts?.sourceScopeId ? { sourceScopeId: opts.sourceScopeId } : {}),
|
|
591
599
|
...(opts?.exact ? { exact: true } : {}),
|
|
600
|
+
...(opts?.loadStamp ? { loadStamp: opts.loadStamp } : {}),
|
|
601
|
+
...(opts?.expect ? { expect: opts.expect } : {}),
|
|
602
|
+
...(opts?.markCopy ? { markCopy: opts.markCopy } : {}),
|
|
592
603
|
}, 'restore');
|
|
593
604
|
return { tables: count, ...switchedOffFrom(switchedOff) };
|
|
594
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* Wipe the copy a carry left in this deployment (#1722), only if nothing was loaded into the
|
|
608
|
+
* scope since `expectLoadStamp` (null: a store no load has stamped). `wiped: false` is the
|
|
609
|
+
* vertical's refusal: something was restored there since, and it stays.
|
|
610
|
+
*
|
|
611
|
+
* `'unfenced'` is a deployment that cannot compare the stamp, and the only answer that lets
|
|
612
|
+
* the caller fall back to an unconditional wipe: a 404 (built before the route), a 501 (a host
|
|
613
|
+
* without the method) are the deployment's own proof that it wiped nothing (`routeAnswer`).
|
|
614
|
+
* Everything else, a truncated or malformed JSON answer and any HTML page included, surfaces as
|
|
615
|
+
* the failure it is: the wipe may or may not have run.
|
|
616
|
+
*/
|
|
617
|
+
async wipeCarriedCopy(input) {
|
|
618
|
+
const verb = 'wipe-carried';
|
|
619
|
+
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
620
|
+
const answer = await this.routeAnswer(verb, FENCED, () => this.options.fetch(`${base}/internal/wipe-carried`, {
|
|
621
|
+
method: 'POST',
|
|
622
|
+
headers: { 'content-type': 'application/json', [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
623
|
+
body: JSON.stringify(input),
|
|
624
|
+
}));
|
|
625
|
+
if (answer === PREDATES)
|
|
626
|
+
return 'unfenced';
|
|
627
|
+
const wiped = answer?.wiped;
|
|
628
|
+
if (typeof wiped !== 'boolean') {
|
|
629
|
+
throw new ControlPlaneError(502, `vertical answered ${verb} with an unexpected shape — the copy of scope ${input.scopeId} may or may not be wiped`);
|
|
630
|
+
}
|
|
631
|
+
return { wiped };
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* What a carry's restore into this scope expects to find unchanged (#1722): the load stamp and
|
|
635
|
+
* the store's write revision. `'unfenced'` on `wipeCarriedCopy`'s terms: only the
|
|
636
|
+
* deployment's own answer that it predates the route; everything else is a failure.
|
|
637
|
+
*/
|
|
638
|
+
async loadMarker(scopeId) {
|
|
639
|
+
const verb = 'load-marker';
|
|
640
|
+
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
641
|
+
const answer = await this.routeAnswer(verb, FENCED, () => this.options.fetch(`${base}/internal/load-marker?scopeId=${encodeURIComponent(scopeId)}`, {
|
|
642
|
+
headers: { [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
643
|
+
}));
|
|
644
|
+
if (answer === PREDATES)
|
|
645
|
+
return 'unfenced';
|
|
646
|
+
const { loadStamp, revision } = (answer ?? {});
|
|
647
|
+
const field = (v) => v === null || (typeof v === 'string' && v.length > 0);
|
|
648
|
+
if (!field(loadStamp) || !field(revision)) {
|
|
649
|
+
throw new ControlPlaneError(502, `vertical answered ${verb} for scope ${scopeId} with an unexpected shape`);
|
|
650
|
+
}
|
|
651
|
+
return { loadStamp: loadStamp, revision: revision };
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* The kept copy of a scope in this deployment (#1722), or null. A deployment built before the
|
|
655
|
+
* fenced wipe cannot have made one, so its "predates" answer reads as null too.
|
|
656
|
+
*/
|
|
657
|
+
async keptCopy(scopeId) {
|
|
658
|
+
const verb = 'kept-copy';
|
|
659
|
+
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
660
|
+
const answer = await this.routeAnswer(verb, FENCED, () => this.options.fetch(`${base}/internal/kept-copy?scopeId=${encodeURIComponent(scopeId)}`, {
|
|
661
|
+
headers: { [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
662
|
+
}));
|
|
663
|
+
if (answer === PREDATES)
|
|
664
|
+
return null;
|
|
665
|
+
const kept = answer?.kept;
|
|
666
|
+
if (kept === null)
|
|
667
|
+
return null;
|
|
668
|
+
const k = kept;
|
|
669
|
+
if (!k || typeof k.carriedTo !== 'string' || typeof k.keptAt !== 'string' || !(k.revision === null || typeof k.revision === 'string')) {
|
|
670
|
+
throw new ControlPlaneError(502, `vertical answered ${verb} for scope ${scopeId} with an unexpected shape`);
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
carriedTo: k.carriedTo,
|
|
674
|
+
keptAt: k.keptAt,
|
|
675
|
+
revision: k.revision,
|
|
676
|
+
...(k.leftAgain && typeof k.leftAgain.to === 'string' && typeof k.leftAgain.at === 'string'
|
|
677
|
+
? { leftAgain: { to: k.leftAgain.to, at: k.leftAgain.at } }
|
|
678
|
+
: {}),
|
|
679
|
+
...(k.clearedOnly && (k.clearedOnly.revision === null || typeof k.clearedOnly.revision === 'string')
|
|
680
|
+
? { clearedOnly: { revision: k.clearedOnly.revision } }
|
|
681
|
+
: {}),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
/** Release the marker of a kept copy that is the live store after all (#1722), at the revision read. */
|
|
685
|
+
async releaseKeptCopy(input) {
|
|
686
|
+
const verb = 'kept-copy-release';
|
|
687
|
+
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
688
|
+
const answer = await this.routeAnswer(verb, FENCED, () => this.options.fetch(`${base}/internal/kept-copy/release`, {
|
|
689
|
+
method: 'POST',
|
|
690
|
+
headers: { 'content-type': 'application/json', [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
691
|
+
body: JSON.stringify(input),
|
|
692
|
+
}));
|
|
693
|
+
if (answer === PREDATES) {
|
|
694
|
+
throw new ControlPlaneError(501, `the deployment holding scope ${input.scopeId} keeps no copies (#1722)`);
|
|
695
|
+
}
|
|
696
|
+
const a = answer;
|
|
697
|
+
if (a?.released === true)
|
|
698
|
+
return { released: true };
|
|
699
|
+
if (a?.refused === 'changed' || a?.refused === 'not-kept')
|
|
700
|
+
return { refused: a.refused };
|
|
701
|
+
throw new ControlPlaneError(502, `vertical answered ${verb} with an unexpected shape — the marker may or may not be cleared`);
|
|
702
|
+
}
|
|
703
|
+
/** Discard the kept copy of a scope in this deployment (#1722), at the revision the operator read. */
|
|
704
|
+
async discardKeptCopy(input) {
|
|
705
|
+
const verb = 'kept-copy-discard';
|
|
706
|
+
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
707
|
+
const answer = await this.routeAnswer(verb, FENCED, () => this.options.fetch(`${base}/internal/kept-copy/discard`, {
|
|
708
|
+
method: 'POST',
|
|
709
|
+
headers: { 'content-type': 'application/json', [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
710
|
+
body: JSON.stringify(input),
|
|
711
|
+
}));
|
|
712
|
+
if (answer === PREDATES) {
|
|
713
|
+
throw new ControlPlaneError(501, `the deployment holding scope ${input.scopeId} keeps no copies (#1722)`);
|
|
714
|
+
}
|
|
715
|
+
const a = answer;
|
|
716
|
+
if (a?.discarded === true)
|
|
717
|
+
return { discarded: true };
|
|
718
|
+
if (a?.refused === 'changed' || a?.refused === 'not-kept')
|
|
719
|
+
return { refused: a.refused };
|
|
720
|
+
throw new ControlPlaneError(502, `vertical answered ${verb} with an unexpected shape — the copy may or may not be discarded`);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* The ONE skew rule for a route a deployment may predate (#1722, #2010): `PREDATES` only for a
|
|
724
|
+
* status that says the route is not there — a 404, and a 501 where `rule.legacy501` says the far
|
|
725
|
+
* end's own fallback answers one. No body is that proof. Everything else is a failure, never
|
|
726
|
+
* `PREDATES`, because the request may have landed: a transport failure, a refusal, a body that
|
|
727
|
+
* fails to read, and a body that is not valid JSON — a truncated `{"held":` from a deployment
|
|
728
|
+
* that DID act, or an HTML page (`htmlNote`), which an old deployment's app shell and an error
|
|
729
|
+
* page after a switch that moved both are. Those last ones are a 502 that ends with
|
|
730
|
+
* `rule.lost`, the verb's own sentence for what a lost answer leaves unknown.
|
|
731
|
+
*/
|
|
732
|
+
async routeAnswer(verb, rule, request) {
|
|
733
|
+
const res = await this.reach(verb, request);
|
|
734
|
+
if (res.status === 404 || (rule.legacy501 && res.status === 501))
|
|
735
|
+
return PREDATES;
|
|
736
|
+
if (!res.ok)
|
|
737
|
+
throw await this.refusal(verb, res);
|
|
738
|
+
return this.readAnswer(verb, res, rule.lost);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* `routeAnswer` for a verb whose "predates" is always a 501 and whose answer has a schema:
|
|
742
|
+
* the parsed answer, or the 501 `rule.predates`, or the 502 `rule.shape` for a wrong shape.
|
|
743
|
+
*/
|
|
744
|
+
async parsedAnswer(verb, rule, schema, request) {
|
|
745
|
+
const answer = await this.routeAnswer(verb, rule, request);
|
|
746
|
+
if (answer === PREDATES)
|
|
747
|
+
throw new ControlPlaneError(501, rule.predates);
|
|
748
|
+
const parsed = schema.safeParse(answer);
|
|
749
|
+
if (!parsed.success)
|
|
750
|
+
throw new ControlPlaneError(502, rule.shape);
|
|
751
|
+
return parsed.data;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* A 200's body, read under #2010's rule: the JSON it carries. A body that fails to read, or is
|
|
755
|
+
* not JSON (an HTML page included, `htmlNote`), is a 502 that names `subject` and ends with
|
|
756
|
+
* `lost`, because the deployment that answered may have acted.
|
|
757
|
+
*/
|
|
758
|
+
async readAnswer(subject, res, lost) {
|
|
759
|
+
let text;
|
|
760
|
+
try {
|
|
761
|
+
text = await res.text();
|
|
762
|
+
}
|
|
763
|
+
catch (e) {
|
|
764
|
+
throw new ControlPlaneError(502, `reading the vertical's answer to ${subject} failed (${e instanceof Error ? e.message : String(e)}) — ${lost}`);
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
return JSON.parse(text);
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
throw new ControlPlaneError(502, `vertical answered ${subject} with a body that is not JSON${htmlNote(res)} — ${lost}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
/** Mark one scope a copy in the vertical's own storage (#2005), given the directory's
|
|
774
|
+
* classification of it; whether this call stamped it. */
|
|
775
|
+
async markCopy(scopeId, lineage) {
|
|
776
|
+
return this.postInternal('/internal/mark-copy', { scopeId, lineage }, 'mark-copy');
|
|
777
|
+
}
|
|
778
|
+
/** Remove a mistaken copy marker (#2005), given the directory's classification; whether it did. */
|
|
779
|
+
async clearCopyMark(scopeId, lineage,
|
|
780
|
+
/** #1722 (Codex #2008 r12–r13): clear only the store with this load stamp and revision; 412
|
|
781
|
+
* otherwise. */
|
|
782
|
+
opts = {}) {
|
|
783
|
+
return this.postInternal('/internal/clear-copy-mark', { scopeId, lineage, ...(opts.expect ? { expect: opts.expect } : {}) }, 'clear-copy-mark');
|
|
784
|
+
}
|
|
595
785
|
/** Facets over one scope's outbox (#1239) — through the vertical that holds the data. */
|
|
596
786
|
async facetEvents(scopeId, input) {
|
|
597
787
|
const q = new URLSearchParams({ scopeId });
|
|
@@ -787,21 +977,16 @@ export class VerticalClient {
|
|
|
787
977
|
: `vertical refused ${verb}: ${res.status} ${res.statusText}`);
|
|
788
978
|
}
|
|
789
979
|
/**
|
|
790
|
-
* A 200
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
980
|
+
* A 200 that is not JSON is a 502 that names the verb, instead of an unhandled SyntaxError →
|
|
981
|
+
* 500 (#389: a pre-#236 script has no `/internal/export`, and its SPA fallback answered the app
|
|
982
|
+
* shell). That shell cannot be told from an error page in between (`htmlNote`), and a truncated
|
|
983
|
+
* answer from a deployment that has the route may follow a write that ran (#2010), so the
|
|
984
|
+
* message says the call may or may not have acted, and names redeploying only as the remedy
|
|
985
|
+
* for the case it cannot prove.
|
|
795
986
|
*/
|
|
796
987
|
async parseInternal(verb, path, res) {
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
return JSON.parse(text);
|
|
800
|
-
}
|
|
801
|
-
catch {
|
|
802
|
-
throw new ControlPlaneError(502, `vertical answered ${verb} (${path}) with non-JSON — its deployed script predates this ` +
|
|
803
|
-
`surface. Redeploy the vertical (or, for a rebind, use abandonData).`);
|
|
804
|
-
}
|
|
988
|
+
return (await this.readAnswer(`${verb} (${path})`, res, 'it may or may not have acted. If this deployment answers its app for /internal/*, it predates ' +
|
|
989
|
+
'this surface: redeploy the vertical (or, for a rebind, use abandonData).'));
|
|
805
990
|
}
|
|
806
991
|
/** A platform-authenticated POST to the vertical's `/internal/*` surface. */
|
|
807
992
|
/**
|
|
@@ -827,10 +1012,11 @@ export class VerticalClient {
|
|
|
827
1012
|
*
|
|
828
1013
|
* The skew rule is `systemSwitch`'s, and here it is the whole point: an EMPTY batch is a
|
|
829
1014
|
* real answer (nothing new), so a deployment that cannot answer must never produce one. A
|
|
830
|
-
* 404 (the route does not exist)
|
|
831
|
-
*
|
|
1015
|
+
* 404 (the route does not exist) is that deployment's own proof (`routeAnswer`), and becomes a
|
|
1016
|
+
* 501 saying to redeploy.
|
|
832
1017
|
* The far end's own 501 (the route exists, the host method does not) passes through
|
|
833
|
-
* verbatim. A JSON 200 of the wrong shape
|
|
1018
|
+
* verbatim. A truncated or unreadable 200 (#2010) and a JSON 200 of the wrong shape are a
|
|
1019
|
+
* 502, never a guess.
|
|
834
1020
|
*/
|
|
835
1021
|
async exportedEvents(input) {
|
|
836
1022
|
return this.crossVerticalCall('exported-events', '/internal/exported-events', input.scopeId, exportedBatch, input);
|
|
@@ -856,8 +1042,8 @@ export class VerticalClient {
|
|
|
856
1042
|
/**
|
|
857
1043
|
* The replay lever's far end (#1705 PR 3): move the watermark of the consumer scope this
|
|
858
1044
|
* deployment serves. The same skew rule, plus the write's one difference: a JSON 200 of the
|
|
859
|
-
* wrong shape may follow a move that happened, so the
|
|
860
|
-
* pulling the lever again. A 404
|
|
1045
|
+
* wrong shape, or one truncated or unreadable (#2010), may follow a move that happened, so the
|
|
1046
|
+
* 502 says to read the edge before pulling the lever again. A 404 proves the route is absent, so nothing moved.
|
|
861
1047
|
*/
|
|
862
1048
|
async importCursorMove(input) {
|
|
863
1049
|
return this.crossVerticalCall('import-cursor', '/internal/import-cursor', input.scopeId, importCursorMoved, input, {
|
|
@@ -869,31 +1055,21 @@ export class VerticalClient {
|
|
|
869
1055
|
* of `body` when there is one, a GET otherwise.
|
|
870
1056
|
*/
|
|
871
1057
|
async crossVerticalCall(verb, path, scopeId, schema, body, opts = {}) {
|
|
872
|
-
const
|
|
873
|
-
|
|
1058
|
+
const lost = opts.write
|
|
1059
|
+
? 'the write may have taken effect: read the edge before retrying.'
|
|
1060
|
+
: 'it may or may not have acted; repeating it is safe.';
|
|
874
1061
|
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
875
1062
|
const secret = { [PLATFORM_SECRET_HEADER]: this.options.platformSecret };
|
|
876
|
-
const
|
|
1063
|
+
const rule = {
|
|
1064
|
+
legacy501: false,
|
|
1065
|
+
lost,
|
|
1066
|
+
predates: `the deployment serving scope ${scopeId} predates cross-vertical events (#1705) — redeploy the vertical. ` +
|
|
1067
|
+
`Nothing was read or delivered; the edge's watermark holds.`,
|
|
1068
|
+
shape: `vertical answered ${verb} with an unexpected shape for scope ${scopeId}` + (opts.write ? ` — ${lost}` : '.'),
|
|
1069
|
+
};
|
|
1070
|
+
return this.parsedAnswer(verb, rule, schema, () => this.options.fetch(`${base}${path}`, body === undefined
|
|
877
1071
|
? { method: 'GET', headers: secret }
|
|
878
1072
|
: { method: 'POST', headers: { ...secret, 'content-type': 'application/json' }, body: JSON.stringify(body) }));
|
|
879
|
-
if (res.status === 404)
|
|
880
|
-
throw predates();
|
|
881
|
-
if (!res.ok)
|
|
882
|
-
throw await this.refusal(verb, res);
|
|
883
|
-
const text = await res.text();
|
|
884
|
-
let raw;
|
|
885
|
-
try {
|
|
886
|
-
raw = JSON.parse(text);
|
|
887
|
-
}
|
|
888
|
-
catch {
|
|
889
|
-
throw predates();
|
|
890
|
-
}
|
|
891
|
-
const parsed = schema.safeParse(raw);
|
|
892
|
-
if (!parsed.success) {
|
|
893
|
-
throw new ControlPlaneError(502, `vertical answered ${verb} with an unexpected shape for scope ${scopeId}.` +
|
|
894
|
-
(opts.write ? ' The write may have taken effect: read the edge before retrying.' : ''));
|
|
895
|
-
}
|
|
896
|
-
return parsed.data;
|
|
897
1073
|
}
|
|
898
1074
|
async postInternal(path, body, verb) {
|
|
899
1075
|
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
@@ -911,13 +1087,17 @@ export class VerticalClient {
|
|
|
911
1087
|
}
|
|
912
1088
|
/** A platform-authenticated GET to the vertical's `/internal/*` surface. */
|
|
913
1089
|
async getInternal(path) {
|
|
1090
|
+
return this.parseInternal('introspection', path, await this.getInternalResponse(path));
|
|
1091
|
+
}
|
|
1092
|
+
/** `getInternal`'s request, answering the response itself, for a caller that reads a header too. */
|
|
1093
|
+
async getInternalResponse(path) {
|
|
914
1094
|
const base = this.options.baseUrl ?? 'https://vertical.invalid';
|
|
915
1095
|
const res = await this.reach('introspection', () => this.options.fetch(`${base}${path}`, {
|
|
916
1096
|
headers: { [PLATFORM_SECRET_HEADER]: this.options.platformSecret },
|
|
917
1097
|
}));
|
|
918
1098
|
if (!res.ok)
|
|
919
1099
|
throw await this.refusal('introspection', res);
|
|
920
|
-
return
|
|
1100
|
+
return res;
|
|
921
1101
|
}
|
|
922
1102
|
}
|
|
923
1103
|
//# sourceMappingURL=vertical-client.js.map
|