@substrat-run/control-plane-api 0.4.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/api.d.ts +44 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +202 -0
- package/dist/api.js.map +1 -0
- package/dist/auth.d.ts +39 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +32 -0
- package/dist/auth.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { PlatformActorId } from '@substrat-run/contracts';
|
|
3
|
+
import type { ScopeHost } from '@substrat-run/kernel';
|
|
4
|
+
import type { PlatformActorAuth } from './auth.js';
|
|
5
|
+
export interface ControlPlaneApiOptions {
|
|
6
|
+
host: ScopeHost;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the platform actor from the request. No default: an unauthenticated
|
|
9
|
+
* control plane is not a sensible fallback, and a package that shipped one
|
|
10
|
+
* would eventually be deployed with it (control-plane.md §6).
|
|
11
|
+
*/
|
|
12
|
+
authenticate: PlatformActorAuth;
|
|
13
|
+
}
|
|
14
|
+
type Vars = {
|
|
15
|
+
actor: PlatformActorId;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The audited HTTP surface over `HostAdmin` (control-plane.md §4.5).
|
|
19
|
+
*
|
|
20
|
+
* This is the OUT-OF-BAND control plane §3 argues for: it is not module code, it
|
|
21
|
+
* never receives a `ctx`, and it never runs in a scope's serialization domain —
|
|
22
|
+
* so `boundary-lint` is untouched (§7). It is one router over the kernel seam,
|
|
23
|
+
* mounted by whichever transport is hosting it (a Node server locally, a Worker
|
|
24
|
+
* holding the `controlPlane` binding on Cloudflare).
|
|
25
|
+
*
|
|
26
|
+
* Two rules hold everywhere below, and they are the reason this can exist at all:
|
|
27
|
+
*
|
|
28
|
+
* 1. **The actor comes from the authenticated request, never the body.** §4.4:
|
|
29
|
+
* every field of an audit row except before/after is stamped platform-side,
|
|
30
|
+
* "never supplied by the caller". A route that read an actor from JSON would
|
|
31
|
+
* make the entire trail forgeable, which is the one thing that must not be
|
|
32
|
+
* retrofitted (K-20). Note there is no route here that accepts an `actor`
|
|
33
|
+
* field at all — it is unrepresentable, not merely ignored.
|
|
34
|
+
* 2. **Reads are exposed; enforcement writes are not.** defineRole / assignRole /
|
|
35
|
+
* grant / grantToOrg / addMember / linkIdentity are on `HostAdmin` but get no
|
|
36
|
+
* route: the console's v1 job is the tenant registry, lifecycle, entitlements
|
|
37
|
+
* and history. `resolveIdentity` especially stays off — it is the auth
|
|
38
|
+
* adapter's read path, not an admin surface.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createControlPlaneApi(options: ControlPlaneApiOptions): Hono<{
|
|
41
|
+
Variables: Vars;
|
|
42
|
+
}>;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAY5B,OAAO,KAAK,EAAE,eAAe,EAAqB,MAAM,yBAAyB,CAAC;AAClF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAGnD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,YAAY,EAAE,iBAAiB,CAAC;CACjC;AAED,KAAK,IAAI,GAAG;IAAE,KAAK,EAAE,eAAe,CAAA;CAAE,CAAC;AAkDvC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAAE,SAAS,EAAE,IAAI,CAAA;CAAE,CAAC,CAwJhG"}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { adminAction, createTenantInput, jurisdiction, scopeId as scopeIdSchema, scopeStatus, storageShape, tenantId as tenantIdSchema, tenantStatus, z, } from '@substrat-run/contracts';
|
|
3
|
+
import { mapError } from './errors.js';
|
|
4
|
+
// -- request schemas ---------------------------------------------------------
|
|
5
|
+
// Parse, don't trust: every input crosses Zod at the boundary. The ids stay
|
|
6
|
+
// CALLER-SUPPLIED rather than minted here, exactly as the contract has them —
|
|
7
|
+
// that is what keeps `createTenant`/`provisionScope` idempotent (§3.3: "safe to
|
|
8
|
+
// re-run"). Minting server-side would be friendlier and would silently turn a
|
|
9
|
+
// retry into a second tenant. This surface is a transport; it does not invent
|
|
10
|
+
// semantics on top of HostAdmin.
|
|
11
|
+
const provisionScopeBody = z.object({
|
|
12
|
+
tenantId: tenantIdSchema,
|
|
13
|
+
scopeId: scopeIdSchema,
|
|
14
|
+
slug: z.string().optional(),
|
|
15
|
+
kind: z.string().optional(),
|
|
16
|
+
name: z.string().optional(),
|
|
17
|
+
vertical: z.string().nullable().optional(),
|
|
18
|
+
storageShape: storageShape.optional(),
|
|
19
|
+
jurisdiction: jurisdiction.optional(),
|
|
20
|
+
});
|
|
21
|
+
const setTenantStatusBody = z.object({ status: tenantStatus });
|
|
22
|
+
/** Repeatable query params arrive as `?status=active&status=suspended`. */
|
|
23
|
+
const listScopesQuery = z.object({
|
|
24
|
+
tenantId: tenantIdSchema.optional(),
|
|
25
|
+
status: z.array(scopeStatus).optional(),
|
|
26
|
+
vertical: z.string().optional(),
|
|
27
|
+
});
|
|
28
|
+
const listRolesQuery = z.object({
|
|
29
|
+
tenantId: tenantIdSchema.optional(),
|
|
30
|
+
// Free-form: a module id or 'vertical'. Not narrowed to the source union here
|
|
31
|
+
// — an unknown source should return nothing, not 400. The console offers only
|
|
32
|
+
// sources it has seen.
|
|
33
|
+
source: z.string().optional(),
|
|
34
|
+
});
|
|
35
|
+
const auditLogQuery = z.object({
|
|
36
|
+
tenantId: tenantIdSchema.optional(),
|
|
37
|
+
scopeId: scopeIdSchema.optional(),
|
|
38
|
+
actor: z.string().optional(),
|
|
39
|
+
action: z.array(adminAction).optional(),
|
|
40
|
+
since: z.string().optional(),
|
|
41
|
+
until: z.string().optional(),
|
|
42
|
+
limit: z.coerce.number().int().positive().max(1000).optional(),
|
|
43
|
+
cursor: z.string().optional(),
|
|
44
|
+
order: z.enum(['asc', 'desc']).optional(),
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* The audited HTTP surface over `HostAdmin` (control-plane.md §4.5).
|
|
48
|
+
*
|
|
49
|
+
* This is the OUT-OF-BAND control plane §3 argues for: it is not module code, it
|
|
50
|
+
* never receives a `ctx`, and it never runs in a scope's serialization domain —
|
|
51
|
+
* so `boundary-lint` is untouched (§7). It is one router over the kernel seam,
|
|
52
|
+
* mounted by whichever transport is hosting it (a Node server locally, a Worker
|
|
53
|
+
* holding the `controlPlane` binding on Cloudflare).
|
|
54
|
+
*
|
|
55
|
+
* Two rules hold everywhere below, and they are the reason this can exist at all:
|
|
56
|
+
*
|
|
57
|
+
* 1. **The actor comes from the authenticated request, never the body.** §4.4:
|
|
58
|
+
* every field of an audit row except before/after is stamped platform-side,
|
|
59
|
+
* "never supplied by the caller". A route that read an actor from JSON would
|
|
60
|
+
* make the entire trail forgeable, which is the one thing that must not be
|
|
61
|
+
* retrofitted (K-20). Note there is no route here that accepts an `actor`
|
|
62
|
+
* field at all — it is unrepresentable, not merely ignored.
|
|
63
|
+
* 2. **Reads are exposed; enforcement writes are not.** defineRole / assignRole /
|
|
64
|
+
* grant / grantToOrg / addMember / linkIdentity are on `HostAdmin` but get no
|
|
65
|
+
* route: the console's v1 job is the tenant registry, lifecycle, entitlements
|
|
66
|
+
* and history. `resolveIdentity` especially stays off — it is the auth
|
|
67
|
+
* adapter's read path, not an admin surface.
|
|
68
|
+
*/
|
|
69
|
+
export function createControlPlaneApi(options) {
|
|
70
|
+
const { host, authenticate } = options;
|
|
71
|
+
const admin = host.admin;
|
|
72
|
+
const app = new Hono();
|
|
73
|
+
// Fail closed, before any route runs: no actor, no reach.
|
|
74
|
+
app.use('*', async (c, next) => {
|
|
75
|
+
const actor = await authenticate(c.req.raw);
|
|
76
|
+
if (!actor)
|
|
77
|
+
return c.json({ error: 'unauthenticated' }, 401);
|
|
78
|
+
c.set('actor', actor);
|
|
79
|
+
await next();
|
|
80
|
+
});
|
|
81
|
+
// One error boundary for every route: adapters throw plain Errors, and each
|
|
82
|
+
// one is a fail-closed refusal that must reach the caller as a status, not a
|
|
83
|
+
// stack trace.
|
|
84
|
+
app.onError((err, c) => {
|
|
85
|
+
if (err instanceof z.ZodError) {
|
|
86
|
+
return c.json({ error: 'invalid request', issues: err.issues }, 400);
|
|
87
|
+
}
|
|
88
|
+
const { status, body } = mapError(err);
|
|
89
|
+
return c.json(body, status);
|
|
90
|
+
});
|
|
91
|
+
// -- tenant registry (§4.1) ------------------------------------------------
|
|
92
|
+
app.get('/tenants', async (c) => c.json(await admin.listTenants()));
|
|
93
|
+
app.post('/tenants', async (c) => {
|
|
94
|
+
const input = createTenantInput.parse(await c.req.json());
|
|
95
|
+
await admin.createTenant(c.get('actor'), input);
|
|
96
|
+
// Idempotent (§4.1): re-creating an existing tenant is a no-op, not an error,
|
|
97
|
+
// so this reads back rather than reporting a create that may not have happened.
|
|
98
|
+
return c.json(await admin.getTenant(input.id), 201);
|
|
99
|
+
});
|
|
100
|
+
app.get('/tenants/:tenantId', async (c) => {
|
|
101
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
102
|
+
const tenant = await admin.getTenant(tenantId);
|
|
103
|
+
if (!tenant)
|
|
104
|
+
return c.json({ error: `unknown tenant: ${tenantId}` }, 404);
|
|
105
|
+
return c.json(tenant);
|
|
106
|
+
});
|
|
107
|
+
app.patch('/tenants/:tenantId/status', async (c) => {
|
|
108
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
109
|
+
const { status } = setTenantStatusBody.parse(await c.req.json());
|
|
110
|
+
// The live weapon (§7): `suspended` fails getScope closed for EVERY scope
|
|
111
|
+
// under the tenant. The blast radius is the console's to show; the audit row
|
|
112
|
+
// is this layer's to guarantee.
|
|
113
|
+
await admin.setTenantStatus(c.get('actor'), tenantId, status);
|
|
114
|
+
return c.json(await admin.getTenant(tenantId));
|
|
115
|
+
});
|
|
116
|
+
// -- entitlements (§4.3) ---------------------------------------------------
|
|
117
|
+
app.get('/tenants/:tenantId/entitlements', async (c) => c.json(await admin.listEntitlements(tenantIdSchema.parse(c.req.param('tenantId')))));
|
|
118
|
+
app.put('/tenants/:tenantId/entitlements/:key', async (c) => {
|
|
119
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
120
|
+
await admin.grantEntitlement(c.get('actor'), tenantId, c.req.param('key'));
|
|
121
|
+
return c.json(await admin.listEntitlements(tenantId));
|
|
122
|
+
});
|
|
123
|
+
app.delete('/tenants/:tenantId/entitlements/:key', async (c) => {
|
|
124
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
125
|
+
await admin.revokeEntitlement(c.get('actor'), tenantId, c.req.param('key'));
|
|
126
|
+
return c.json(await admin.listEntitlements(tenantId));
|
|
127
|
+
});
|
|
128
|
+
// -- the scope directory (§3.2/§4.2) ---------------------------------------
|
|
129
|
+
app.get('/scopes', async (c) => {
|
|
130
|
+
const filter = listScopesQuery.parse({
|
|
131
|
+
tenantId: c.req.query('tenantId'),
|
|
132
|
+
status: c.req.queries('status'),
|
|
133
|
+
vertical: c.req.query('vertical'),
|
|
134
|
+
});
|
|
135
|
+
return c.json(await admin.listScopes(filter));
|
|
136
|
+
});
|
|
137
|
+
app.post('/scopes', async (c) => {
|
|
138
|
+
const input = provisionScopeBody.parse(await c.req.json());
|
|
139
|
+
await host.provisionScope(c.get('actor'), input);
|
|
140
|
+
const record = await admin.getScopeRecord(input.tenantId, input.scopeId);
|
|
141
|
+
return c.json(record, 201);
|
|
142
|
+
});
|
|
143
|
+
app.get('/tenants/:tenantId/scopes/:scopeId', async (c) => {
|
|
144
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
145
|
+
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
146
|
+
const record = await admin.getScopeRecord(tenantId, scopeId);
|
|
147
|
+
// Absent, or present under another tenant — indistinguishable on purpose (K-3).
|
|
148
|
+
if (!record)
|
|
149
|
+
return c.json({ error: `unknown scope for tenant: (${tenantId}, ${scopeId})` }, 404);
|
|
150
|
+
return c.json(record);
|
|
151
|
+
});
|
|
152
|
+
// The four lifecycle transitions, one route each — mirroring the four audited
|
|
153
|
+
// actions rather than collapsing into a PATCH that would accept a target
|
|
154
|
+
// status the transition graph forbids. The graph is enforced below the seam;
|
|
155
|
+
// an illegal transition surfaces as a 409.
|
|
156
|
+
const transitions = {
|
|
157
|
+
suspend: (a, t, s) => admin.suspendScope(a, t, s),
|
|
158
|
+
unsuspend: (a, t, s) => admin.unsuspendScope(a, t, s),
|
|
159
|
+
archive: (a, t, s) => admin.archiveScope(a, t, s),
|
|
160
|
+
unarchive: (a, t, s) => admin.unarchiveScope(a, t, s),
|
|
161
|
+
};
|
|
162
|
+
for (const [action, run] of Object.entries(transitions)) {
|
|
163
|
+
app.post(`/tenants/:tenantId/scopes/:scopeId/${action}`, async (c) => {
|
|
164
|
+
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
165
|
+
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
166
|
+
await run(c.get('actor'), tenantId, scopeId);
|
|
167
|
+
return c.json(await admin.getScopeRecord(tenantId, scopeId));
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// -- roles, read only (§4.5 console item 4) --------------------------------
|
|
171
|
+
// The READ lands; `defineRole` deliberately does not. Creating a role over
|
|
172
|
+
// HTTP is a permission change, and the permission diff is a human checkpoint
|
|
173
|
+
// (D-22/D-29) — that surface needs its own decision, not a route added because
|
|
174
|
+
// the verb was adjacent.
|
|
175
|
+
app.get('/roles', async (c) => {
|
|
176
|
+
const filter = listRolesQuery.parse({
|
|
177
|
+
tenantId: c.req.query('tenantId'),
|
|
178
|
+
source: c.req.query('source'),
|
|
179
|
+
});
|
|
180
|
+
return c.json(await admin.listRoles(filter));
|
|
181
|
+
});
|
|
182
|
+
// -- the admin log (§4.4/§4.5) ---------------------------------------------
|
|
183
|
+
app.get('/admin-log', async (c) => {
|
|
184
|
+
const filter = auditLogQuery.parse({
|
|
185
|
+
tenantId: c.req.query('tenantId'),
|
|
186
|
+
scopeId: c.req.query('scopeId'),
|
|
187
|
+
actor: c.req.query('actor'),
|
|
188
|
+
action: c.req.queries('action'),
|
|
189
|
+
since: c.req.query('since'),
|
|
190
|
+
until: c.req.query('until'),
|
|
191
|
+
limit: c.req.query('limit'),
|
|
192
|
+
cursor: c.req.query('cursor'),
|
|
193
|
+
order: c.req.query('order'),
|
|
194
|
+
});
|
|
195
|
+
const entries = await admin.auditLog(filter);
|
|
196
|
+
// The cursor IS the last entry's id (ULID order is chronological), so the
|
|
197
|
+
// page carries its own continuation and the console never assembles one.
|
|
198
|
+
return c.json({ entries, nextCursor: entries.at(-1)?.id ?? null });
|
|
199
|
+
});
|
|
200
|
+
return app;
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type PlatformActorId } from '@substrat-run/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* The identity seam for the control plane (control-plane.md §6).
|
|
4
|
+
*
|
|
5
|
+
* The data model needs to know THAT there is an actor, not HOW it authenticated
|
|
6
|
+
* — so this whole surface is buildable and testable against a stub while real
|
|
7
|
+
* staff auth (SSO, MFA, short sessions, a small closed population) is designed.
|
|
8
|
+
* D-16 commits to identity being a swappable adapter; this is that being cashed
|
|
9
|
+
* in for platform staff rather than tenant users.
|
|
10
|
+
*
|
|
11
|
+
* Returning null means "not authenticated" and the request is refused. There is
|
|
12
|
+
* no "anonymous actor" — §4.4's whole point is that a surface which can act
|
|
13
|
+
* without a durable record of WHO acted is worse than no surface, and an actor
|
|
14
|
+
* the log cannot name is exactly that.
|
|
15
|
+
*/
|
|
16
|
+
export type PlatformActorAuth = (request: Request) => Promise<PlatformActorId | null> | PlatformActorId | null;
|
|
17
|
+
/** Header the dev stub reads. Mirrors the demos' `x-principal` dev affordance. */
|
|
18
|
+
export declare const DEV_ACTOR_HEADER = "x-platform-actor";
|
|
19
|
+
/**
|
|
20
|
+
* A dev stub that trusts an `x-platform-actor` header verbatim.
|
|
21
|
+
*
|
|
22
|
+
* UNSAFE_ by name, deliberately — it is the same convention the kernel uses for
|
|
23
|
+
* `UNSAFE_allowAllChecker`, and for the same reason: an unsafe default that is
|
|
24
|
+
* merely *documented* gets shipped, while one that must be typed out in the
|
|
25
|
+
* caller's own code gets noticed in review.
|
|
26
|
+
*
|
|
27
|
+
* **Never expose this on a non-local listener.** control-plane.md §6: real auth
|
|
28
|
+
* gates EXPOSING the console, not BUILDING it — nothing with cross-tenant reach
|
|
29
|
+
* goes anywhere non-local on a stub. The demos' `x-principal` header is a dev
|
|
30
|
+
* affordance for ONE tenant's principal; this header names a subject with reach
|
|
31
|
+
* across every tenant on the platform, so "a super-admin on top of it is a
|
|
32
|
+
* liability, not a milestone".
|
|
33
|
+
*
|
|
34
|
+
* It still parses: a header that is not a ULID is rejected rather than written
|
|
35
|
+
* into the audit log, because a malformed actor makes the trail unreadable
|
|
36
|
+
* exactly when it matters.
|
|
37
|
+
*/
|
|
38
|
+
export declare function UNSAFE_devPlatformActorAuth(): PlatformActorAuth;
|
|
39
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAEhF;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,eAAe,GAAG,IAAI,CAAC;AAE/G,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AAEnD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,2BAA2B,IAAI,iBAAiB,CAO/D"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { platformActorId } from '@substrat-run/contracts';
|
|
2
|
+
/** Header the dev stub reads. Mirrors the demos' `x-principal` dev affordance. */
|
|
3
|
+
export const DEV_ACTOR_HEADER = 'x-platform-actor';
|
|
4
|
+
/**
|
|
5
|
+
* A dev stub that trusts an `x-platform-actor` header verbatim.
|
|
6
|
+
*
|
|
7
|
+
* UNSAFE_ by name, deliberately — it is the same convention the kernel uses for
|
|
8
|
+
* `UNSAFE_allowAllChecker`, and for the same reason: an unsafe default that is
|
|
9
|
+
* merely *documented* gets shipped, while one that must be typed out in the
|
|
10
|
+
* caller's own code gets noticed in review.
|
|
11
|
+
*
|
|
12
|
+
* **Never expose this on a non-local listener.** control-plane.md §6: real auth
|
|
13
|
+
* gates EXPOSING the console, not BUILDING it — nothing with cross-tenant reach
|
|
14
|
+
* goes anywhere non-local on a stub. The demos' `x-principal` header is a dev
|
|
15
|
+
* affordance for ONE tenant's principal; this header names a subject with reach
|
|
16
|
+
* across every tenant on the platform, so "a super-admin on top of it is a
|
|
17
|
+
* liability, not a milestone".
|
|
18
|
+
*
|
|
19
|
+
* It still parses: a header that is not a ULID is rejected rather than written
|
|
20
|
+
* into the audit log, because a malformed actor makes the trail unreadable
|
|
21
|
+
* exactly when it matters.
|
|
22
|
+
*/
|
|
23
|
+
export function UNSAFE_devPlatformActorAuth() {
|
|
24
|
+
return (request) => {
|
|
25
|
+
const raw = request.headers.get(DEV_ACTOR_HEADER);
|
|
26
|
+
if (!raw)
|
|
27
|
+
return null;
|
|
28
|
+
const parsed = platformActorId.safeParse(raw);
|
|
29
|
+
return parsed.success ? parsed.data : null;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAwB,MAAM,yBAAyB,CAAC;AAkBhF,kFAAkF;AAClF,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AAEnD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,CAAC,OAAO,EAAE,EAAE;QACjB,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAClD,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,MAAM,MAAM,GAAG,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAC9C,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;IAC7C,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
2
|
+
export interface ApiError {
|
|
3
|
+
status: ContentfulStatusCode;
|
|
4
|
+
body: {
|
|
5
|
+
error: string;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export declare function mapError(err: unknown): ApiError;
|
|
9
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AA6CnE,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,oBAAoB,CAAC;IAC7B,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACzB;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAM/C"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map an adapter throw onto an HTTP status.
|
|
3
|
+
*
|
|
4
|
+
* `HostAdmin` throws plain `Error`s — deliberately, so their messages survive
|
|
5
|
+
* the Cloudflare RPC hop intact (a ZodError would not). That leaves this layer
|
|
6
|
+
* matching on message text, which is the weakest seam in this package and worth
|
|
7
|
+
* naming rather than hiding:
|
|
8
|
+
*
|
|
9
|
+
* - It is less brittle than it looks. Every pattern below is a message the
|
|
10
|
+
* CONTRACT SUITE asserts on (`/unknown tenant/`, `/illegal scope transition/`,
|
|
11
|
+
* `/already taken/`, `/not active/`), against both adapters. Changing one
|
|
12
|
+
* turns a contract test red, not just this mapping.
|
|
13
|
+
* - It is still text. The durable fix is typed errors on `HostAdmin` — a tagged
|
|
14
|
+
* union the adapters throw and this reads. That is a kernel change, and it is
|
|
15
|
+
* not worth blocking the transport on.
|
|
16
|
+
*
|
|
17
|
+
* Anything unmatched is a 500 with a GENERIC body: an unrecognised throw is, by
|
|
18
|
+
* definition, one whose message we have not reviewed for what it discloses, and
|
|
19
|
+
* this surface has cross-tenant reach.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* ORDER IS SIGNIFICANT — first match wins, so every specific pattern must precede
|
|
23
|
+
* the general one it would otherwise be swallowed by. `cannot provision scope
|
|
24
|
+
* under unknown tenant` contains `unknown tenant:`, and listing the general one
|
|
25
|
+
* first turned a precondition conflict into a 404 claiming POST /scopes does not
|
|
26
|
+
* exist. That is the message-matching fragility this file admits to above, caught
|
|
27
|
+
* by the test below rather than by reading.
|
|
28
|
+
*/
|
|
29
|
+
const STATUS_PATTERNS = [
|
|
30
|
+
// Well-formed, but conflicts with current state or references something absent.
|
|
31
|
+
// The addressed collection exists; the request cannot be applied to it.
|
|
32
|
+
[/cannot provision scope under unknown tenant/, 409],
|
|
33
|
+
[/already taken/, 409],
|
|
34
|
+
[/illegal scope transition/, 409],
|
|
35
|
+
[/non-active tenant/, 409],
|
|
36
|
+
[/not active \(status:/, 409],
|
|
37
|
+
// The ADDRESSED resource does not exist — including the K-3 fail-closed case
|
|
38
|
+
// where it exists under a DIFFERENT tenant and must read as absent.
|
|
39
|
+
[/unknown tenant:/, 404],
|
|
40
|
+
[/unknown scope for tenant/, 404],
|
|
41
|
+
[/scope has no tenant record/, 404],
|
|
42
|
+
];
|
|
43
|
+
export function mapError(err) {
|
|
44
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
45
|
+
for (const [pattern, status] of STATUS_PATTERNS) {
|
|
46
|
+
if (pattern.test(message))
|
|
47
|
+
return { status, body: { error: message } };
|
|
48
|
+
}
|
|
49
|
+
return { status: 500, body: { error: 'internal error' } };
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH;;;;;;;GAOG;AACH,MAAM,eAAe,GAA8C;IACjE,gFAAgF;IAChF,wEAAwE;IACxE,CAAC,6CAA6C,EAAE,GAAG,CAAC;IACpD,CAAC,eAAe,EAAE,GAAG,CAAC;IACtB,CAAC,0BAA0B,EAAE,GAAG,CAAC;IACjC,CAAC,mBAAmB,EAAE,GAAG,CAAC;IAC1B,CAAC,sBAAsB,EAAE,GAAG,CAAC;IAC7B,6EAA6E;IAC7E,oEAAoE;IACpE,CAAC,iBAAiB,EAAE,GAAG,CAAC;IACxB,CAAC,0BAA0B,EAAE,GAAG,CAAC;IACjC,CAAC,4BAA4B,EAAE,GAAG,CAAC;CACpC,CAAC;AAOF,MAAM,UAAU,QAAQ,CAAC,GAAY;IACnC,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACjE,KAAK,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,eAAe,EAAE,CAAC;QAChD,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC;IACzE,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,CAAC;AAC5D,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createControlPlaneApi } from './api.js';
|
|
2
|
+
export type { ControlPlaneApiOptions } from './api.js';
|
|
3
|
+
export { DEV_ACTOR_HEADER, UNSAFE_devPlatformActorAuth } from './auth.js';
|
|
4
|
+
export type { PlatformActorAuth } from './auth.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACjD,YAAY,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,MAAM,WAAW,CAAC;AAC1E,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,MAAM,WAAW,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@substrat-run/control-plane-api",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "HTTP surface over HostAdmin — the audited control-plane transport (control-plane.md §4.5)",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/substrat-run/substrat.git",
|
|
9
|
+
"directory": "packages/control-plane-api"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/substrat-run/substrat",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"dev": "tsx dev/server.mts"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@substrat-run/contracts": "workspace:^",
|
|
32
|
+
"@substrat-run/kernel": "workspace:^",
|
|
33
|
+
"hono": "^4.6.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@hono/node-server": "^1.13.0",
|
|
37
|
+
"@substrat-run/adapter-sqlite": "workspace:^",
|
|
38
|
+
"tsx": "^4.19.0",
|
|
39
|
+
"typescript": "^5.6.0",
|
|
40
|
+
"vitest": "^3.0.0"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|