@substrat-run/control-plane-api 0.14.0 → 0.16.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 (48) hide show
  1. package/README.md +58 -0
  2. package/dist/api.d.ts +18 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js +352 -14
  5. package/dist/api.js.map +1 -1
  6. package/dist/auth.d.ts +2 -0
  7. package/dist/auth.d.ts.map +1 -1
  8. package/dist/auth.js +11 -0
  9. package/dist/auth.js.map +1 -1
  10. package/dist/cf-observability.d.ts +22 -0
  11. package/dist/cf-observability.d.ts.map +1 -0
  12. package/dist/cf-observability.js +110 -0
  13. package/dist/cf-observability.js.map +1 -0
  14. package/dist/client.d.ts +3 -1
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +7 -0
  17. package/dist/client.js.map +1 -1
  18. package/dist/deploy.d.ts +3 -46
  19. package/dist/deploy.d.ts.map +1 -1
  20. package/dist/deploy.js +22 -35
  21. package/dist/deploy.js.map +1 -1
  22. package/dist/errors.d.ts.map +1 -1
  23. package/dist/errors.js +13 -0
  24. package/dist/errors.js.map +1 -1
  25. package/dist/index.d.ts +6 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/mask.d.ts +4 -0
  30. package/dist/mask.d.ts.map +1 -0
  31. package/dist/mask.js +71 -0
  32. package/dist/mask.js.map +1 -0
  33. package/dist/observability.d.ts +53 -0
  34. package/dist/observability.d.ts.map +1 -0
  35. package/dist/observability.js +17 -0
  36. package/dist/observability.js.map +1 -0
  37. package/dist/push-token.d.ts +33 -0
  38. package/dist/push-token.d.ts.map +1 -0
  39. package/dist/push-token.js +114 -0
  40. package/dist/push-token.js.map +1 -0
  41. package/dist/vertical-client.d.ts +76 -5
  42. package/dist/vertical-client.d.ts.map +1 -1
  43. package/dist/vertical-client.js +65 -0
  44. package/dist/vertical-client.js.map +1 -1
  45. package/dist/wfp.d.ts.map +1 -1
  46. package/dist/wfp.js +4 -0
  47. package/dist/wfp.js.map +1 -1
  48. package/package.json +5 -5
package/dist/errors.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ControlPlaneError } from './client.js';
1
2
  /**
2
3
  * Map an adapter throw onto an HTTP status.
3
4
  *
@@ -56,11 +57,23 @@ const STATUS_PATTERNS = [
56
57
  [/unknown scope /, 404],
57
58
  // A read-only introspection read (§5.4) for a table the scope's schema does not have.
58
59
  [/unknown table /, 404],
60
+ // The SQL console's gate (#219) refused the statement — a malformed request, not a
61
+ // server fault. The prefix is pinned by the contract suite against both adapters.
62
+ [/read-only console/, 400],
59
63
  [/unknown vertical /, 404],
60
64
  [/unknown version /, 404],
61
65
  [/scope has no tenant record/, 404],
62
66
  ];
63
67
  export function mapError(err) {
68
+ // A ControlPlaneError is a DELIBERATE downstream answer, not an unreviewed throw —
69
+ // the VerticalClient wraps the vertical's own JSON status/message in it. Passing it
70
+ // through verbatim is what lets an honest refusal (e.g. auth-server's 501 for an
71
+ // unimplemented verb) reach the dashboard as itself, instead of collapsing into the
72
+ // generic 500 below (the shape of the 2026-07-25 incident, on this side of the seam).
73
+ // Several routes hand-catch it already; this makes the boundary consistent for the rest.
74
+ if (err instanceof ControlPlaneError) {
75
+ return { status: err.status, body: { error: err.message } };
76
+ }
64
77
  const message = err instanceof Error ? err.message : String(err);
65
78
  for (const [pattern, status] of STATUS_PATTERNS) {
66
79
  if (pattern.test(message))
@@ -1 +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,iFAAiF;IACjF,mFAAmF;IACnF,CAAC,uBAAuB,EAAE,GAAG,CAAC;IAC9B,iFAAiF;IACjF,8EAA8E;IAC9E,kEAAkE;IAClE,CAAC,cAAc,EAAE,GAAG,CAAC;IACrB,CAAC,kCAAkC,EAAE,GAAG,CAAC;IACzC,CAAC,qBAAqB,EAAE,GAAG,CAAC;IAC5B,CAAC,cAAc,EAAE,GAAG,CAAC;IACrB,CAAC,cAAc,EAAE,GAAG,CAAC;IACrB,CAAC,sCAAsC,EAAE,GAAG,CAAC;IAC7C,+EAA+E;IAC/E,2EAA2E;IAC3E,CAAC,iBAAiB,EAAE,GAAG,CAAC;IACxB,6EAA6E;IAC7E,oEAAoE;IACpE,CAAC,iBAAiB,EAAE,GAAG,CAAC;IACxB,CAAC,0BAA0B,EAAE,GAAG,CAAC;IACjC,CAAC,gBAAgB,EAAE,GAAG,CAAC;IACvB,sFAAsF;IACtF,CAAC,gBAAgB,EAAE,GAAG,CAAC;IACvB,CAAC,mBAAmB,EAAE,GAAG,CAAC;IAC1B,CAAC,kBAAkB,EAAE,GAAG,CAAC;IACzB,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"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;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,iFAAiF;IACjF,mFAAmF;IACnF,CAAC,uBAAuB,EAAE,GAAG,CAAC;IAC9B,iFAAiF;IACjF,8EAA8E;IAC9E,kEAAkE;IAClE,CAAC,cAAc,EAAE,GAAG,CAAC;IACrB,CAAC,kCAAkC,EAAE,GAAG,CAAC;IACzC,CAAC,qBAAqB,EAAE,GAAG,CAAC;IAC5B,CAAC,cAAc,EAAE,GAAG,CAAC;IACrB,CAAC,cAAc,EAAE,GAAG,CAAC;IACrB,CAAC,sCAAsC,EAAE,GAAG,CAAC;IAC7C,+EAA+E;IAC/E,2EAA2E;IAC3E,CAAC,iBAAiB,EAAE,GAAG,CAAC;IACxB,6EAA6E;IAC7E,oEAAoE;IACpE,CAAC,iBAAiB,EAAE,GAAG,CAAC;IACxB,CAAC,0BAA0B,EAAE,GAAG,CAAC;IACjC,CAAC,gBAAgB,EAAE,GAAG,CAAC;IACvB,sFAAsF;IACtF,CAAC,gBAAgB,EAAE,GAAG,CAAC;IACvB,mFAAmF;IACnF,kFAAkF;IAClF,CAAC,mBAAmB,EAAE,GAAG,CAAC;IAC1B,CAAC,mBAAmB,EAAE,GAAG,CAAC;IAC1B,CAAC,kBAAkB,EAAE,GAAG,CAAC;IACzB,CAAC,4BAA4B,EAAE,GAAG,CAAC;CACpC,CAAC;AAOF,MAAM,UAAU,QAAQ,CAAC,GAAY;IACnC,mFAAmF;IACnF,oFAAoF;IACpF,iFAAiF;IACjF,oFAAoF;IACpF,sFAAsF;IACtF,yFAAyF;IACzF,IAAI,GAAG,YAAY,iBAAiB,EAAE,CAAC;QACrC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAA8B,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;IACtF,CAAC;IACD,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 CHANGED
@@ -1,13 +1,17 @@
1
1
  export { createControlPlaneApi } from './api.js';
2
2
  export type { ControlPlaneApiOptions } from './api.js';
3
- export { DEV_ACTOR_HEADER, SERVICE_TOKEN_HEADER, UNSAFE_devPlatformActorAuth, sessionPlatformAuth, staffAllowlist, serviceTokenAuth, firstPlatformActorAuth, } from './auth.js';
3
+ export { DEV_ACTOR_HEADER, SERVICE_TOKEN_HEADER, UNSAFE_devPlatformActorAuth, sessionPlatformAuth, staffAllowlist, serviceTokenAuth, firstPlatformActorAuth, firstBuilderAuth, } from './auth.js';
4
+ export { mintPushToken, verifyPushToken, pushTokenBuilderAuth, pushActorFor } from './push-token.js';
4
5
  export type { PlatformActorAuth, StaffIdentity, StaffSessionReader, StaffActorResolver, BuilderIdentity, BuilderAuth, Principal, } from './auth.js';
5
6
  export { ControlPlaneClient, ControlPlaneError } from './client.js';
6
7
  export type { ControlPlaneClientOptions, ClientProvisionScopeInput } from './client.js';
7
8
  export { VerticalClient } from './vertical-client.js';
8
- export type { VerticalClientOptions, ProvisionInstanceInput, ProvisionedInstance, } from './vertical-client.js';
9
+ export type { VerticalClientOptions, ProvisionInstanceInput, ProvisionedInstance, ConfigureInstanceInput, } from './vertical-client.js';
9
10
  export { assertSandboxContract, deployManifest, deploymentRefFor } from './deploy.js';
10
11
  export type { DeployVerticalFn, VerticalBundle, DeclaredBinding, DeployManifest } from './deploy.js';
11
12
  export { createWfpUploader } from './wfp.js';
12
13
  export type { WfpUploaderOptions } from './wfp.js';
14
+ export type { ObservabilityReader, ServiceMetricsRow, RecentLogEvent } from './observability.js';
15
+ export { createCfObservabilityReader } from './cf-observability.js';
16
+ export type { CfObservabilityOptions } from './cf-observability.js';
13
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,SAAS,GACV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACtF,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC"}
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,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACrG,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,SAAS,GACV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACtF,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACjG,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AACpE,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  export { createControlPlaneApi } from './api.js';
2
- export { DEV_ACTOR_HEADER, SERVICE_TOKEN_HEADER, UNSAFE_devPlatformActorAuth, sessionPlatformAuth, staffAllowlist, serviceTokenAuth, firstPlatformActorAuth, } from './auth.js';
2
+ export { DEV_ACTOR_HEADER, SERVICE_TOKEN_HEADER, UNSAFE_devPlatformActorAuth, sessionPlatformAuth, staffAllowlist, serviceTokenAuth, firstPlatformActorAuth, firstBuilderAuth, } from './auth.js';
3
+ export { mintPushToken, verifyPushToken, pushTokenBuilderAuth, pushActorFor } from './push-token.js';
3
4
  export { ControlPlaneClient, ControlPlaneError } from './client.js';
4
5
  export { VerticalClient } from './vertical-client.js';
5
6
  export { assertSandboxContract, deployManifest, deploymentRefFor } from './deploy.js';
6
7
  export { createWfpUploader } from './wfp.js';
8
+ export { createCfObservabilityReader } from './cf-observability.js';
7
9
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,WAAW,CAAC;AAUnB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAMtD,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEtF,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAUrG,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAOtD,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEtF,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/mask.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { ScopeDumpTable } from '@substrat-run/contracts';
2
+ /** Mask one dump in place-shape (returns new arrays; never mutates the input). */
3
+ export declare function maskDump(tables: ScopeDumpTable[]): ScopeDumpTable[];
4
+ //# sourceMappingURL=mask.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mask.d.ts","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAoD9D,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAqBnE"}
package/dist/mask.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The default masking pass over a scope dump (preview-and-snapshots.md §6/§8).
3
+ *
4
+ * A `scope pull` moves real customer data out of the governed environment, so the
5
+ * dump is masked BY DEFAULT and full fidelity is the explicit break-glass flag.
6
+ * This is the GENERIC sweep — §10's open question 2 answered for v1: a name-based
7
+ * column heuristic, with declarative per-vertical redaction rules as the later
8
+ * refinement. Two rules:
9
+ *
10
+ * 1. A string cell in a column whose name matches the PII heuristic is replaced
11
+ * with `[masked]`. Non-strings (ids, counts, flags) pass through — the sweep
12
+ * targets free text, and ids are what make the copy debuggable at all.
13
+ * 2. A string cell in a JSON-carrying column (`payload`, `detail`, `before`,
14
+ * `after`, `data`) that parses as JSON is swept RECURSIVELY by key with the
15
+ * same heuristic, then re-serialized — fat event payloads keep their shape
16
+ * (consumers and timelines stay debuggable) while the PII fields inside them
17
+ * are masked.
18
+ *
19
+ * Deliberately lossy and deliberately dumb: a heuristic sweep can miss a column
20
+ * named `x7`, which is why the pull is ALSO staff-gated, audited, and
21
+ * jurisdiction-checked — masking is one layer of §6's defense, not the gate.
22
+ */
23
+ // Free-text/PII column names. `name` is included on purpose: entity names
24
+ // (customers, properties, contacts) are customer data even when they look benign.
25
+ const PII_COLUMN = /(^|_)(email|e?mail_address|phone|mobile|tel|address|street|city|postal|zip|ssn|personnummer|name|first_name|last_name|full_name|contact|note|notes|comment|comments|message|subject|body|description)($|_)/i;
26
+ // Columns that carry JSON documents worth sweeping by key rather than blanking.
27
+ const JSON_COLUMN = /(^|_)(payload|detail|details|data|before|after)($|_)/i;
28
+ const MASKED = '[masked]';
29
+ // Column names are snake_case but JSON payload keys are camelCase — normalize to
30
+ // snake before testing so `customerEmail` matches the same heuristic as `email`.
31
+ const matchesPii = (name) => PII_COLUMN.test(name.replace(/([a-z0-9])([A-Z])/g, '$1_$2'));
32
+ function maskJsonValue(value, keyMatched) {
33
+ if (typeof value === 'string')
34
+ return keyMatched ? MASKED : value;
35
+ if (Array.isArray(value))
36
+ return value.map((v) => maskJsonValue(v, keyMatched));
37
+ if (value !== null && typeof value === 'object') {
38
+ const out = {};
39
+ for (const [k, v] of Object.entries(value)) {
40
+ out[k] = maskJsonValue(v, keyMatched || matchesPii(k));
41
+ }
42
+ return out;
43
+ }
44
+ return value;
45
+ }
46
+ /** Mask one dump in place-shape (returns new arrays; never mutates the input). */
47
+ export function maskDump(tables) {
48
+ return tables.map((t) => {
49
+ const piiCols = t.columns.map((c) => matchesPii(c));
50
+ const jsonCols = t.columns.map((c) => JSON_COLUMN.test(c));
51
+ if (!piiCols.some(Boolean) && !jsonCols.some(Boolean))
52
+ return t;
53
+ const rows = t.rows.map((row) => row.map((cell, i) => {
54
+ if (typeof cell !== 'string')
55
+ return cell;
56
+ if (piiCols[i])
57
+ return MASKED;
58
+ if (jsonCols[i]) {
59
+ try {
60
+ return JSON.stringify(maskJsonValue(JSON.parse(cell), false));
61
+ }
62
+ catch {
63
+ return cell; // not JSON — leave it; the column heuristic did not claim it
64
+ }
65
+ }
66
+ return cell;
67
+ }));
68
+ return { ...t, rows };
69
+ });
70
+ }
71
+ //# sourceMappingURL=mask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mask.js","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,0EAA0E;AAC1E,kFAAkF;AAClF,MAAM,UAAU,GAAG,6MAA6M,CAAC;AAEjO,gFAAgF;AAChF,MAAM,WAAW,GAAG,uDAAuD,CAAC;AAE5E,MAAM,MAAM,GAAG,UAAU,CAAC;AAE1B,iFAAiF;AACjF,iFAAiF;AACjF,MAAM,UAAU,GAAG,CAAC,IAAY,EAAW,EAAE,CAC3C,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC,CAAC;AAE/D,SAAS,aAAa,CAAC,KAAc,EAAE,UAAmB;IACxD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;IAClE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;IAChF,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,GAAG,GAA4B,EAAE,CAAC;QACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;YACtE,GAAG,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,EAAE,UAAU,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,QAAQ,CAAC,MAAwB;IAC/C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,CAAC,CAAC;QAChE,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAC9B,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;YAClB,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC1C,IAAI,OAAO,CAAC,CAAC,CAAC;gBAAE,OAAO,MAAM,CAAC;YAC9B,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChB,IAAI,CAAC;oBACH,OAAO,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;gBAChE,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,IAAI,CAAC,CAAC,6DAA6D;gBAC5E,CAAC;YACH,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CACH,CAAC;QACF,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The observability read seam (design/observability.md §4.1) — what the console's
3
+ * fleet view and (later, owner-narrowed) the dashboard's builder view consume.
4
+ *
5
+ * This file is the CONTRACT, deliberately free of provider vocabulary: a "service"
6
+ * is a deployed unit of code (a Cloudflare worker script today, a container
7
+ * tomorrow), a "namespace" is whatever pool the platform runs pushed verticals in
8
+ * (a WfP dispatch namespace today). The same posture as `DeployVerticalFn` in
9
+ * `deploy.ts`: the seam lives here, each provider's implementation lives in its own
10
+ * module (`cf-observability.ts` for Cloudflare), and the host injects one — so an
11
+ * APM/OTel backend can slot in behind the identical routes later
12
+ * (master-plan §5.7, §6 "Convention + adapter") without touching this package's
13
+ * consumers. Absent ⇒ the observability routes 501, the platform's standard shape
14
+ * for an unconfigured capability.
15
+ */
16
+ /** One service's invocation aggregates over the queried window. */
17
+ export interface ServiceMetricsRow {
18
+ service: string;
19
+ /** The pushed-vertical pool the service runs in, or null for platform services. */
20
+ namespace: string | null;
21
+ requests: number;
22
+ errors: number;
23
+ subrequests: number;
24
+ /** Per-request CPU time quantiles, microseconds. */
25
+ cpuTimeP50: number;
26
+ cpuTimeP99: number;
27
+ }
28
+ export interface RecentLogEvent {
29
+ /** Unix ms, when the event was recorded. */
30
+ timestamp: number | null;
31
+ level: string | null;
32
+ message: string | null;
33
+ service: string | null;
34
+ /** How the invocation ended (e.g. `ok`, `exception`), provider-worded. */
35
+ outcome: string | null;
36
+ /** The event as the backend returned it — the fields above are a best-effort
37
+ * projection, and the raw event is what makes a projection miss debuggable. */
38
+ raw: unknown;
39
+ }
40
+ export interface ObservabilityReader {
41
+ /** Per-service invocation metrics for the trailing window (fleet + builder views). */
42
+ serviceMetrics(input: {
43
+ hours: number;
44
+ }): Promise<ServiceMetricsRow[]>;
45
+ /** Recent log events, optionally narrowed to one service and/or level. */
46
+ recentLogs(input: {
47
+ service?: string;
48
+ level?: string;
49
+ hours: number;
50
+ limit: number;
51
+ }): Promise<RecentLogEvent[]>;
52
+ }
53
+ //# sourceMappingURL=observability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,mEAAmE;AACnE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,4CAA4C;IAC5C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,0EAA0E;IAC1E,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB;oFACgF;IAChF,GAAG,EAAE,OAAO,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,sFAAsF;IACtF,cAAc,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;IACvE,0EAA0E;IAC1E,UAAU,CAAC,KAAK,EAAE;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;CAC/B"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The observability read seam (design/observability.md §4.1) — what the console's
3
+ * fleet view and (later, owner-narrowed) the dashboard's builder view consume.
4
+ *
5
+ * This file is the CONTRACT, deliberately free of provider vocabulary: a "service"
6
+ * is a deployed unit of code (a Cloudflare worker script today, a container
7
+ * tomorrow), a "namespace" is whatever pool the platform runs pushed verticals in
8
+ * (a WfP dispatch namespace today). The same posture as `DeployVerticalFn` in
9
+ * `deploy.ts`: the seam lives here, each provider's implementation lives in its own
10
+ * module (`cf-observability.ts` for Cloudflare), and the host injects one — so an
11
+ * APM/OTel backend can slot in behind the identical routes later
12
+ * (master-plan §5.7, §6 "Convention + adapter") without touching this package's
13
+ * consumers. Absent ⇒ the observability routes 501, the platform's standard shape
14
+ * for an unconfigured capability.
15
+ */
16
+ export {};
17
+ //# sourceMappingURL=observability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.js","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
@@ -0,0 +1,33 @@
1
+ import { type TenantId } from '@substrat-run/contracts';
2
+ import { type BuilderAuth } from './auth.js';
3
+ interface PushTokenClaim {
4
+ v: 1;
5
+ tenantId: string;
6
+ tenantSlug: string;
7
+ actor: string;
8
+ iat: number;
9
+ }
10
+ /**
11
+ * The audited subject for a tenant's CI pushes — deterministic from the tenant, so
12
+ * every push from that tenant's pipelines lands in the admin log under ONE nameable
13
+ * actor (the mirror of builder-auth's `builderActorFor`, which is per-human; CI is
14
+ * per-tenant because the pipeline, not a person, is what authenticated).
15
+ */
16
+ export declare function pushActorFor(tenantId: TenantId): Promise<string>;
17
+ /** Mint a push token for one tenant. `iat` is informational (no expiry in v1). */
18
+ export declare function mintPushToken(secret: string, identity: {
19
+ actor: string;
20
+ tenantId: TenantId;
21
+ tenantSlug: string;
22
+ }): Promise<string>;
23
+ /** Verify a push token string → its claim, or null (bad prefix/shape/signature). */
24
+ export declare function verifyPushToken(secret: string, token: string): Promise<PushTokenClaim | null>;
25
+ /**
26
+ * A `BuilderAuth` over push tokens: reads `x-service-token`, handles only `spt1.…`
27
+ * values (anything else → null, falling through to the other readers), verifies, and
28
+ * returns the builder principal the token carries. Compose with the OIDC reader via
29
+ * `firstBuilderAuth`.
30
+ */
31
+ export declare function pushTokenBuilderAuth(secret: string): BuilderAuth;
32
+ export {};
33
+ //# sourceMappingURL=push-token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-token.d.ts","sourceRoot":"","sources":["../src/push-token.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+C,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACrG,OAAO,EAAwB,KAAK,WAAW,EAAwB,MAAM,WAAW,CAAC;AA6BzF,UAAU,cAAc;IACtB,CAAC,EAAE,CAAC,CAAC;IACL,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAgBD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAOtE;AAED,kFAAkF;AAClF,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAClE,OAAO,CAAC,MAAM,CAAC,CAYjB;AAED,oFAAoF;AACpF,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA4BnG;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAYhE"}
@@ -0,0 +1,114 @@
1
+ import { platformActorId, tenantId as tenantIdSchema } from '@substrat-run/contracts';
2
+ import { SERVICE_TOKEN_HEADER } from './auth.js';
3
+ /**
4
+ * Tenant-scoped push tokens — the CI credential the builder plane was missing.
5
+ *
6
+ * A builder's OIDC session (builder-auth in the worker) is per-human and short-lived;
7
+ * the platform `SERVICE_TOKEN` is staff-equivalent and must never land in a customer
8
+ * repo. A push token sits between: a long-lived MACHINE credential that authenticates
9
+ * as a **builder principal** for ONE tenant — so it reaches only the builder-allowlisted
10
+ * routes, only that tenant's `<tenantSlug>/…` namespace, and never prod promotion or
11
+ * admission (those checks live in api.ts and apply to every builder alike).
12
+ *
13
+ * Format: `spt1.<b64url payload>.<b64url HMAC-SHA256 sig>` — stateless, verified
14
+ * against a dedicated `pushTokenSecret`. Deliberately NOT the platform secrets:
15
+ * PLATFORM_SECRET is injected into every pushed vertical (a vertical holding the
16
+ * push-token signing key could forge tokens for any tenant), and deriving from
17
+ * SERVICE_TOKEN would tie CI-credential rotation to service-token rotation. Rotating
18
+ * the dedicated secret invalidates every issued push token (customers reconnect) —
19
+ * that is the whole revocation story in v1, so treat it as set-once like the
20
+ * dashboard's SECRET_BOX_KEY and keep it out of routine rotation.
21
+ *
22
+ * The token is presented in the SAME header the CLI already sends its credential in
23
+ * (`x-service-token`, via `SUBSTRAT_SERVICE_TOKEN`), discriminated by the `spt1.`
24
+ * prefix — a random-hex service token can never collide with it, and the CLI and the
25
+ * generated CI workflow need no changes at all.
26
+ */
27
+ const PREFIX = 'spt1';
28
+ const enc = new TextEncoder();
29
+ const b64url = (bytes) => btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
30
+ const b64urlToBytes = (s) => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
31
+ async function hmacKey(secret, usage) {
32
+ return crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, [usage]);
33
+ }
34
+ const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
35
+ /**
36
+ * The audited subject for a tenant's CI pushes — deterministic from the tenant, so
37
+ * every push from that tenant's pipelines lands in the admin log under ONE nameable
38
+ * actor (the mirror of builder-auth's `builderActorFor`, which is per-human; CI is
39
+ * per-tenant because the pipeline, not a person, is what authenticated).
40
+ */
41
+ export async function pushActorFor(tenantId) {
42
+ const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', enc.encode(`push-token:${tenantId}`)));
43
+ let s = '';
44
+ for (let i = 0; i < 26; i++)
45
+ s += CROCKFORD[digest[i] % 32];
46
+ return platformActorId.parse(s);
47
+ }
48
+ /** Mint a push token for one tenant. `iat` is informational (no expiry in v1). */
49
+ export async function mintPushToken(secret, identity) {
50
+ const claim = {
51
+ v: 1,
52
+ tenantId: identity.tenantId,
53
+ tenantSlug: identity.tenantSlug,
54
+ actor: identity.actor,
55
+ iat: Date.now(),
56
+ };
57
+ const payload = b64url(enc.encode(JSON.stringify(claim)));
58
+ const signingInput = `${PREFIX}.${payload}`;
59
+ const sig = new Uint8Array(await crypto.subtle.sign('HMAC', await hmacKey(secret, 'sign'), enc.encode(signingInput)));
60
+ return `${signingInput}.${b64url(sig)}`;
61
+ }
62
+ /** Verify a push token string → its claim, or null (bad prefix/shape/signature). */
63
+ export async function verifyPushToken(secret, token) {
64
+ const parts = token.split('.');
65
+ if (parts.length !== 3 || parts[0] !== PREFIX)
66
+ return null;
67
+ const [, payload, sig] = parts;
68
+ let ok = false;
69
+ try {
70
+ ok = await crypto.subtle.verify('HMAC', await hmacKey(secret, 'verify'), b64urlToBytes(sig), enc.encode(`${PREFIX}.${payload}`));
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ if (!ok)
76
+ return null;
77
+ try {
78
+ const claim = JSON.parse(new TextDecoder().decode(b64urlToBytes(payload)));
79
+ if (claim.v !== 1)
80
+ return null;
81
+ // Parse, don't trust — the signature proves WE minted it, the parse proves the
82
+ // fields still are what a BuilderIdentity needs (a future format bump fails closed).
83
+ tenantIdSchema.parse(claim.tenantId);
84
+ platformActorId.parse(claim.actor);
85
+ if (typeof claim.tenantSlug !== 'string' || !claim.tenantSlug)
86
+ return null;
87
+ return claim;
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * A `BuilderAuth` over push tokens: reads `x-service-token`, handles only `spt1.…`
95
+ * values (anything else → null, falling through to the other readers), verifies, and
96
+ * returns the builder principal the token carries. Compose with the OIDC reader via
97
+ * `firstBuilderAuth`.
98
+ */
99
+ export function pushTokenBuilderAuth(secret) {
100
+ return async (request) => {
101
+ const presented = request.headers.get(SERVICE_TOKEN_HEADER);
102
+ if (!presented || !presented.startsWith(`${PREFIX}.`))
103
+ return null;
104
+ const claim = await verifyPushToken(secret, presented);
105
+ if (!claim)
106
+ return null;
107
+ return {
108
+ actor: platformActorId.parse(claim.actor),
109
+ tenantId: tenantIdSchema.parse(claim.tenantId),
110
+ tenantSlug: claim.tenantSlug,
111
+ };
112
+ };
113
+ }
114
+ //# sourceMappingURL=push-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-token.js","sourceRoot":"","sources":["../src/push-token.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import type { PrincipalId, ReadScopeTableInput, ScopeId, ScopeTable, ScopeTablePage, TenantId } from '@substrat-run/contracts';
1
+ import type { PrincipalId, QueryScopeInput, ReadScopeTableInput, ScopeDumpTable, ScopeId, ScopeQueryResult, ScopeTable, ScopeTablePage, TenantId } from '@substrat-run/contracts';
2
2
  /**
3
3
  * The platform's client for calling a VERTICAL (K-31).
4
4
  *
@@ -8,10 +8,16 @@ import type { PrincipalId, ReadScopeTableInput, ScopeId, ScopeTable, ScopeTableP
8
8
  * because only the vertical can create a usable scope DO — the DO class bundles the
9
9
  * modules and lives in the vertical's own deployment.
10
10
  *
11
- * Deliberately tiny. The platform asks a vertical to do two kinds of thing: create an
12
- * instance (K-31), and — read-only — introspect a scope's own database (§5.4), because
13
- * the scope's data DO lives in the vertical's deployment, not the platform's. Every
14
- * other verb would be authority the platform holds over someone else's code.
11
+ * Deliberately tiny. The platform asks a vertical to do three kinds of thing: create an
12
+ * instance (K-31); — read-only — introspect a scope's own database (§5.4), because the
13
+ * scope's data DO lives in the vertical's deployment, not the platform's; and manage
14
+ * scope-STORAGE lifecycle snapshot a scope into a sibling, wipe a reaped fork
15
+ * (preview-and-snapshots.md §9, the ratified trust line: infrastructure verbs over the
16
+ * DO's storage, extending the authority provisionInstance already asserts). Every other
17
+ * verb — anything that reads or writes DOMAIN data — would be authority the platform
18
+ * holds over someone else's code. Note the lifecycle verbs move no data across the
19
+ * boundary: a snapshot copies between two DOs inside the vertical's own deployment and
20
+ * returns only a table count.
15
21
  */
16
22
  export interface VerticalClientOptions {
17
23
  /**
@@ -32,6 +38,24 @@ export interface ProvisionInstanceInput {
32
38
  owner: PrincipalId;
33
39
  slug: string;
34
40
  name: string;
41
+ /**
42
+ * Per-instance config delivered WITH provisioning, so a new app arrives configured
43
+ * atomically — no window where the instance is live but unconfigured (an issuer with
44
+ * no admin, an app with no auth). Same entries `configureInstance` upserts later; a
45
+ * vertical that predates the field ignores it (its body parse strips unknown keys).
46
+ */
47
+ config?: Record<string, string>;
48
+ }
49
+ export interface ConfigureInstanceInput {
50
+ /** The scope's tenant — CP-less verticals shard identity/config storage per tenant
51
+ * (e.g. Meridian's IdentityDO is addressed by tenant id), so the address rides along. */
52
+ tenantId: TenantId;
53
+ scopeId: ScopeId;
54
+ /** Upserts, key by key — never a full replace, so partial writes compose. */
55
+ entries: Array<{
56
+ key: string;
57
+ value: string;
58
+ }>;
35
59
  }
36
60
  export interface ProvisionedInstance {
37
61
  tenantId: TenantId;
@@ -49,6 +73,15 @@ export declare class VerticalClient {
49
73
  * phase of a two-phase creation and the reconciliation sweep re-runs exactly it.
50
74
  */
51
75
  provisionInstance(input: ProvisionInstanceInput): Promise<ProvisionedInstance>;
76
+ /**
77
+ * Deliver per-instance CONFIG to the scope's own storage (vertical-auth-detach.md
78
+ * §2.2) — the write half of the dashboard's Env tab, and the same trust line as
79
+ * provisionInstance: the platform names a scope inside the vertical's deployment and
80
+ * hands it settings; the vertical owns what they mean. Idempotent upserts, so the
81
+ * reconciliation sweep can re-run it. A vertical that has no live-config support
82
+ * answers 501, which the caller may treat as "authored but not delivered".
83
+ */
84
+ configureInstance(input: ConfigureInstanceInput): Promise<void>;
52
85
  /**
53
86
  * Read the scope's OWN database tables (kernel-design §5.4 admin-query RPC).
54
87
  *
@@ -60,6 +93,44 @@ export declare class VerticalClient {
60
93
  listScopeTables(scopeId: ScopeId): Promise<ScopeTable[]>;
61
94
  /** A bounded page of one of the scope's tables. */
62
95
  readScopeTable(scopeId: ScopeId, input: ReadScopeTableInput): Promise<ScopeTablePage>;
96
+ /**
97
+ * One read-only SQL statement against the scope's DB — the console (#219). The
98
+ * vertical enforces read-only-ness in its own deployment (the kernel gate + the
99
+ * DO's rolled-back transaction); a vertical that cannot answer safely (e.g. one
100
+ * that redacts secret columns on table reads) refuses with its own status, which
101
+ * the ControlPlaneError relays verbatim.
102
+ */
103
+ queryScope(scopeId: ScopeId, input: QueryScopeInput): Promise<ScopeQueryResult>;
104
+ /**
105
+ * Copy one scope's data into a fresh sibling scope DO, inside the vertical's own
106
+ * deployment (§9's data half). The platform names source and destination; the bytes
107
+ * never cross the boundary — the response is a table count, not a dump. The
108
+ * directory half (provenance row, activation, version bind) is the caller's job.
109
+ */
110
+ snapshotScope(input: {
111
+ sourceScopeId: ScopeId;
112
+ newScopeId: ScopeId;
113
+ }): Promise<{
114
+ tables: number;
115
+ }>;
116
+ /**
117
+ * Wipe a reaped fork's storage (§9's reap half). The platform calls this before
118
+ * deleting the directory row — same storage-before-row ordering as the in-process
119
+ * deleteSnapshot, so a crash between the two converges on retry. The fork-only
120
+ * refusal lives with the directory record, on the platform's side.
121
+ */
122
+ deleteScope(input: {
123
+ scopeId: ScopeId;
124
+ }): Promise<void>;
125
+ /**
126
+ * The scope's full dump — the ONE verb here that deliberately moves scope bytes
127
+ * across the boundary, for the governed `scope pull` (§8). The control-plane route
128
+ * in front of it is the gate: staff-only, audited, masked by default, jurisdiction-
129
+ * checked. Everything else on this surface stays byte-free by design.
130
+ */
131
+ exportScope(scopeId: ScopeId): Promise<ScopeDumpTable[]>;
132
+ /** A platform-authenticated POST to the vertical's `/internal/*` surface. */
133
+ private postInternal;
63
134
  /** A platform-authenticated GET to the vertical's `/internal/*` surface. */
64
135
  private getInternal;
65
136
  }
@@ -1 +1 @@
1
- {"version":3,"file":"vertical-client.d.ts","sourceRoot":"","sources":["../src/vertical-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,OAAO,EACP,UAAU,EACV,cAAc,EACd,QAAQ,EACT,MAAM,yBAAyB,CAAC;AAIjC;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,wDAAwD;IACxD,KAAK,EAAE,WAAW,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;CACpB;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,EAAE,qBAAqB,EAAI;IAE/D;;;;;;OAMG;IACG,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAqBnF;IAED;;;;;;;OAOG;IACG,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAE7D;IAED,mDAAmD;IAC7C,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC,CAS1F;IAED,4EAA4E;YAC9D,WAAW;CAc1B"}
1
+ {"version":3,"file":"vertical-client.d.ts","sourceRoot":"","sources":["../src/vertical-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,OAAO,EACP,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,QAAQ,EACT,MAAM,yBAAyB,CAAC;AAIjC;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qEAAqE;IACrE,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,wDAAwD;IACxD,KAAK,EAAE,WAAW,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,sBAAsB;IACrC;8FAC0F;IAC1F,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;CACpB;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,EAAE,qBAAqB,EAAI;IAE/D;;;;;;OAMG;IACG,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAqBnF;IAED;;;;;;;OAOG;IACG,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpE;IAED;;;;;;;OAOG;IACG,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAE7D;IAED,mDAAmD;IAC7C,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC,CAS1F;IAED;;;;;;OAMG;IACG,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAEpF;IAED;;;;;OAKG;IACG,aAAa,CAAC,KAAK,EAAE;QACzB,aAAa,EAAE,OAAO,CAAC;QACvB,UAAU,EAAE,OAAO,CAAC;KACrB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAE9B;IAED;;;;;OAKG;IACG,WAAW,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5D;IAED;;;;;OAKG;IACG,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAI7D;IAED,6EAA6E;YAC/D,YAAY;IAoB1B,4EAA4E;YAC9D,WAAW;CAc1B"}
@@ -30,6 +30,17 @@ export class VerticalClient {
30
30
  }
31
31
  return (await res.json());
32
32
  }
33
+ /**
34
+ * Deliver per-instance CONFIG to the scope's own storage (vertical-auth-detach.md
35
+ * §2.2) — the write half of the dashboard's Env tab, and the same trust line as
36
+ * provisionInstance: the platform names a scope inside the vertical's deployment and
37
+ * hands it settings; the vertical owns what they mean. Idempotent upserts, so the
38
+ * reconciliation sweep can re-run it. A vertical that has no live-config support
39
+ * answers 501, which the caller may treat as "authored but not delivered".
40
+ */
41
+ async configureInstance(input) {
42
+ await this.postInternal('/internal/configure', input, 'configure');
43
+ }
33
44
  /**
34
45
  * Read the scope's OWN database tables (kernel-design §5.4 admin-query RPC).
35
46
  *
@@ -50,6 +61,60 @@ export class VerticalClient {
50
61
  });
51
62
  return this.getInternal(`/internal/tables/${encodeURIComponent(input.table)}?${q}`);
52
63
  }
64
+ /**
65
+ * One read-only SQL statement against the scope's DB — the console (#219). The
66
+ * vertical enforces read-only-ness in its own deployment (the kernel gate + the
67
+ * DO's rolled-back transaction); a vertical that cannot answer safely (e.g. one
68
+ * that redacts secret columns on table reads) refuses with its own status, which
69
+ * the ControlPlaneError relays verbatim.
70
+ */
71
+ async queryScope(scopeId, input) {
72
+ return this.postInternal('/internal/query', { scopeId, sql: input.sql }, 'query');
73
+ }
74
+ /**
75
+ * Copy one scope's data into a fresh sibling scope DO, inside the vertical's own
76
+ * deployment (§9's data half). The platform names source and destination; the bytes
77
+ * never cross the boundary — the response is a table count, not a dump. The
78
+ * directory half (provenance row, activation, version bind) is the caller's job.
79
+ */
80
+ async snapshotScope(input) {
81
+ return this.postInternal('/internal/snapshot', input, 'snapshot');
82
+ }
83
+ /**
84
+ * Wipe a reaped fork's storage (§9's reap half). The platform calls this before
85
+ * deleting the directory row — same storage-before-row ordering as the in-process
86
+ * deleteSnapshot, so a crash between the two converges on retry. The fork-only
87
+ * refusal lives with the directory record, on the platform's side.
88
+ */
89
+ async deleteScope(input) {
90
+ await this.postInternal('/internal/delete-scope', input, 'delete-scope');
91
+ }
92
+ /**
93
+ * The scope's full dump — the ONE verb here that deliberately moves scope bytes
94
+ * across the boundary, for the governed `scope pull` (§8). The control-plane route
95
+ * in front of it is the gate: staff-only, audited, masked by default, jurisdiction-
96
+ * checked. Everything else on this surface stays byte-free by design.
97
+ */
98
+ async exportScope(scopeId) {
99
+ return this.getInternal(`/internal/export?scopeId=${encodeURIComponent(scopeId)}`);
100
+ }
101
+ /** A platform-authenticated POST to the vertical's `/internal/*` surface. */
102
+ async postInternal(path, body, verb) {
103
+ const base = this.options.baseUrl ?? 'https://vertical.invalid';
104
+ const res = await this.options.fetch(`${base}${path}`, {
105
+ method: 'POST',
106
+ headers: {
107
+ 'content-type': 'application/json',
108
+ [PLATFORM_SECRET_HEADER]: this.options.platformSecret,
109
+ },
110
+ body: JSON.stringify(body),
111
+ });
112
+ if (!res.ok) {
113
+ const parsed = (await res.json().catch(() => null));
114
+ throw new ControlPlaneError(res.status, parsed?.error ?? `vertical refused ${verb}: ${res.status} ${res.statusText}`);
115
+ }
116
+ return (await res.json());
117
+ }
53
118
  /** A platform-authenticated GET to the vertical's `/internal/*` surface. */
54
119
  async getInternal(path) {
55
120
  const base = this.options.baseUrl ?? 'https://vertical.invalid';