@substrat-run/control-plane-api 0.134.0 → 0.135.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.
Files changed (47) hide show
  1. package/dist/api.d.ts.map +1 -1
  2. package/dist/api.js +22 -2
  3. package/dist/api.js.map +1 -1
  4. package/dist/auth.d.ts +2 -13
  5. package/dist/auth.d.ts.map +1 -1
  6. package/dist/auth.js +4 -13
  7. package/dist/auth.js.map +1 -1
  8. package/dist/browser.d.ts +4 -4
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +6 -3
  11. package/dist/browser.js.map +1 -1
  12. package/dist/cf-observability.js +1 -1
  13. package/dist/cf-observability.js.map +1 -1
  14. package/dist/errors.d.ts.map +1 -1
  15. package/dist/errors.js +6 -4
  16. package/dist/errors.js.map +1 -1
  17. package/dist/failure-attribution.js +1 -1
  18. package/dist/failure-attribution.js.map +1 -1
  19. package/dist/index.d.ts +0 -4
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +0 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/platform-drain.js +1 -1
  24. package/dist/platform-drain.js.map +1 -1
  25. package/dist/preview-auth.js +1 -1
  26. package/dist/preview-auth.js.map +1 -1
  27. package/dist/staff-client.d.ts +4 -3
  28. package/dist/staff-client.d.ts.map +1 -1
  29. package/dist/staff-client.js +5 -2
  30. package/dist/staff-client.js.map +1 -1
  31. package/dist/vertical-client.d.ts +3 -1
  32. package/dist/vertical-client.d.ts.map +1 -1
  33. package/dist/vertical-client.js +8 -2
  34. package/dist/vertical-client.js.map +1 -1
  35. package/package.json +7 -6
  36. package/dist/client.d.ts +0 -111
  37. package/dist/client.d.ts.map +0 -1
  38. package/dist/client.js +0 -122
  39. package/dist/client.js.map +0 -1
  40. package/dist/identity-tenants.d.ts +0 -30
  41. package/dist/identity-tenants.d.ts.map +0 -1
  42. package/dist/identity-tenants.js +0 -23
  43. package/dist/identity-tenants.js.map +0 -1
  44. package/dist/transport.d.ts +0 -56
  45. package/dist/transport.d.ts.map +0 -1
  46. package/dist/transport.js +0 -88
  47. package/dist/transport.js.map +0 -1
package/dist/client.d.ts DELETED
@@ -1,111 +0,0 @@
1
- import type { EntitlementGrant, EntitlementGrantInput, QueryScopeInput, ReadScopeTableInput, Scope, ScopeId, ScopeQueryResult, DenialFilter, DenialSummary, PermissionDenial, ScopeTable, ScopeTablePage, Tenant, TenantId } from '@substrat-run/contracts';
2
- import { ControlPlaneError, ControlPlaneTransport } from './transport.js';
3
- import { type IdentityTenant } from './identity-tenants.js';
4
- /**
5
- * A typed HTTP client for the control-plane API — the vertical side of the
6
- * connect seam (first-flow.md slice 4).
7
- *
8
- * A vertical that runs against a *separately deployed* shared control plane uses
9
- * this to (a) register its tenant, entitlements, and scope on boot, and (b) gate
10
- * each request on the directory's authoritative lifecycle — `assertScopeActive`
11
- * fails closed exactly as the kernel's own `getScope` does, so a suspend in the
12
- * console bites the vertical's next operation even across process and deployment
13
- * boundaries.
14
- *
15
- * What it deliberately does NOT do: write roles or grants. Those are not on the
16
- * control-plane HTTP surface (api.ts §4.5 — permission writes are the human
17
- * checkpoint, D-22/D-29), so a connected vertical keeps its permission model
18
- * local and treats the shared plane as the authority for tenant/scope lifecycle
19
- * and entitlements only.
20
- *
21
- * `fetch` is injectable: pass a Worker service-binding's fetch, or the router's
22
- * own `app.fetch` for an in-process test, instead of the global.
23
- */
24
- export interface ControlPlaneClientOptions {
25
- /** Base URL of the control-plane API, e.g. `https://cp.example.com` or `http://127.0.0.1:8788`. */
26
- baseUrl: string;
27
- /**
28
- * The platform actor id stamped as the audit subject on every write — sent as the
29
- * dev-only `x-platform-actor` header, and ONLY when no `serviceToken` is set. With a
30
- * token the control plane resolves the subject from the token (its fixed service
31
- * actor), so the header is never consulted there and is not sent (#980).
32
- */
33
- actor: string;
34
- /**
35
- * A service credential (`x-service-token`) proving the caller is an authorized
36
- * vertical, not just anyone with an actor id. Required when the control plane
37
- * has real auth — the dev-actor header alone does not authenticate there. When
38
- * set, it is the request's only credential: `actor` is not sent.
39
- */
40
- serviceToken?: string;
41
- /** Defaults to the global `fetch`. */
42
- fetch?: typeof globalThis.fetch;
43
- }
44
- export { ControlPlaneError };
45
- export interface ClientProvisionScopeInput {
46
- tenantId: TenantId;
47
- scopeId: ScopeId;
48
- slug?: string;
49
- kind?: string;
50
- name?: string;
51
- vertical?: string | null;
52
- jurisdiction?: 'eu' | 'us' | 'global';
53
- }
54
- export declare class ControlPlaneClient extends ControlPlaneTransport {
55
- constructor(options: ControlPlaneClientOptions);
56
- createTenant(input: {
57
- id: TenantId;
58
- slug: string;
59
- name: string;
60
- }): Promise<Tenant>;
61
- grantEntitlement(tenantId: TenantId, key: string, plan?: EntitlementGrantInput): Promise<EntitlementGrant[]>;
62
- provisionScope(input: ClientProvisionScopeInput): Promise<Scope>;
63
- /**
64
- * Confirm the scope exists here, moving the directory row provisioning → active.
65
- *
66
- * In this (push) direction the vertical has already built the scope locally, so
67
- * registering and confirming are the same moment — but they stay two calls so
68
- * `provisionScope` means one thing everywhere, and so the directory is never the
69
- * one deciding a scope is ready (K-31).
70
- */
71
- activateScope(tenantId: TenantId, scopeId: ScopeId): Promise<Scope>;
72
- getTenant(tenantId: TenantId): Promise<Tenant | undefined>;
73
- getScopeRecord(tenantId: TenantId, scopeId: ScopeId): Promise<Scope | undefined>;
74
- listEntitlements(tenantId: TenantId): Promise<EntitlementGrant[]>;
75
- /**
76
- * The tenants a login builds for, each flagged with whether it holds the `builder`
77
- * entitlement — the builder studio's membership read (builder-plane.md §4). `externalId`
78
- * is the login's OIDC subject. Service-token gated: an unset token is refused by the
79
- * plane, never bypassed.
80
- *
81
- * The answer is PARSED, not asserted: a plane that renamed `entitled`, or answered an
82
- * `{ error }` body with a 200, would hand a cast `entitled: undefined` — falsy, so the
83
- * studio would lock the tenant out with the ordinary "not enabled" page and nothing
84
- * anywhere would say the directory had changed shape. A refused parse throws instead.
85
- * The message names the shape that was wrong, never the body — the body is directory
86
- * facts about a person's tenants.
87
- */
88
- identityTenants(externalId: string): Promise<IdentityTenant[]>;
89
- /** Every table in the scope's own database, with row counts (Data view). */
90
- listScopeTables(tenantId: TenantId, scopeId: ScopeId): Promise<ScopeTable[]>;
91
- /** A bounded page of one table of the scope's database. */
92
- readScopeTable(tenantId: TenantId, scopeId: ScopeId, input: ReadScopeTableInput): Promise<ScopeTablePage>;
93
- /** One read-only SQL statement against the scope's database — the console (#219). */
94
- queryScope(tenantId: TenantId, scopeId: ScopeId, input: QueryScopeInput): Promise<ScopeQueryResult>;
95
- /**
96
- * The scope's recorded permission refusals, bucketed per (actor, permission) with the
97
- * window's own facts beside them — the view to open first, because the raw log's
98
- * volume is attacker-influenceable and a prober can flood a newest-first page.
99
- */
100
- summarizeDenials(tenantId: TenantId, scopeId: ScopeId, filter?: DenialFilter): Promise<DenialSummary>;
101
- /** The raw rows behind a bucket, newest first. */
102
- listDenials(tenantId: TenantId, scopeId: ScopeId, filter?: DenialFilter): Promise<PermissionDenial[]>;
103
- /**
104
- * The gate. Throws unless the tenant is active AND the scope exists and is
105
- * active — the same fail-closed logic the kernel's `validateScopeAccess`
106
- * applies locally, so a tenant-level cascade suspend bites too, not just a
107
- * per-scope one. Call it before handing a request to the local scope host.
108
- */
109
- assertScopeActive(tenantId: TenantId, scopeId: ScopeId): Promise<void>;
110
- }
111
- //# sourceMappingURL=client.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js DELETED
@@ -1,122 +0,0 @@
1
- import { denialQuery } from '@substrat-run/contracts';
2
- import { ControlPlaneError, ControlPlaneTransport } from './transport.js';
3
- import { identityTenantsResponse } from './identity-tenants.js';
4
- // The error is raised by the transport; this is the path callers (and `errors.ts`)
5
- // have always imported it from.
6
- export { ControlPlaneError };
7
- export class ControlPlaneClient extends ControlPlaneTransport {
8
- constructor(options) {
9
- super(options);
10
- }
11
- // -- registration (idempotent, mirrors HostAdmin) --------------------------
12
- createTenant(input) {
13
- return this.call('/tenants', { method: 'POST', body: JSON.stringify(input) });
14
- }
15
- grantEntitlement(tenantId, key, plan) {
16
- return this.call(`/tenants/${tenantId}/entitlements/${key}`, {
17
- method: 'PUT',
18
- body: plan === undefined ? undefined : JSON.stringify(plan),
19
- });
20
- }
21
- provisionScope(input) {
22
- return this.call('/scopes', { method: 'POST', body: JSON.stringify(input) });
23
- }
24
- /**
25
- * Confirm the scope exists here, moving the directory row provisioning → active.
26
- *
27
- * In this (push) direction the vertical has already built the scope locally, so
28
- * registering and confirming are the same moment — but they stay two calls so
29
- * `provisionScope` means one thing everywhere, and so the directory is never the
30
- * one deciding a scope is ready (K-31).
31
- */
32
- activateScope(tenantId, scopeId) {
33
- return this.call(`/tenants/${tenantId}/scopes/${scopeId}/activate`, { method: 'POST' });
34
- }
35
- // -- reads used for gating -------------------------------------------------
36
- getTenant(tenantId) {
37
- return this.call(`/tenants/${tenantId}`, undefined, true);
38
- }
39
- getScopeRecord(tenantId, scopeId) {
40
- return this.call(`/tenants/${tenantId}/scopes/${scopeId}`, undefined, true);
41
- }
42
- listEntitlements(tenantId) {
43
- return this.call(`/tenants/${tenantId}/entitlements`);
44
- }
45
- /**
46
- * The tenants a login builds for, each flagged with whether it holds the `builder`
47
- * entitlement — the builder studio's membership read (builder-plane.md §4). `externalId`
48
- * is the login's OIDC subject. Service-token gated: an unset token is refused by the
49
- * plane, never bypassed.
50
- *
51
- * The answer is PARSED, not asserted: a plane that renamed `entitled`, or answered an
52
- * `{ error }` body with a 200, would hand a cast `entitled: undefined` — falsy, so the
53
- * studio would lock the tenant out with the ordinary "not enabled" page and nothing
54
- * anywhere would say the directory had changed shape. A refused parse throws instead.
55
- * The message names the shape that was wrong, never the body — the body is directory
56
- * facts about a person's tenants.
57
- */
58
- async identityTenants(externalId) {
59
- const res = await this.send('/internal/builder/identity-tenants', {
60
- method: 'POST',
61
- body: JSON.stringify({ externalId }),
62
- });
63
- const parsed = identityTenantsResponse.safeParse(await res.json().catch(() => null));
64
- if (!parsed.success) {
65
- throw new ControlPlaneError(res.status, `identity-tenants returned an unexpected shape: ${parsed.error.message}`);
66
- }
67
- return parsed.data.tenants;
68
- }
69
- // -- read-only scope-DB introspection (§5.4 admin-query RPC) ----------------
70
- /** Every table in the scope's own database, with row counts (Data view). */
71
- listScopeTables(tenantId, scopeId) {
72
- return this.call(`/tenants/${tenantId}/scopes/${scopeId}/tables`);
73
- }
74
- /** A bounded page of one table of the scope's database. */
75
- readScopeTable(tenantId, scopeId, input) {
76
- const q = new URLSearchParams({ limit: String(input.limit), offset: String(input.offset) });
77
- return this.call(`/tenants/${tenantId}/scopes/${scopeId}/tables/${encodeURIComponent(input.table)}?${q}`);
78
- }
79
- /** One read-only SQL statement against the scope's database — the console (#219). */
80
- queryScope(tenantId, scopeId, input) {
81
- return this.call(`/tenants/${tenantId}/scopes/${scopeId}/query`, {
82
- method: 'POST',
83
- body: JSON.stringify(input),
84
- });
85
- }
86
- // -- the denial log (K-35, #867) --------------------------------------------
87
- /**
88
- * The scope's recorded permission refusals, bucketed per (actor, permission) with the
89
- * window's own facts beside them — the view to open first, because the raw log's
90
- * volume is attacker-influenceable and a prober can flood a newest-first page.
91
- */
92
- summarizeDenials(tenantId, scopeId, filter) {
93
- return this.call(`/tenants/${tenantId}/scopes/${scopeId}/denials/summary${denialQuery(filter)}`);
94
- }
95
- /** The raw rows behind a bucket, newest first. */
96
- listDenials(tenantId, scopeId, filter) {
97
- return this.call(`/tenants/${tenantId}/scopes/${scopeId}/denials${denialQuery(filter)}`);
98
- }
99
- /**
100
- * The gate. Throws unless the tenant is active AND the scope exists and is
101
- * active — the same fail-closed logic the kernel's `validateScopeAccess`
102
- * applies locally, so a tenant-level cascade suspend bites too, not just a
103
- * per-scope one. Call it before handing a request to the local scope host.
104
- */
105
- async assertScopeActive(tenantId, scopeId) {
106
- const [tenant, scope] = await Promise.all([
107
- this.getTenant(tenantId),
108
- this.getScopeRecord(tenantId, scopeId),
109
- ]);
110
- if (!tenant)
111
- throw new ControlPlaneError(403, `unknown tenant: ${tenantId}`);
112
- if (tenant.status !== 'active') {
113
- throw new ControlPlaneError(403, `tenant not active (status: ${tenant.status}): ${tenantId}`);
114
- }
115
- if (!scope)
116
- throw new ControlPlaneError(403, `unknown scope for tenant: (${tenantId}, ${scopeId})`);
117
- if (scope.status !== 'active') {
118
- throw new ControlPlaneError(403, `scope not active (status: ${scope.status}): ${scopeId}`);
119
- }
120
- }
121
- }
122
- //# sourceMappingURL=client.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -1,30 +0,0 @@
1
- import { z } from '@substrat-run/contracts';
2
- /**
3
- * A tenant a login builds for, as the control plane's studio membership read states it
4
- * (`POST /internal/builder/identity-tenants`, builder-plane.md §4).
5
- *
6
- * The three directory fields are parsed with the SAME schemas the `tenant` record
7
- * publishes (`contracts/src/tenancy.ts`), not with generic strings: the id is a ULID, the
8
- * slug is the constrained slug, the name is non-empty. That costs nothing in lockout risk
9
- * — `createTenantInput` picks those same three, so a tenant that exists satisfied them at
10
- * creation — and it buys the half a loose parse would miss. A non-empty but malformed id
11
- * passes `z.string()` and then becomes a Durable Object key and a team route, which is the
12
- * downstream use this parse exists to protect.
13
- */
14
- export declare const identityTenant: z.ZodObject<{
15
- id: z.core.$ZodBranded<z.ZodString, "TenantId", "out">;
16
- slug: z.ZodString;
17
- name: z.ZodString;
18
- entitled: z.ZodBoolean;
19
- }, z.core.$strip>;
20
- /** The `/internal/builder/identity-tenants` response body. */
21
- export declare const identityTenantsResponse: z.ZodObject<{
22
- tenants: z.ZodArray<z.ZodObject<{
23
- id: z.core.$ZodBranded<z.ZodString, "TenantId", "out">;
24
- slug: z.ZodString;
25
- name: z.ZodString;
26
- entitled: z.ZodBoolean;
27
- }, z.core.$strip>>;
28
- }, z.core.$strip>;
29
- export type IdentityTenant = z.infer<typeof identityTenant>;
30
- //# sourceMappingURL=identity-tenants.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"identity-tenants.d.ts","sourceRoot":"","sources":["../src/identity-tenants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,CAAC,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc;;;;;iBAMzB,CAAC;AAEH,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB;;;;;;;iBAAiD,CAAC;AAEtF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC"}
@@ -1,23 +0,0 @@
1
- import { slug, tenantId, z } from '@substrat-run/contracts';
2
- /**
3
- * A tenant a login builds for, as the control plane's studio membership read states it
4
- * (`POST /internal/builder/identity-tenants`, builder-plane.md §4).
5
- *
6
- * The three directory fields are parsed with the SAME schemas the `tenant` record
7
- * publishes (`contracts/src/tenancy.ts`), not with generic strings: the id is a ULID, the
8
- * slug is the constrained slug, the name is non-empty. That costs nothing in lockout risk
9
- * — `createTenantInput` picks those same three, so a tenant that exists satisfied them at
10
- * creation — and it buys the half a loose parse would miss. A non-empty but malformed id
11
- * passes `z.string()` and then becomes a Durable Object key and a team route, which is the
12
- * downstream use this parse exists to protect.
13
- */
14
- export const identityTenant = z.object({
15
- id: tenantId,
16
- slug,
17
- name: z.string().min(1),
18
- /** Whether the tenant holds the `builder` entitlement (the control plane applies expiry at read). */
19
- entitled: z.boolean(),
20
- });
21
- /** The `/internal/builder/identity-tenants` response body. */
22
- export const identityTenantsResponse = z.object({ tenants: z.array(identityTenant) });
23
- //# sourceMappingURL=identity-tenants.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"identity-tenants.js","sourceRoot":"","sources":["../src/identity-tenants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,EAAE,EAAE,QAAQ;IACZ,IAAI;IACJ,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,qGAAqG;IACrG,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;CACtB,CAAC,CAAC;AAEH,8DAA8D;AAC9D,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC"}
@@ -1,56 +0,0 @@
1
- import type { ConnectionProbe } from '@substrat-run/contracts';
2
- /**
3
- * The one HTTP seam every control-plane caller shares (#971): how a request is
4
- * authenticated, how a failure is read, and what an empty answer is. The callers
5
- * differ in what they ask — `ControlPlaneClient` is the vertical's connect seam,
6
- * `ControlPlaneStaffClient` the console's staff surface — not in how they ask it.
7
- *
8
- * Browser-safe: nothing here reaches past `fetch` and `@substrat-run/contracts`, which is
9
- * why the console can import it (`@substrat-run/control-plane-api/client`).
10
- */
11
- export interface ControlPlaneTransportOptions {
12
- /** Base URL of the control-plane API; a path such as `/api` is fine in a browser. */
13
- baseUrl: string;
14
- /**
15
- * The dev-only `x-platform-actor` header, sent only when no `serviceToken` is set. `null`
16
- * sends none: the console's session mode, where the staff cookie authenticates instead.
17
- */
18
- actor: string | null;
19
- serviceToken?: string;
20
- fetch?: typeof globalThis.fetch;
21
- /** Passed to `fetch` only when set — `'include'` carries the console's staff session cookie. */
22
- credentials?: RequestCredentials;
23
- }
24
- /** A non-2xx (or unreachable) control-plane response. `status` is 0 on a transport error. */
25
- export declare class ControlPlaneError extends Error {
26
- readonly status: number;
27
- /**
28
- * The provider's own answer, when the plane refused a connect because the credential
29
- * was rejected upstream (#605, 422). Carried so a console can show WHY — "Scrive:
30
- * No valid access credentials were provided" — instead of a generic save failure.
31
- */
32
- readonly probe?: ConnectionProbe | undefined;
33
- constructor(status: number, message: string,
34
- /**
35
- * The provider's own answer, when the plane refused a connect because the credential
36
- * was rejected upstream (#605, 422). Carried so a console can show WHY — "Scrive:
37
- * No valid access credentials were provided" — instead of a generic save failure.
38
- */
39
- probe?: ConnectionProbe | undefined);
40
- }
41
- export declare class ControlPlaneTransport {
42
- private readonly baseUrl;
43
- private readonly actor;
44
- private readonly serviceToken?;
45
- private readonly fetchImpl;
46
- private readonly credentials?;
47
- constructor(options: ControlPlaneTransportOptions);
48
- /**
49
- * One request, and the one place a failure is read: a transport error or a non-2xx answer
50
- * throws `ControlPlaneError`, so what comes back is always a successful `Response`. A 404
51
- * is handed back as-is when the caller allows it.
52
- */
53
- protected send(path: string, init?: RequestInit, allow404?: boolean): Promise<Response>;
54
- protected call<T>(path: string, init?: RequestInit, allow404?: boolean): Promise<T>;
55
- }
56
- //# sourceMappingURL=transport.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAI/D;;;;;;;;GAQG;AACH,MAAM,WAAW,4BAA4B;IAC3C,qFAAqF;IACrF,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,gGAAgG;IAChG,WAAW,CAAC,EAAE,kBAAkB,CAAC;CAClC;AAED,6FAA6F;AAC7F,qBAAa,iBAAkB,SAAQ,KAAK;IAExC,QAAQ,CAAC,MAAM,EAAE,MAAM;IAEvB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe;IARlC,YACW,MAAM,EAAE,MAAM,EACvB,OAAO,EAAE,MAAM;IACf;;;;OAIG;IACM,KAAK,CAAC,EAAE,eAAe,YAAA,EAIjC;CACF;AAED,qBAAa,qBAAqB;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IACpD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAqB;IAElD,YAAY,OAAO,EAAE,4BAA4B,EAShD;IAED;;;;OAIG;IACH,UAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,EAAE,QAAQ,UAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAwC1F;IAED,UAAgB,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,EAAE,QAAQ,UAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,CAItF;CACF"}
package/dist/transport.js DELETED
@@ -1,88 +0,0 @@
1
- import { problemDetail } from '@substrat-run/contracts';
2
- import { DEV_ACTOR_HEADER, SERVICE_TOKEN_HEADER } from './auth.js';
3
- /** A non-2xx (or unreachable) control-plane response. `status` is 0 on a transport error. */
4
- export class ControlPlaneError extends Error {
5
- status;
6
- probe;
7
- constructor(status, message,
8
- /**
9
- * The provider's own answer, when the plane refused a connect because the credential
10
- * was rejected upstream (#605, 422). Carried so a console can show WHY — "Scrive:
11
- * No valid access credentials were provided" — instead of a generic save failure.
12
- */
13
- probe) {
14
- super(message);
15
- this.status = status;
16
- this.probe = probe;
17
- this.name = 'ControlPlaneError';
18
- }
19
- }
20
- export class ControlPlaneTransport {
21
- baseUrl;
22
- actor;
23
- serviceToken;
24
- fetchImpl;
25
- credentials;
26
- constructor(options) {
27
- this.baseUrl = options.baseUrl.replace(/\/$/, '');
28
- this.actor = options.actor;
29
- this.serviceToken = options.serviceToken;
30
- this.credentials = options.credentials;
31
- // Bind to globalThis: workerd throws "Illegal invocation" if `fetch` is called
32
- // with a `this` other than the global scope (which `this.fetchImpl(...)` would
33
- // otherwise set to this instance). An injected fetch is used as-is.
34
- this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
35
- }
36
- /**
37
- * One request, and the one place a failure is read: a transport error or a non-2xx answer
38
- * throws `ControlPlaneError`, so what comes back is always a successful `Response`. A 404
39
- * is handed back as-is when the caller allows it.
40
- */
41
- async send(path, init, allow404 = false) {
42
- let res;
43
- try {
44
- res = await this.fetchImpl(`${this.baseUrl}${path}`, {
45
- ...init,
46
- ...(this.credentials ? { credentials: this.credentials } : {}),
47
- headers: {
48
- // One credential per request (#980): a service token identifies the caller as
49
- // the plane's service actor and is checked BEFORE the dev-actor stub, so the
50
- // actor header would be ignored there — and a dev-only header has no business
51
- // leaving a production caller at all. Without a token, the actor header IS the
52
- // (local, UNSAFE) credential.
53
- ...(this.serviceToken
54
- ? { [SERVICE_TOKEN_HEADER]: this.serviceToken }
55
- : this.actor !== null
56
- ? { [DEV_ACTOR_HEADER]: this.actor }
57
- : {}),
58
- 'content-type': 'application/json',
59
- ...init?.headers,
60
- },
61
- });
62
- }
63
- catch (e) {
64
- // A transport failure (control plane down) must fail closed, not silently
65
- // pass — a vertical that cannot reach the authority does not get to run.
66
- throw new ControlPlaneError(0, `control plane unreachable: ${e.message}`);
67
- }
68
- if (res.status === 404 && allow404)
69
- return res;
70
- if (!res.ok) {
71
- // `problemDetail` is the one reading of a failed body (#971): the RFC 9457
72
- // `detail` first, the deprecated `error` duplicate second, both against the
73
- // published schema — so this works either way, and keeps working the day the
74
- // duplicate is deleted. The status line is the fallback only when the body
75
- // said nothing readable.
76
- const body = await res.json().catch(() => null);
77
- throw new ControlPlaneError(res.status, problemDetail(body) ?? `${res.status} ${res.statusText}`);
78
- }
79
- return res;
80
- }
81
- async call(path, init, allow404 = false) {
82
- const res = await this.send(path, init, allow404);
83
- if (res.status === 404 && allow404)
84
- return undefined;
85
- return res.status === 204 ? undefined : (await res.json());
86
- }
87
- }
88
- //# sourceMappingURL=transport.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}