@pithy-sh/core 0.10.0 → 0.11.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 (180) hide show
  1. package/dist/access/access.d.ts +172 -0
  2. package/dist/access/access.d.ts.map +1 -0
  3. package/dist/access/access.js +147 -0
  4. package/dist/access/acting.d.ts +49 -0
  5. package/dist/access/acting.d.ts.map +1 -0
  6. package/dist/access/acting.js +21 -0
  7. package/dist/access/policyCheck.d.ts +33 -0
  8. package/dist/access/policyCheck.d.ts.map +1 -0
  9. package/dist/access/policyCheck.js +48 -0
  10. package/dist/access/usage.d.ts +83 -0
  11. package/dist/access/usage.d.ts.map +1 -0
  12. package/dist/access/usage.js +104 -0
  13. package/dist/access/visibility.d.ts +66 -0
  14. package/dist/access/visibility.d.ts.map +1 -0
  15. package/dist/access/visibility.js +86 -0
  16. package/dist/audit/auditEvent.d.ts +5 -0
  17. package/dist/audit/auditEvent.d.ts.map +1 -1
  18. package/dist/audit/auditEvent.js +2 -0
  19. package/dist/capability/bindings.d.ts +47 -2
  20. package/dist/capability/bindings.d.ts.map +1 -1
  21. package/dist/capability/bindings.js +117 -7
  22. package/dist/capability/capability.d.ts +98 -28
  23. package/dist/capability/capability.d.ts.map +1 -1
  24. package/dist/capability/manifest.d.ts +2 -1
  25. package/dist/capability/manifest.d.ts.map +1 -1
  26. package/dist/capability/manifest.js +1 -1
  27. package/dist/capability/settings.d.ts +86 -2
  28. package/dist/capability/settings.d.ts.map +1 -1
  29. package/dist/capability/settings.js +80 -1
  30. package/dist/cli/commandCatalog.generated.d.ts +184 -22
  31. package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
  32. package/dist/cli/commandCatalog.generated.js +196 -22
  33. package/dist/cli/commandFacts.d.ts +1 -1
  34. package/dist/cli/commandFacts.d.ts.map +1 -1
  35. package/dist/cli/commandFacts.js +1 -0
  36. package/dist/controlPlane/capability.d.ts.map +1 -1
  37. package/dist/controlPlane/capability.js +12 -2
  38. package/dist/controlPlane/context.d.ts +5 -0
  39. package/dist/controlPlane/context.d.ts.map +1 -1
  40. package/dist/controlPlane/context.js +3 -1
  41. package/dist/controlPlane/data/connection.d.ts +47 -0
  42. package/dist/controlPlane/data/connection.d.ts.map +1 -1
  43. package/dist/controlPlane/data/connection.js +55 -1
  44. package/dist/controlPlane/http/cors.d.ts +4 -4
  45. package/dist/controlPlane/http/cors.js +4 -4
  46. package/dist/controlPlane/http/guard.d.ts +11 -0
  47. package/dist/controlPlane/http/guard.d.ts.map +1 -1
  48. package/dist/controlPlane/http/guard.js +17 -1
  49. package/dist/controlPlane/http/handlers.d.ts +60 -0
  50. package/dist/controlPlane/http/handlers.d.ts.map +1 -1
  51. package/dist/controlPlane/http/handlers.js +31 -2
  52. package/dist/controlPlane/http/readiness.d.ts +33 -0
  53. package/dist/controlPlane/http/readiness.d.ts.map +1 -0
  54. package/dist/controlPlane/http/readiness.js +39 -0
  55. package/dist/controlPlane/http/responses.d.ts +147 -0
  56. package/dist/controlPlane/http/responses.d.ts.map +1 -1
  57. package/dist/controlPlane/http/responses.js +61 -4
  58. package/dist/controlPlane/http/routes.d.ts +1 -0
  59. package/dist/controlPlane/http/routes.d.ts.map +1 -1
  60. package/dist/controlPlane/http/routes.js +17 -3
  61. package/dist/controlPlane/http/verify.d.ts.map +1 -1
  62. package/dist/controlPlane/http/verify.js +2 -1
  63. package/dist/controlPlane/migrationState/migrationState.d.ts +102 -0
  64. package/dist/controlPlane/migrationState/migrationState.d.ts.map +1 -0
  65. package/dist/controlPlane/migrationState/migrationState.js +142 -0
  66. package/dist/controlPlane/migrations/0003_connection_principal.d.ts +21 -0
  67. package/dist/controlPlane/migrations/0003_connection_principal.d.ts.map +1 -0
  68. package/dist/controlPlane/migrations/0003_connection_principal.js +33 -0
  69. package/dist/controlPlane/scope/scope.d.ts +13 -2
  70. package/dist/controlPlane/scope/scope.d.ts.map +1 -1
  71. package/dist/controlPlane/scope/scope.js +14 -3
  72. package/dist/createBackend.d.ts.map +1 -1
  73. package/dist/createBackend.js +7 -2
  74. package/dist/createEntrypoint.d.ts.map +1 -1
  75. package/dist/createEntrypoint.js +5 -3
  76. package/dist/entitlement/allowance.d.ts +25 -0
  77. package/dist/entitlement/allowance.d.ts.map +1 -0
  78. package/dist/entitlement/allowance.js +80 -0
  79. package/dist/entitlement/entitlement.d.ts +41 -0
  80. package/dist/entitlement/entitlement.d.ts.map +1 -1
  81. package/dist/entitlement/meter.d.ts +57 -0
  82. package/dist/entitlement/meter.d.ts.map +1 -0
  83. package/dist/entitlement/meter.js +13 -0
  84. package/dist/error/http.d.ts +144 -0
  85. package/dist/error/http.d.ts.map +1 -1
  86. package/dist/error/payload.d.ts +288 -0
  87. package/dist/error/payload.d.ts.map +1 -1
  88. package/dist/error/payload.js +100 -4
  89. package/dist/http/authContext.d.ts +7 -0
  90. package/dist/http/authContext.d.ts.map +1 -1
  91. package/dist/http/authContext.js +8 -1
  92. package/dist/http/scopeGates.d.ts +20 -0
  93. package/dist/http/scopeGates.d.ts.map +1 -0
  94. package/dist/http/scopeGates.js +36 -0
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +2 -2
  98. package/dist/logger/record.d.ts +84 -0
  99. package/dist/logger/record.d.ts.map +1 -1
  100. package/dist/migrations/capabilitySets.d.ts +23 -0
  101. package/dist/migrations/capabilitySets.d.ts.map +1 -0
  102. package/dist/migrations/capabilitySets.js +74 -0
  103. package/dist/migrations/registry.d.ts +26 -0
  104. package/dist/migrations/registry.d.ts.map +1 -1
  105. package/dist/migrations/registry.js +38 -7
  106. package/dist/migrations/state.d.ts +60 -0
  107. package/dist/migrations/state.d.ts.map +1 -0
  108. package/dist/migrations/state.js +46 -0
  109. package/dist/naming/feature.d.ts +27 -5
  110. package/dist/naming/feature.d.ts.map +1 -1
  111. package/dist/naming/feature.js +36 -6
  112. package/dist/naming/limits.d.ts +47 -3
  113. package/dist/naming/limits.d.ts.map +1 -1
  114. package/dist/naming/limits.js +47 -3
  115. package/dist/naming/provisionScope.d.ts +6 -0
  116. package/dist/naming/provisionScope.d.ts.map +1 -1
  117. package/dist/naming/provisionScope.js +8 -3
  118. package/dist/naming/resourceNames.d.ts +21 -2
  119. package/dist/naming/resourceNames.d.ts.map +1 -1
  120. package/dist/naming/resourceNames.js +21 -3
  121. package/dist/seed/writeD1.d.ts +7 -1
  122. package/dist/seed/writeD1.d.ts.map +1 -1
  123. package/dist/seed/writeD1.js +9 -3
  124. package/dist/seed/writeKv.d.ts +2 -0
  125. package/dist/seed/writeKv.d.ts.map +1 -1
  126. package/dist/seed/writeKv.js +6 -2
  127. package/dist/version.generated.d.ts +1 -1
  128. package/dist/version.generated.js +1 -1
  129. package/dist/worker/serviceLoopback.d.ts +36 -0
  130. package/dist/worker/serviceLoopback.d.ts.map +1 -0
  131. package/dist/worker/serviceLoopback.js +74 -0
  132. package/dist/workflow/faults.d.ts +12 -1
  133. package/dist/workflow/faults.d.ts.map +1 -1
  134. package/dist/workflow/faults.js +22 -15
  135. package/package.json +2 -2
  136. package/src/access/access.ts +298 -0
  137. package/src/access/acting.ts +59 -0
  138. package/src/access/policyCheck.ts +77 -0
  139. package/src/access/usage.ts +141 -0
  140. package/src/access/visibility.ts +109 -0
  141. package/src/audit/auditEvent.ts +4 -0
  142. package/src/capability/bindings.ts +159 -13
  143. package/src/capability/capability.ts +99 -29
  144. package/src/capability/settings.ts +146 -2
  145. package/src/cli/commandCatalog.generated.ts +212 -22
  146. package/src/cli/commandFacts.ts +1 -0
  147. package/src/controlPlane/capability.ts +25 -1
  148. package/src/controlPlane/context.ts +4 -0
  149. package/src/controlPlane/data/connection.ts +90 -0
  150. package/src/controlPlane/http/cors.ts +4 -4
  151. package/src/controlPlane/http/guard.ts +21 -0
  152. package/src/controlPlane/http/handlers.ts +41 -0
  153. package/src/controlPlane/http/readiness.ts +50 -0
  154. package/src/controlPlane/http/responses.ts +140 -2
  155. package/src/controlPlane/http/routes.ts +25 -1
  156. package/src/controlPlane/http/verify.ts +2 -0
  157. package/src/controlPlane/migrationState/migrationState.ts +200 -0
  158. package/src/controlPlane/migrations/0003_connection_principal.ts +37 -0
  159. package/src/controlPlane/scope/scope.ts +14 -2
  160. package/src/createBackend.ts +24 -6
  161. package/src/createEntrypoint.ts +9 -3
  162. package/src/entitlement/allowance.ts +85 -0
  163. package/src/entitlement/entitlement.ts +38 -0
  164. package/src/entitlement/meter.ts +69 -0
  165. package/src/error/payload.ts +211 -4
  166. package/src/http/authContext.ts +13 -1
  167. package/src/http/scopeGates.ts +34 -0
  168. package/src/index.ts +1 -0
  169. package/src/migrations/capabilitySets.ts +87 -0
  170. package/src/migrations/registry.ts +46 -2
  171. package/src/migrations/state.ts +94 -0
  172. package/src/naming/feature.ts +46 -7
  173. package/src/naming/limits.ts +35 -3
  174. package/src/naming/provisionScope.ts +19 -1
  175. package/src/naming/resourceNames.ts +52 -5
  176. package/src/seed/writeD1.ts +13 -4
  177. package/src/seed/writeKv.ts +6 -2
  178. package/src/version.generated.ts +1 -1
  179. package/src/worker/serviceLoopback.ts +105 -0
  180. package/src/workflow/faults.ts +51 -24
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
4
  import { z } from "zod";
5
+ import { InternalError } from "../error/pithyError";
5
6
  import type { HostEnvReport } from "../workflow/hostEnv";
6
7
  import { hostEnvProviderSentence } from "../workflow/hostEnv";
7
8
 
@@ -115,8 +116,8 @@ export interface SettingsCheckContext {
115
116
  *
116
117
  * Closed for the reason `CapabilityHealth` is: handing a capability a raw Cloudflare client would make
117
118
  * every capability a place a network call can be invented, and would put `@pithy-sh/cloudflare` in the
118
- * dependency list of packages that need none of it. Three questions cover what the first checks ask; a
119
- * fourth lands here, once, when a capability actually needs it.
119
+ * dependency list of packages that need none of it. A question lands here once a capability actually needs
120
+ * it, and not before: six cover what the checks ask.
120
121
  *
121
122
  * Every method may throw. A throw is the account failing to answer, and the runner reports that
122
123
  * capability's account tier as unchecked rather than as a pass.
@@ -141,6 +142,47 @@ export interface SettingsAccountReader {
141
142
  * created it with, so the check looks where the value went.
142
143
  */
143
144
  storeEntry(name: string): Promise<boolean>;
145
+ /**
146
+ * The ids of the rows in one of the kit's own tables whose column holds one of `values`, in the app
147
+ * database a Worker binds as `DB` in one deployed environment (#727).
148
+ *
149
+ * Asked by media and storage after a project narrows `visibility.allowed`: narrowing never rewrites a
150
+ * record, so the records left outside the policy have to be *named*, and only the deployed database
151
+ * holds them. Closed in its own way — the table must be a `pithy_` one and the column an identifier,
152
+ * both refused otherwise, and the values are bound, never interpolated — so it reads a fact about a
153
+ * capability's own rows and cannot become a query language.
154
+ */
155
+ appRows(request: AppRowsRequest): Promise<readonly string[]>;
156
+ /**
157
+ * Whether that environment's **D1 vault** holds a `json` secret that still carries a top-level `field` —
158
+ * asked of its manager Worker, which opens the value and answers a bit, never the value (#728). `false`
159
+ * for a secret that is not stored, since it carries nothing.
160
+ *
161
+ * Asked by media of `media-storage-credentials`, which held a supplied Cloudflare token until the runtime
162
+ * token replaced it: the upgrade is finished only once the bundle has been rewritten without it.
163
+ */
164
+ vaultSecretField(request: { name: string; environment: string; field: string }): Promise<boolean>;
165
+ /**
166
+ * Whether the account holds a Cloudflare API token of exactly this name (#728). The name is the token's,
167
+ * composed with the namer that minted it — never a pattern, so a check names one credential it can see.
168
+ *
169
+ * Asked by secrets of the manager token the runtime token superseded, which nothing revokes for you.
170
+ */
171
+ accountToken(name: string): Promise<boolean>;
172
+ }
173
+
174
+ /** What {@link SettingsAccountReader.appRows} is asked. */
175
+ export interface AppRowsRequest {
176
+ /** The deployed environment whose app database is read. `dev` is refused: it has no deployed database. */
177
+ readonly environment: string;
178
+ /** The Worker's `apps/` directory — whose `wrangler.jsonc` names the database for that environment. */
179
+ readonly workerDirectory: string;
180
+ /** The kit table, as it is named in SQL — `pithy_media_assets`. Must carry the `pithy_` prefix. */
181
+ readonly table: string;
182
+ /** The column compared, as it is named in SQL — `visibility`. */
183
+ readonly column: string;
184
+ /** The values that make a row worth naming. */
185
+ readonly values: readonly string[];
144
186
  }
145
187
 
146
188
  /** What the account tier is told: everything the local tier gets, plus the account it may ask. */
@@ -149,6 +191,56 @@ export interface SettingsAccountContext extends SettingsCheckContext {
149
191
  account: SettingsAccountReader;
150
192
  }
151
193
 
194
+ /** One environment an account check could not answer for, and why. */
195
+ export interface UnansweredEnvironment {
196
+ /** The environment id. */
197
+ readonly environment: string;
198
+ /** What the account, or the project, said instead of an answer. */
199
+ readonly cause: unknown;
200
+ }
201
+
202
+ /**
203
+ * An account check that answered for some environments and not for others.
204
+ *
205
+ * A check that asks per environment must not let one unanswerable environment cost the others their
206
+ * findings: staging stood up before prod is the ordinary bring-up, and a check that threw on prod's missing
207
+ * database would throw away what it had already found in staging. So it collects what it can, and throws
208
+ * this at the end carrying those findings. The runner reports them as findings and the tier as unchecked,
209
+ * both — a fault and an unanswered question are two facts, and neither hides the other.
210
+ */
211
+ export class SettingsAccountIncomplete extends InternalError {
212
+ /** What the environments that did answer were found to have wrong. */
213
+ readonly findings: readonly SettingsFinding[];
214
+ /** The environments that did not answer, in the order they were asked. */
215
+ readonly unanswered: readonly string[];
216
+
217
+ constructor(findings: readonly SettingsFinding[], unanswered: readonly UnansweredEnvironment[]) {
218
+ const reasons = unanswered.map(({ environment, cause }) => `${environment}: ${describeCause(cause)}`).join("; ");
219
+ super(
220
+ {
221
+ message: `The account check could not answer for ${unanswered.map(({ environment }) => environment).join(", ")}.`,
222
+ detail: reasons,
223
+ },
224
+ { cause: unanswered[0]?.cause },
225
+ );
226
+ this.findings = findings;
227
+ this.unanswered = unanswered.map(({ environment }) => environment);
228
+ }
229
+ }
230
+
231
+ /** A cause, as one line. */
232
+ function describeCause(cause: unknown): string {
233
+ return cause instanceof Error ? cause.message : String(cause);
234
+ }
235
+
236
+ /**
237
+ * The findings an account check reached before it threw — none for an ordinary throw, and what it had
238
+ * collected for a {@link SettingsAccountIncomplete}.
239
+ */
240
+ export function settingsFindingsBefore(error: unknown): readonly SettingsFinding[] {
241
+ return error instanceof SettingsAccountIncomplete ? error.findings : [];
242
+ }
243
+
152
244
  /**
153
245
  * A capability's settings check, as it hangs off the capability.
154
246
  *
@@ -163,6 +255,58 @@ export interface CapabilitySettings {
163
255
  account?(context: SettingsAccountContext): Promise<SettingsFinding[]>;
164
256
  }
165
257
 
258
+ /**
259
+ * **Two or more checks on one capability, as the one `settings` it declares (#728).** Media names records a
260
+ * narrowed visibility no longer admits and a bundle still holding a superseded token: two questions, one
261
+ * capability, one seam.
262
+ *
263
+ * Each tier runs every check's, in declaration order. A check that could not answer for some environments
264
+ * costs those environments and nothing else: the others' findings are kept, and the run ends in one
265
+ * {@link SettingsAccountIncomplete} carrying every finding and every unanswered environment. A check that
266
+ * throws outright is unanswered for every environment the context declares.
267
+ */
268
+ export function allSettings(...checks: readonly CapabilitySettings[]): CapabilitySettings {
269
+ const accounts = checks.flatMap((check) => (check.account ? [check.account.bind(check)] : []));
270
+ return {
271
+ async local(context) {
272
+ const findings: SettingsFinding[] = [];
273
+ for (const check of checks) findings.push(...(await check.local(context)));
274
+ return findings;
275
+ },
276
+ ...(accounts.length === 0
277
+ ? {}
278
+ : {
279
+ async account(context) {
280
+ const findings: SettingsFinding[] = [];
281
+ const unanswered: UnansweredEnvironment[] = [];
282
+ for (const account of accounts) {
283
+ try {
284
+ findings.push(...(await account(context)));
285
+ } catch (cause) {
286
+ findings.push(...settingsFindingsBefore(cause));
287
+ if (cause instanceof SettingsAccountIncomplete) {
288
+ for (const environment of cause.unanswered) {
289
+ if (!unanswered.some((entry) => entry.environment === environment)) {
290
+ unanswered.push({ environment, cause: cause.cause ?? cause });
291
+ }
292
+ }
293
+ continue;
294
+ }
295
+ // With no environment to name, there is nothing the throw could be reported against.
296
+ if (context.environments.length === 0) throw cause;
297
+ for (const { name } of context.environments) {
298
+ if (!unanswered.some((entry) => entry.environment === name))
299
+ unanswered.push({ environment: name, cause });
300
+ }
301
+ }
302
+ }
303
+ if (unanswered.length > 0) throw new SettingsAccountIncomplete(findings, unanswered);
304
+ return findings;
305
+ },
306
+ }),
307
+ };
308
+ }
309
+
166
310
  /**
167
311
  * Turn a host env's own parse report into findings — the bridge that makes "one schema, two readers" real.
168
312
  *
@@ -161,6 +161,41 @@ export const COMMANDS = {
161
161
  },
162
162
  "groups": []
163
163
  },
164
+ "completion": {
165
+ "positionals": [
166
+ {
167
+ "name": "shell",
168
+ "required": false,
169
+ "variadic": false
170
+ }
171
+ ],
172
+ "flags": {
173
+ "--remove": {
174
+ "type": "boolean",
175
+ "required": false,
176
+ "repeatable": false
177
+ },
178
+ "--status": {
179
+ "type": "boolean",
180
+ "required": false,
181
+ "repeatable": false
182
+ },
183
+ "--json": {
184
+ "type": "boolean",
185
+ "required": false,
186
+ "repeatable": false
187
+ }
188
+ },
189
+ "groups": [
190
+ {
191
+ "kind": "atMostOne",
192
+ "flags": [
193
+ "--remove",
194
+ "--status"
195
+ ]
196
+ }
197
+ ]
198
+ },
164
199
  "contract": {
165
200
  "positionals": [],
166
201
  "flags": {
@@ -251,6 +286,16 @@ export const COMMANDS = {
251
286
  "type": "string",
252
287
  "required": false,
253
288
  "repeatable": false
289
+ },
290
+ "--purpose": {
291
+ "type": "string",
292
+ "required": false,
293
+ "repeatable": false
294
+ },
295
+ "--kind": {
296
+ "type": "string",
297
+ "required": false,
298
+ "repeatable": false
254
299
  }
255
300
  },
256
301
  "groups": [
@@ -278,13 +323,22 @@ export const COMMANDS = {
278
323
  ],
279
324
  "any": false
280
325
  },
326
+ {
327
+ "kind": "requires",
328
+ "flag": "--kind",
329
+ "needs": [
330
+ "--public-key"
331
+ ],
332
+ "any": false
333
+ },
281
334
  {
282
335
  "kind": "requires",
283
336
  "flag": "--update",
284
337
  "needs": [
285
338
  "--worker-url",
286
339
  "--scope",
287
- "--public-key"
340
+ "--public-key",
341
+ "--purpose"
288
342
  ],
289
343
  "any": true
290
344
  }
@@ -1042,11 +1096,6 @@ export const COMMANDS = {
1042
1096
  "required": false,
1043
1097
  "repeatable": false
1044
1098
  },
1045
- "--api-token": {
1046
- "type": "string",
1047
- "required": false,
1048
- "repeatable": false
1049
- },
1050
1099
  "--r2-access-key-id": {
1051
1100
  "type": "string",
1052
1101
  "required": false,
@@ -1057,11 +1106,6 @@ export const COMMANDS = {
1057
1106
  "required": false,
1058
1107
  "repeatable": false
1059
1108
  },
1060
- "--r2-api-token": {
1061
- "type": "string",
1062
- "required": false,
1063
- "repeatable": false
1064
- },
1065
1109
  "--json": {
1066
1110
  "type": "boolean",
1067
1111
  "required": false,
@@ -1447,6 +1491,28 @@ export const COMMANDS = {
1447
1491
  },
1448
1492
  "groups": []
1449
1493
  },
1494
+ "secrets tables": {
1495
+ "positionals": [],
1496
+ "flags": {
1497
+ "--env": {
1498
+ "type": "string",
1499
+ "required": false,
1500
+ "repeatable": false,
1501
+ "default": "dev"
1502
+ },
1503
+ "--worker": {
1504
+ "type": "string",
1505
+ "required": false,
1506
+ "repeatable": false
1507
+ },
1508
+ "--json": {
1509
+ "type": "boolean",
1510
+ "required": false,
1511
+ "repeatable": false
1512
+ }
1513
+ },
1514
+ "groups": []
1515
+ },
1450
1516
  "secrets update": {
1451
1517
  "positionals": [
1452
1518
  {
@@ -1547,6 +1613,27 @@ export const COMMANDS = {
1547
1613
  },
1548
1614
  "groups": []
1549
1615
  },
1616
+ "setup": {
1617
+ "positionals": [],
1618
+ "flags": {
1619
+ "--skip-install": {
1620
+ "type": "boolean",
1621
+ "required": false,
1622
+ "repeatable": false
1623
+ },
1624
+ "--dry-run": {
1625
+ "type": "boolean",
1626
+ "required": false,
1627
+ "repeatable": false
1628
+ },
1629
+ "--json": {
1630
+ "type": "boolean",
1631
+ "required": false,
1632
+ "repeatable": false
1633
+ }
1634
+ },
1635
+ "groups": []
1636
+ },
1550
1637
  "storage deprovision": {
1551
1638
  "positionals": [],
1552
1639
  "flags": {
@@ -1586,11 +1673,6 @@ export const COMMANDS = {
1586
1673
  "required": false,
1587
1674
  "repeatable": false
1588
1675
  },
1589
- "--api-token": {
1590
- "type": "string",
1591
- "required": false,
1592
- "repeatable": false
1593
- },
1594
1676
  "--r2-access-key-id": {
1595
1677
  "type": "string",
1596
1678
  "required": false,
@@ -1986,6 +2068,34 @@ export const COMMANDS = {
1986
2068
  },
1987
2069
  "groups": []
1988
2070
  },
2071
+ "token forget": {
2072
+ "positionals": [
2073
+ {
2074
+ "name": "profile",
2075
+ "required": true,
2076
+ "variadic": false
2077
+ }
2078
+ ],
2079
+ "flags": {
2080
+ "--env": {
2081
+ "type": "string",
2082
+ "required": false,
2083
+ "repeatable": false,
2084
+ "default": "dev"
2085
+ },
2086
+ "--force": {
2087
+ "type": "boolean",
2088
+ "required": false,
2089
+ "repeatable": false
2090
+ },
2091
+ "--json": {
2092
+ "type": "boolean",
2093
+ "required": false,
2094
+ "repeatable": false
2095
+ }
2096
+ },
2097
+ "groups": []
2098
+ },
1989
2099
  "token list": {
1990
2100
  "positionals": [],
1991
2101
  "flags": {
@@ -2046,7 +2156,12 @@ export const COMMANDS = {
2046
2156
  "vectorize:write",
2047
2157
  "ai:read",
2048
2158
  "zone:read",
2049
- "routes:write"
2159
+ "routes:write",
2160
+ "analytics:read",
2161
+ "images:write",
2162
+ "stream:write",
2163
+ "queues:write",
2164
+ "artifacts:write"
2050
2165
  ]
2051
2166
  },
2052
2167
  "--json": {
@@ -2123,7 +2238,12 @@ export const COMMANDS = {
2123
2238
  "vectorize:write",
2124
2239
  "ai:read",
2125
2240
  "zone:read",
2126
- "routes:write"
2241
+ "routes:write",
2242
+ "analytics:read",
2243
+ "images:write",
2244
+ "stream:write",
2245
+ "queues:write",
2246
+ "artifacts:write"
2127
2247
  ]
2128
2248
  },
2129
2249
  "--keep-previous": {
@@ -2416,6 +2536,32 @@ export const COMMANDS = {
2416
2536
  },
2417
2537
  "groups": []
2418
2538
  },
2539
+ "worker bind": {
2540
+ "positionals": [],
2541
+ "flags": {
2542
+ "--from": {
2543
+ "type": "string",
2544
+ "required": false,
2545
+ "repeatable": false
2546
+ },
2547
+ "--to": {
2548
+ "type": "string",
2549
+ "required": false,
2550
+ "repeatable": false
2551
+ },
2552
+ "--binding": {
2553
+ "type": "string",
2554
+ "required": false,
2555
+ "repeatable": false
2556
+ },
2557
+ "--json": {
2558
+ "type": "boolean",
2559
+ "required": false,
2560
+ "repeatable": false
2561
+ }
2562
+ },
2563
+ "groups": []
2564
+ },
2419
2565
  "worker list": {
2420
2566
  "positionals": [],
2421
2567
  "flags": {
@@ -2447,17 +2593,27 @@ export const COMMANDS = {
2447
2593
  "worker rename": {
2448
2594
  "positionals": [
2449
2595
  {
2450
- "name": "from",
2451
- "required": true,
2596
+ "name": "old",
2597
+ "required": false,
2452
2598
  "variadic": false
2453
2599
  },
2454
2600
  {
2455
- "name": "to",
2456
- "required": true,
2601
+ "name": "new",
2602
+ "required": false,
2457
2603
  "variadic": false
2458
2604
  }
2459
2605
  ],
2460
2606
  "flags": {
2607
+ "--from": {
2608
+ "type": "string",
2609
+ "required": false,
2610
+ "repeatable": false
2611
+ },
2612
+ "--to": {
2613
+ "type": "string",
2614
+ "required": false,
2615
+ "repeatable": false
2616
+ },
2461
2617
  "--force": {
2462
2618
  "type": "boolean",
2463
2619
  "required": false,
@@ -2496,6 +2652,40 @@ export const COMMANDS = {
2496
2652
  }
2497
2653
  },
2498
2654
  "groups": []
2655
+ },
2656
+ "worker unbind": {
2657
+ "positionals": [],
2658
+ "flags": {
2659
+ "--from": {
2660
+ "type": "string",
2661
+ "required": false,
2662
+ "repeatable": false
2663
+ },
2664
+ "--to": {
2665
+ "type": "string",
2666
+ "required": false,
2667
+ "repeatable": false
2668
+ },
2669
+ "--binding": {
2670
+ "type": "string",
2671
+ "required": false,
2672
+ "repeatable": false
2673
+ },
2674
+ "--json": {
2675
+ "type": "boolean",
2676
+ "required": false,
2677
+ "repeatable": false
2678
+ }
2679
+ },
2680
+ "groups": [
2681
+ {
2682
+ "kind": "exactlyOne",
2683
+ "flags": [
2684
+ "--to",
2685
+ "--binding"
2686
+ ]
2687
+ }
2688
+ ]
2499
2689
  }
2500
2690
  } as const;
2501
2691
 
@@ -62,6 +62,7 @@ export const WORKER_NARROWS = [
62
62
  "env",
63
63
  "init",
64
64
  "migrate",
65
+ "secrets tables",
65
66
  "seed",
66
67
  "support deprovision",
67
68
  "upgrade",
@@ -13,11 +13,18 @@ import { ControlPlaneConfig, type ControlPlaneConfigInput } from "./config/confi
13
13
  import { ControlPlaneConnection } from "./data/connection";
14
14
  import { CONTROL_PLANE_CONNECTIONS_TABLE, type ControlPlaneDatabase, controlPlaneTables } from "./data/tables";
15
15
  import type { CapabilityDeclaration } from "./discovery/adminRoute";
16
- import { type CapabilityHealthSource, capabilityHealthSources } from "./discovery/health";
16
+ import { type CapabilityHealthSource, capabilityHealthSources, defineCapabilityHealth } from "./discovery/health";
17
17
  import { createControlPlaneVerifier } from "./http/guard";
18
18
  import { controlPlaneRouteDescriptors, registerControlPlaneRoutes } from "./http/routes";
19
+ import {
20
+ type ComposedMigrationDatabase,
21
+ composeMigrationDatabases,
22
+ MIGRATION_HEALTH_KEYS,
23
+ readMigrationHealth,
24
+ } from "./migrationState/migrationState";
19
25
  import { controlplane_0001_init } from "./migrations/0001_init";
20
26
  import { controlplane_0002_management_origin } from "./migrations/0002_management_origin";
27
+ import { controlplane_0003_connection_principal } from "./migrations/0003_connection_principal";
21
28
  import { d1ReplayGuard } from "./replay/d1Guard";
22
29
  import type { ReplayGuard } from "./replay/guard";
23
30
  import {
@@ -79,6 +86,7 @@ export function controlplane(options: ControlPlaneOptions = {}): ControlPlaneCap
79
86
  const migrations: Record<string, Migration> = {
80
87
  "0001_init": controlplane_0001_init,
81
88
  "0002_management_origin": controlplane_0002_management_origin,
89
+ "0003_connection_principal": controlplane_0003_connection_principal,
82
90
  };
83
91
  const requiredBindings: BindingSpecInput[] = [
84
92
  // The app database — the connections table lives here, beside auth's and audit's. Not the secrets
@@ -132,6 +140,12 @@ export function controlplane(options: ControlPlaneOptions = {}): ControlPlaneCap
132
140
  // assembly, because an adopter is never offered such a scope and the number could never be granted.
133
141
  let health: ReadonlyMap<string, CapabilityHealthSource> = new Map();
134
142
 
143
+ // Every database this Worker binds and what this build declares for it (#729), composed by the same hook
144
+ // through `collectMigrationSets` — the function `pithy migrate` uses, so the route and `pithy doctor`
145
+ // compose the same way. The inputs differ on a D1 two Workers share: the CLI merges every co-bound
146
+ // Worker's sets and this Worker holds only its own, so `pending` and `undeclared` can differ there.
147
+ let migrationDatabases: readonly ComposedMigrationDatabase[] = [];
148
+
135
149
  const capability = defineCapability({
136
150
  name: "controlplane",
137
151
  // `@pithy-sh/core`'s version, stamped by `scripts/stampVersions.ts` — a Worker cannot read its own
@@ -164,6 +178,14 @@ export function controlplane(options: ControlPlaneOptions = {}): ControlPlaneCap
164
178
  // The seam describes itself too. Built from the same resolved `basePath` the routes mount on, so a
165
179
  // moved mount point is reported rather than becoming a lie a client believes.
166
180
  adminRoutes: controlPlaneRouteDescriptors(config.basePath),
181
+ // Two counts on the seam's own manifest entry, so a client sees *this environment is behind* on the read
182
+ // it already makes and calls the route only to see what. Behind `migrations:read`, which the route above
183
+ // requires — a connection without it gets no number rather than a zero. A database that cannot be read
184
+ // fails the producer, and the seam reports the summary unavailable rather than a partial total.
185
+ health: defineCapabilityHealth({
186
+ keys: MIGRATION_HEALTH_KEYS,
187
+ read: (c) => readMigrationHealth(c.env as Record<string, unknown>, migrationDatabases),
188
+ }),
167
189
  compose: ({ capabilities }) => {
168
190
  // Every capability, including those exposing nothing. A client rendering a capability it cannot
169
191
  // act on is useful; a client that cannot tell "no admin surface" from "not installed" is not.
@@ -186,6 +208,7 @@ export function controlplane(options: ControlPlaneOptions = {}): ControlPlaneCap
186
208
  config: { ...(cap.manifestConfig?.values ?? {}) },
187
209
  }));
188
210
  health = capabilityHealthSources(capabilities);
211
+ migrationDatabases = composeMigrationDatabases(capabilities);
189
212
  },
190
213
  middleware: [
191
214
  (app) => {
@@ -228,6 +251,7 @@ export function controlplane(options: ControlPlaneOptions = {}): ControlPlaneCap
228
251
  database,
229
252
  composedCapabilities: () => composed,
230
253
  composedHealth: () => health,
254
+ composedMigrations: () => migrationDatabases,
231
255
  now,
232
256
  });
233
257
  },
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
 
4
4
  import { z } from "zod";
5
+ import { ControlPlaneActorKind } from "./data/connection";
5
6
  import { ControlPlaneScope } from "./scope/scope";
6
7
 
7
8
  /**
@@ -56,6 +57,9 @@ export const ControlPlaneContext = z
56
57
  tokenId: z
57
58
  .string()
58
59
  .describe("The token's `jti`, already claimed in the replay set. Recorded to tie one audit event to one call."),
60
+ actorKind: ControlPlaneActorKind.nullable().describe(
61
+ "What kind of client the connection is for — `agent`, `dashboard` or `script` — read from the adopter's own row, never from the token. The gate stamps it onto every control-plane audit row this call writes. Null for a connection registered before the kind was recorded (#721).",
62
+ ),
59
63
  })
60
64
  .describe(
61
65
  "The verified control-plane caller for one request. Never an AuthContext: a management client is not a user of the adopter's app.",