@substrat-run/control-plane-api 0.92.1 → 0.95.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 +18 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +81 -15
- package/dist/api.js.map +1 -1
- package/dist/deploy.d.ts +4 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/mask.d.ts +7 -5
- package/dist/mask.d.ts.map +1 -1
- package/dist/mask.js +63 -37
- package/dist/mask.js.map +1 -1
- package/dist/platform-drain.d.ts +12 -0
- package/dist/platform-drain.d.ts.map +1 -1
- package/dist/platform-drain.js +34 -1
- package/dist/platform-drain.js.map +1 -1
- package/dist/pseudonymize.d.ts +69 -0
- package/dist/pseudonymize.d.ts.map +1 -0
- package/dist/pseudonymize.js +222 -0
- package/dist/pseudonymize.js.map +1 -0
- package/dist/wfp.d.ts +21 -0
- package/dist/wfp.d.ts.map +1 -1
- package/dist/wfp.js +6 -1
- package/dist/wfp.js.map +1 -1
- package/package.json +6 -6
package/dist/api.d.ts
CHANGED
|
@@ -45,6 +45,12 @@ export interface ConnectionInspector {
|
|
|
45
45
|
probeCandidate?: (secret: Record<string, string>) => Promise<ConnectionProbe>;
|
|
46
46
|
}
|
|
47
47
|
export interface ControlPlaneApiOptions {
|
|
48
|
+
/**
|
|
49
|
+
* The platform's margin over list price for model usage it provides (#1054), whole
|
|
50
|
+
* percent, applied at read time by `GET /model-usage/summary`. One global number
|
|
51
|
+
* today; a per-provider rate is an additive change later. Default 20.
|
|
52
|
+
*/
|
|
53
|
+
modelMarginPercent?: number;
|
|
48
54
|
host: ScopeHost;
|
|
49
55
|
/**
|
|
50
56
|
* How to reach each vertical, by slug (K-31). Absent slugs simply cannot be
|
|
@@ -158,6 +164,17 @@ export interface ControlPlaneApiOptions {
|
|
|
158
164
|
* token; set once, out of routine rotation (rotating it revokes every issued token).
|
|
159
165
|
*/
|
|
160
166
|
pushTokenSecret?: string;
|
|
167
|
+
/**
|
|
168
|
+
* Keys the pseudonymizer behind a masked export (#1034, `pseudonymize.ts`). The salt
|
|
169
|
+
* never reaches the dump and no mapping is stored, so it is not a decryption key —
|
|
170
|
+
* what it buys is STABILITY: with one configured, two pulls of the same scope read
|
|
171
|
+
* the same, which is what makes a masked copy usable as a standing preview.
|
|
172
|
+
*
|
|
173
|
+
* Absent ⇒ a fresh random salt per export. Still deterministic within one response
|
|
174
|
+
* (a customer reads the same on every screen and in the timeline) and strictly
|
|
175
|
+
* safer, since two exports cannot be correlated — just not stable across pulls.
|
|
176
|
+
*/
|
|
177
|
+
maskSalt?: string;
|
|
161
178
|
/**
|
|
162
179
|
* Cloudflare-native observability reads (design/observability.md §4.1) —
|
|
163
180
|
* host-injected like `deployVertical`, so this package holds no credential and the
|
|
@@ -248,6 +265,7 @@ type Vars = {
|
|
|
248
265
|
actor: PlatformActorId;
|
|
249
266
|
principal: Principal;
|
|
250
267
|
};
|
|
268
|
+
export declare const DEFAULT_MODEL_MARGIN_PERCENT = 20;
|
|
251
269
|
/**
|
|
252
270
|
* The audited HTTP surface over `HostAdmin` (control-plane.md §4.5).
|
|
253
271
|
*
|
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/api.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { adminAction, ASSET_PART_PREFIX, assetHash, channelName, connectionActivity, connectionActivitySource, connectionCredential, connectionFilter, connectionProbe, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, promotionAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, registerVerticalInput, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, } from '@substrat-run/contracts';
|
|
2
|
+
import { parseHostname, withLabel, RESERVED_LABEL_SEPARATOR, adminAction, ASSET_PART_PREFIX, assetHash, channelName, connectionActivity, connectionActivitySource, connectionCredential, connectionFilter, connectionProbe, createTenantInput, entitlementGrantInput, hostname as hostnameSchema, hostnameRegion, hostnameStatus, identityLink, listPageQuery, matchesOutboundHost, pageOf, platformRequestFilter, principalId as principalIdSchema, promotionAcknowledgement, provisionableJurisdiction, publishVersionInput, queryScopeInput, readScopeTableInput, DEFAULT_DENIAL_LIMIT, DENIAL_LIMIT_MAX, registerVerticalInput, scopeDump, dataSubjectId as dataSubjectIdSchema, scopeId as scopeIdSchema, scopeStatus, storageShape, surfaceName, tenantId as tenantIdSchema, tenantStatus, versionOrigin, z, PROBLEM_CONTENT_TYPE, toProblem, } from '@substrat-run/contracts';
|
|
3
3
|
import { migrationProgress, ulid } from '@substrat-run/kernel';
|
|
4
4
|
import { TENANT_HEADER } from './auth.js';
|
|
5
5
|
import { connectionGrantsForScope } from './vertical-client.js';
|
|
@@ -9,6 +9,7 @@ import { ControlPlaneError } from './client.js';
|
|
|
9
9
|
import { provisionSiblingScope } from './platform-drain.js';
|
|
10
10
|
import { mapError } from './errors.js';
|
|
11
11
|
import { maskDump, maskRecords } from './mask.js';
|
|
12
|
+
import { createPseudonymizer } from './pseudonymize.js';
|
|
12
13
|
import { openDump, sealDump } from './seal.js';
|
|
13
14
|
import { assertSandboxContract, deployManifest, storedDeployManifest, deploymentRefFor, stableDeploymentRefFor, nextMigrationTag, upstreamStatusOf, } from './deploy.js';
|
|
14
15
|
/**
|
|
@@ -260,6 +261,22 @@ const opsFailuresQuery = z.object({
|
|
|
260
261
|
// Bounded by default exactly as /admin-log, and for the same reason.
|
|
261
262
|
...listPageQuery.shape,
|
|
262
263
|
});
|
|
264
|
+
const modelUsageQuery = z.object({
|
|
265
|
+
tenantId: tenantIdSchema.optional(),
|
|
266
|
+
scopeId: scopeIdSchema.optional(),
|
|
267
|
+
vertical: z.string().optional(),
|
|
268
|
+
model: z.string().optional(),
|
|
269
|
+
since: z.string().optional(),
|
|
270
|
+
until: z.string().optional(),
|
|
271
|
+
...listPageQuery.shape,
|
|
272
|
+
});
|
|
273
|
+
/** The summary window: a half-open `[since, until)`; defaults to the current calendar month so far. */
|
|
274
|
+
const modelUsageSummaryQuery = z.object({
|
|
275
|
+
tenantId: tenantIdSchema.optional(),
|
|
276
|
+
since: z.string().optional(),
|
|
277
|
+
until: z.string().optional(),
|
|
278
|
+
});
|
|
279
|
+
export const DEFAULT_MODEL_MARGIN_PERCENT = 20;
|
|
263
280
|
/** The upstream provider's trace handle, when a failure message carries one —
|
|
264
281
|
* Cloudflare's `internal error; reference = <id>` shape (#559). */
|
|
265
282
|
const UPSTREAM_REFERENCE = /\breference\s*=\s*([a-z0-9]+)/i;
|
|
@@ -1992,7 +2009,14 @@ export function createControlPlaneApi(options) {
|
|
|
1992
2009
|
const dump = await admin.exportScope(actor, tenantId, scopeId);
|
|
1993
2010
|
const vertical = await verticalForScope(c, scope);
|
|
1994
2011
|
const tables = vertical ? await vertical.exportScope(scopeId) : dump.tables;
|
|
1995
|
-
|
|
2012
|
+
if (full)
|
|
2013
|
+
return c.json({ ...dump, tables, masked: false });
|
|
2014
|
+
// One pseudonymizer for the whole response (#1034): the same customer has to read
|
|
2015
|
+
// the same in their own row, in every event payload that quoted them, and in the
|
|
2016
|
+
// timeline — a per-table generator would break exactly the joins that make a
|
|
2017
|
+
// masked copy worth pulling.
|
|
2018
|
+
const mask = await createPseudonymizer(options.maskSalt ?? crypto.randomUUID());
|
|
2019
|
+
return c.json({ ...dump, tables: await maskDump(tables, mask), masked: true });
|
|
1996
2020
|
}
|
|
1997
2021
|
catch (e) {
|
|
1998
2022
|
if (e instanceof ControlPlaneError) {
|
|
@@ -2062,13 +2086,17 @@ export function createControlPlaneApi(options) {
|
|
|
2062
2086
|
// a vertical-held scope's real tables overlay it. A reaped scope has no storage
|
|
2063
2087
|
// left to read, so it is skipped here while its RECORD stays above — the tombstone
|
|
2064
2088
|
// is honest, an error would not be.
|
|
2089
|
+
// ONE pseudonymizer for the whole export (#1034), shared by the directory half
|
|
2090
|
+
// and every scope's tables: the person named in `members` has to read the same in
|
|
2091
|
+
// the scope row that references them and in the event payload that quoted them.
|
|
2092
|
+
const mask = full ? undefined : await createPseudonymizer(options.maskSalt ?? crypto.randomUUID());
|
|
2065
2093
|
const live = scopes.filter((s) => s.status !== 'reaped');
|
|
2066
2094
|
const data = [];
|
|
2067
2095
|
for (const scope of live) {
|
|
2068
2096
|
const dump = await admin.exportScope(actor, tenantId, scope.id);
|
|
2069
2097
|
const vertical = await verticalForScope(c, scope);
|
|
2070
2098
|
const tables = vertical ? await vertical.exportScope(scope.id) : dump.tables;
|
|
2071
|
-
data.push({ ...dump, tables:
|
|
2099
|
+
data.push({ ...dump, tables: mask ? await maskDump(tables, mask) : tables });
|
|
2072
2100
|
}
|
|
2073
2101
|
// The admin log is FULL-only (#36): it records what STAFF did, so it is not the
|
|
2074
2102
|
// customer's Art. 20 data, and it carries staff actor ids and internal action
|
|
@@ -2079,19 +2107,19 @@ export function createControlPlaneApi(options) {
|
|
|
2079
2107
|
tenantId,
|
|
2080
2108
|
capturedAt: new Date().toISOString(),
|
|
2081
2109
|
masked: !full,
|
|
2082
|
-
tenant:
|
|
2083
|
-
scopes:
|
|
2084
|
-
orgs:
|
|
2085
|
-
members:
|
|
2110
|
+
tenant: mask ? (await maskRecords([tenant], mask))[0] : tenant,
|
|
2111
|
+
scopes: mask ? await maskRecords(scopes, mask) : scopes,
|
|
2112
|
+
orgs: mask ? await maskRecords(orgs, mask) : orgs,
|
|
2113
|
+
members: mask ? await maskRecords(members, mask) : members,
|
|
2086
2114
|
// Roles, entitlements and hostnames are configuration rather than personal data,
|
|
2087
2115
|
// so they read the same in both fidelities — but they still go through the sweep,
|
|
2088
2116
|
// because deciding per-collection what "cannot contain PII" means is exactly the
|
|
2089
2117
|
// assumption that ages badly. One rule, applied everywhere.
|
|
2090
|
-
roles:
|
|
2091
|
-
entitlements:
|
|
2118
|
+
roles: mask ? await maskRecords(roles, mask) : roles,
|
|
2119
|
+
entitlements: mask ? await maskRecords(entitlements, mask) : entitlements,
|
|
2092
2120
|
// Identity links are the sharpest item here: `externalId` is usually an email.
|
|
2093
|
-
identityLinks:
|
|
2094
|
-
hostnames:
|
|
2121
|
+
identityLinks: mask ? await maskRecords(identityLinks, mask) : identityLinks,
|
|
2122
|
+
hostnames: mask ? await maskRecords(hostnames, mask) : hostnames,
|
|
2095
2123
|
stores: [...stores, ...blobStores].map((s) => ({
|
|
2096
2124
|
kind: s.kind,
|
|
2097
2125
|
vertical: s.vertical,
|
|
@@ -2099,7 +2127,7 @@ export function createControlPlaneApi(options) {
|
|
|
2099
2127
|
ref: s.ref,
|
|
2100
2128
|
createdAt: s.createdAt,
|
|
2101
2129
|
})),
|
|
2102
|
-
connections:
|
|
2130
|
+
connections: mask ? await maskRecords(connections, mask) : connections,
|
|
2103
2131
|
adminLog,
|
|
2104
2132
|
data,
|
|
2105
2133
|
};
|
|
@@ -2746,6 +2774,9 @@ export function createControlPlaneApi(options) {
|
|
|
2746
2774
|
modules,
|
|
2747
2775
|
doClasses: manifest.doClasses,
|
|
2748
2776
|
bindings: [...manifest.bindings, ...storeBindings],
|
|
2777
|
+
// #1054: the model runtime is bound only for a version that ASKED for it, so the
|
|
2778
|
+
// capability is visible in the manifest diff at admit rather than fleet-wide.
|
|
2779
|
+
...(manifest.usesModels ? { usesModels: true } : {}),
|
|
2749
2780
|
// #340/#578: the version's static files travel with it onto the serving script —
|
|
2750
2781
|
// from the RETAINED manifest, with no bytes. The runtime's asset store dedupes
|
|
2751
2782
|
// per SCRIPT (not namespace-wide — the #578 finding), so the serving script only
|
|
@@ -3602,8 +3633,10 @@ export function createControlPlaneApi(options) {
|
|
|
3602
3633
|
* hostname `<label>--<tag>.<domain>` bound to `previewId`. Non-canonical, so it never
|
|
3603
3634
|
* demotes the prod surface. Shared by the fork and clean-room paths. */
|
|
3604
3635
|
const bindPreviewHostname = async (actor, baseHostname, tenantId, previewId, tag, surface) => {
|
|
3605
|
-
const
|
|
3606
|
-
|
|
3636
|
+
const parsed = parseHostname(baseHostname);
|
|
3637
|
+
if (!parsed)
|
|
3638
|
+
throw new ControlPlaneError(400, `'${baseHostname}' is not a hostname a preview can be minted beside`);
|
|
3639
|
+
const hostname = withLabel(parsed, `${parsed.label}${RESERVED_LABEL_SEPARATOR}${tag}`);
|
|
3607
3640
|
const existing = (await admin.listHostnames(actor, { scopeId: previewId })).find((h) => h.hostname === hostname);
|
|
3608
3641
|
if (!existing) {
|
|
3609
3642
|
await admin.bindHostname(actor, {
|
|
@@ -3658,7 +3691,7 @@ export function createControlPlaneApi(options) {
|
|
|
3658
3691
|
const actor = c.get('actor');
|
|
3659
3692
|
const surface = opts.surface ?? 'app';
|
|
3660
3693
|
// #527 guard: a preview MUST serve the version it just bound. Routing resolves
|
|
3661
|
-
// `COALESCE(scope.servingRef, version.deploymentRef)` (control-plane-do `
|
|
3694
|
+
// `COALESCE(scope.servingRef, version.deploymentRef)` (control-plane-do `readRoute`),
|
|
3662
3695
|
// so a preview that still carries an inherited `serving_ref` — or otherwise resolves away
|
|
3663
3696
|
// from this version's own dispatch script — would answer with OTHER code (the promoted
|
|
3664
3697
|
// prod build) while we report success. That is worse than a failure: it sends a reviewer
|
|
@@ -3981,6 +4014,39 @@ export function createControlPlaneApi(options) {
|
|
|
3981
4014
|
const entries = await admin.listOpsFailures(c.get('actor'), filter);
|
|
3982
4015
|
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
3983
4016
|
});
|
|
4017
|
+
// -- model usage (#1054): meter 3, the one D-30 could not compute -------------
|
|
4018
|
+
// The ledger the `model-usage` intents drain into. Staff-only: the tenant-facing
|
|
4019
|
+
// read is the vertical's own usage screen over its metering engine; this is the
|
|
4020
|
+
// platform's copy, the one an invoice reconciles against.
|
|
4021
|
+
app.get('/model-usage', async (c) => {
|
|
4022
|
+
const filter = modelUsageQuery.parse({
|
|
4023
|
+
tenantId: c.req.query('tenantId'),
|
|
4024
|
+
scopeId: c.req.query('scopeId'),
|
|
4025
|
+
vertical: c.req.query('vertical'),
|
|
4026
|
+
model: c.req.query('model'),
|
|
4027
|
+
since: c.req.query('since'),
|
|
4028
|
+
until: c.req.query('until'),
|
|
4029
|
+
limit: c.req.query('limit'),
|
|
4030
|
+
cursor: c.req.query('cursor'),
|
|
4031
|
+
order: c.req.query('order'),
|
|
4032
|
+
});
|
|
4033
|
+
const entries = await admin.listModelUsage(c.get('actor'), filter);
|
|
4034
|
+
return c.json(pageOf(entries, filter.limit, (e) => e.id));
|
|
4035
|
+
});
|
|
4036
|
+
// Folded per (tenant, vertical, model) with the platform's margin applied at read
|
|
4037
|
+
// time. Nothing is stored by the read: D-30's "meter, don't bill" holds.
|
|
4038
|
+
app.get('/model-usage/summary', async (c) => {
|
|
4039
|
+
const q = modelUsageSummaryQuery.parse({
|
|
4040
|
+
tenantId: c.req.query('tenantId'),
|
|
4041
|
+
since: c.req.query('since'),
|
|
4042
|
+
until: c.req.query('until'),
|
|
4043
|
+
});
|
|
4044
|
+
const now = new Date();
|
|
4045
|
+
const since = q.since ?? new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)).toISOString();
|
|
4046
|
+
const until = q.until ?? now.toISOString();
|
|
4047
|
+
const summary = await admin.summarizeModelUsage(c.get('actor'), { ...(q.tenantId ? { tenantId: q.tenantId } : {}), since, until }, options.modelMarginPercent ?? DEFAULT_MODEL_MARGIN_PERCENT);
|
|
4048
|
+
return c.json(summary);
|
|
4049
|
+
});
|
|
3984
4050
|
return app;
|
|
3985
4051
|
}
|
|
3986
4052
|
//# sourceMappingURL=api.js.map
|