@pithy-sh/core 0.11.0 → 0.13.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 (157) hide show
  1. package/dist/audit/composed.d.ts +18 -0
  2. package/dist/audit/composed.d.ts.map +1 -0
  3. package/dist/audit/composed.js +28 -0
  4. package/dist/audit/recorder.d.ts +10 -2
  5. package/dist/audit/recorder.d.ts.map +1 -1
  6. package/dist/capability/capability.d.ts +23 -0
  7. package/dist/capability/capability.d.ts.map +1 -1
  8. package/dist/capability/capability.js +2 -0
  9. package/dist/capability/purge.d.ts +122 -0
  10. package/dist/capability/purge.d.ts.map +1 -0
  11. package/dist/capability/purge.js +131 -0
  12. package/dist/cli/commandCatalog.generated.d.ts +54 -2
  13. package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
  14. package/dist/cli/commandCatalog.generated.js +61 -0
  15. package/dist/cli/commandFacts.d.ts +1 -0
  16. package/dist/cli/commandFacts.d.ts.map +1 -1
  17. package/dist/cli/commandFacts.js +1 -0
  18. package/dist/controlPlane/capability.d.ts.map +1 -1
  19. package/dist/controlPlane/capability.js +7 -0
  20. package/dist/controlPlane/data/connection.d.ts +2 -12
  21. package/dist/controlPlane/data/connection.d.ts.map +1 -1
  22. package/dist/controlPlane/data/connection.js +1 -1
  23. package/dist/controlPlane/discovery/adminRoute.d.ts +29 -0
  24. package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -1
  25. package/dist/controlPlane/discovery/adminRoute.js +3 -0
  26. package/dist/controlPlane/error/errors.d.ts +4 -3
  27. package/dist/controlPlane/error/errors.d.ts.map +1 -1
  28. package/dist/controlPlane/http/guard.d.ts.map +1 -1
  29. package/dist/controlPlane/http/guard.js +40 -1
  30. package/dist/controlPlane/http/handlers.d.ts +79 -0
  31. package/dist/controlPlane/http/handlers.d.ts.map +1 -1
  32. package/dist/controlPlane/http/handlers.js +37 -3
  33. package/dist/controlPlane/http/responses.d.ts +98 -0
  34. package/dist/controlPlane/http/responses.d.ts.map +1 -1
  35. package/dist/controlPlane/http/responses.js +41 -1
  36. package/dist/controlPlane/http/routes.d.ts +1 -0
  37. package/dist/controlPlane/http/routes.d.ts.map +1 -1
  38. package/dist/controlPlane/http/routes.js +17 -3
  39. package/dist/controlPlane/scope/scope.d.ts +13 -3
  40. package/dist/controlPlane/scope/scope.d.ts.map +1 -1
  41. package/dist/controlPlane/scope/scope.js +14 -4
  42. package/dist/createBackend.d.ts.map +1 -1
  43. package/dist/createBackend.js +2 -0
  44. package/dist/createEntrypoint.d.ts +7 -1
  45. package/dist/createEntrypoint.d.ts.map +1 -1
  46. package/dist/createEntrypoint.js +15 -5
  47. package/dist/crypto/envelope.d.ts +86 -0
  48. package/dist/crypto/envelope.d.ts.map +1 -0
  49. package/dist/crypto/envelope.js +129 -0
  50. package/dist/crypto/fieldKeys.d.ts +41 -0
  51. package/dist/crypto/fieldKeys.d.ts.map +1 -0
  52. package/dist/crypto/fieldKeys.js +56 -0
  53. package/dist/data/codecs.d.ts +23 -10
  54. package/dist/data/codecs.d.ts.map +1 -1
  55. package/dist/data/codecs.js +50 -36
  56. package/dist/error/extend.d.ts +6 -5
  57. package/dist/error/extend.d.ts.map +1 -1
  58. package/dist/error/http.d.ts +72 -0
  59. package/dist/error/http.d.ts.map +1 -1
  60. package/dist/error/payload.d.ts +144 -0
  61. package/dist/error/payload.d.ts.map +1 -1
  62. package/dist/error/payload.js +48 -0
  63. package/dist/error/pithyError.d.ts +18 -7
  64. package/dist/error/pithyError.d.ts.map +1 -1
  65. package/dist/error/pithyError.js +6 -0
  66. package/dist/http/validation.d.ts +11 -2
  67. package/dist/http/validation.d.ts.map +1 -1
  68. package/dist/http/validation.js +20 -3
  69. package/dist/index.d.ts +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +2 -2
  72. package/dist/logger/logger.d.ts +12 -5
  73. package/dist/logger/logger.d.ts.map +1 -1
  74. package/dist/logger/record.d.ts +42 -0
  75. package/dist/logger/record.d.ts.map +1 -1
  76. package/dist/privacy/blindIndex.d.ts +27 -0
  77. package/dist/privacy/blindIndex.d.ts.map +1 -0
  78. package/dist/privacy/blindIndex.js +57 -0
  79. package/dist/privacy/classes.d.ts +142 -0
  80. package/dist/privacy/classes.d.ts.map +1 -0
  81. package/dist/privacy/classes.js +209 -0
  82. package/dist/privacy/classify.d.ts +336 -0
  83. package/dist/privacy/classify.d.ts.map +1 -0
  84. package/dist/privacy/classify.js +1024 -0
  85. package/dist/privacy/encrypted.d.ts +210 -0
  86. package/dist/privacy/encrypted.d.ts.map +1 -0
  87. package/dist/privacy/encrypted.js +350 -0
  88. package/dist/privacy/encryptedColumns.d.ts +64 -0
  89. package/dist/privacy/encryptedColumns.d.ts.map +1 -0
  90. package/dist/privacy/encryptedColumns.js +134 -0
  91. package/dist/privacy/fieldRotation.d.ts +34 -0
  92. package/dist/privacy/fieldRotation.d.ts.map +1 -0
  93. package/dist/privacy/fieldRotation.js +186 -0
  94. package/dist/privacy/inventory.d.ts +12 -0
  95. package/dist/privacy/inventory.d.ts.map +1 -0
  96. package/dist/privacy/inventory.js +154 -0
  97. package/dist/privacy/posture.d.ts +114 -0
  98. package/dist/privacy/posture.d.ts.map +1 -0
  99. package/dist/privacy/posture.js +136 -0
  100. package/dist/privacy/redact.d.ts +7 -0
  101. package/dist/privacy/redact.d.ts.map +1 -0
  102. package/dist/privacy/redact.js +404 -0
  103. package/dist/privacy/report.d.ts +58 -0
  104. package/dist/privacy/report.d.ts.map +1 -0
  105. package/dist/privacy/report.js +15 -0
  106. package/dist/privacy/retirement.d.ts +68 -0
  107. package/dist/privacy/retirement.d.ts.map +1 -0
  108. package/dist/privacy/retirement.js +72 -0
  109. package/dist/privacy/sealed.d.ts +87 -0
  110. package/dist/privacy/sealed.d.ts.map +1 -0
  111. package/dist/privacy/sealed.js +221 -0
  112. package/dist/privacy/types.d.ts +559 -0
  113. package/dist/privacy/types.d.ts.map +1 -0
  114. package/dist/privacy/types.js +2 -0
  115. package/dist/version.generated.d.ts +1 -1
  116. package/dist/version.generated.js +1 -1
  117. package/package.json +1 -1
  118. package/src/audit/composed.ts +30 -0
  119. package/src/audit/recorder.ts +10 -2
  120. package/src/capability/capability.ts +26 -0
  121. package/src/capability/purge.ts +187 -0
  122. package/src/cli/commandCatalog.generated.ts +65 -0
  123. package/src/cli/commandFacts.ts +1 -0
  124. package/src/controlPlane/capability.ts +11 -0
  125. package/src/controlPlane/discovery/adminRoute.ts +11 -0
  126. package/src/controlPlane/error/errors.ts +4 -3
  127. package/src/controlPlane/http/guard.ts +60 -1
  128. package/src/controlPlane/http/handlers.ts +57 -4
  129. package/src/controlPlane/http/responses.ts +100 -0
  130. package/src/controlPlane/http/routes.ts +31 -1
  131. package/src/controlPlane/scope/scope.ts +14 -3
  132. package/src/controlPlane/token/base64url.ts +1 -1
  133. package/src/createBackend.ts +4 -0
  134. package/src/createEntrypoint.ts +17 -2
  135. package/src/crypto/envelope.ts +196 -0
  136. package/src/crypto/fieldKeys.ts +76 -0
  137. package/src/data/codecs.ts +69 -37
  138. package/src/error/extend.ts +6 -5
  139. package/src/error/payload.ts +110 -0
  140. package/src/error/pithyError.ts +21 -7
  141. package/src/http/validation.ts +35 -3
  142. package/src/index.ts +1 -0
  143. package/src/logger/logger.ts +12 -5
  144. package/src/privacy/blindIndex.ts +81 -0
  145. package/src/privacy/classes.ts +277 -0
  146. package/src/privacy/classify.ts +1447 -0
  147. package/src/privacy/encrypted.ts +513 -0
  148. package/src/privacy/encryptedColumns.ts +185 -0
  149. package/src/privacy/fieldRotation.ts +234 -0
  150. package/src/privacy/inventory.ts +147 -0
  151. package/src/privacy/posture.ts +205 -0
  152. package/src/privacy/redact.ts +423 -0
  153. package/src/privacy/report.ts +64 -0
  154. package/src/privacy/retirement.ts +102 -0
  155. package/src/privacy/sealed.ts +323 -0
  156. package/src/privacy/types.ts +804 -0
  157. package/src/version.generated.ts +1 -1
@@ -1,6 +1,6 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
- import { workerVersion } from "../../worker/identity.js";
3
+ import { workerVersionMetadata } from "../../worker/identity.js";
4
4
  import { ControlPlaneConnection } from "../data/connection.js";
5
5
  import { CONTROL_PLANE_CONNECTIONS_TABLE } from "../data/tables.js";
6
6
  import { healthWire } from "../discovery/healthSummary.js";
@@ -8,6 +8,7 @@ import { readCapabilityHealth } from "../discovery/health.js";
8
8
  import { ControlPlaneAuditActions, safeEmit } from "../audit/actions.js";
9
9
  import { ControlPlaneInvalidCredentialError, ControlPlaneKeyConflictError } from "../error/errors.js";
10
10
  import { appendKey, expireKey, pruneKeys } from "../data/keyLifecycle.js";
11
+ import { privacyAttestation, privacyPosture } from "../../privacy/posture.js";
11
12
  import { causeMessage } from "../../error/cause.js";
12
13
  import { readDatabaseMigrations } from "../migrationState/migrationState.js";
13
14
  //#region src/controlPlane/http/handlers.ts
@@ -114,10 +115,16 @@ function manifestHandler(deps) {
114
115
  ...declaration,
115
116
  ...healthWire(await readCapabilityHealth(sources.get(declaration.name), context.grantedScopes, (source) => source.read(c)))
116
117
  })));
118
+ const build = workerVersionMetadata(c.env);
117
119
  return c.json({
118
120
  environment: context.environment,
119
121
  connectionId: context.connectionId,
120
- version: workerVersion(c.env),
122
+ version: build.id,
123
+ versionCreatedAt: build.createdAt,
124
+ privacy: {
125
+ ...privacyPosture(privacyAttestation(c.env), deps.privacyInventory),
126
+ retirement: await deps.retirement(c.env)
127
+ },
121
128
  capabilities,
122
129
  grantedScopes: [...context.grantedScopes]
123
130
  });
@@ -272,5 +279,32 @@ function expireKeyHandler(deps) {
272
279
  });
273
280
  };
274
281
  }
282
+ /**
283
+ * `GET /control-plane/privacy` — the full privacy inventory (#132).
284
+ *
285
+ * Every storage schema this Worker composed that holds a marked field, each field's path and class, and where it
286
+ * is stored — derived from the running code, so unmarking a field removes it on the next deploy with nothing in
287
+ * between. Behind `privacy:read`, because field names are the kind of thing an adopter may withhold from a client
288
+ * that has no business seeing them. Read-only.
289
+ */
290
+ function privacyHandler(deps) {
291
+ return async (c) => {
292
+ const context = caller(c);
293
+ const inventory = deps.privacyInventory();
294
+ return c.json({
295
+ environment: context.environment,
296
+ posture: {
297
+ ...privacyPosture(privacyAttestation(c.env), () => inventory),
298
+ retirement: await deps.retirement(c.env)
299
+ },
300
+ schemasWalked: inventory.schemasWalked,
301
+ schemas: inventory.schemas.map((schema) => ({
302
+ ...schema,
303
+ fields: schema.fields.map((field) => ({ ...field })),
304
+ encrypted: schema.encrypted.map((field) => ({ ...field }))
305
+ }))
306
+ });
307
+ };
308
+ }
275
309
  //#endregion
276
- export { expireKeyHandler, listKeysHandler, manifestHandler, migrationsHandler, pingHandler, registerKeyHandler };
310
+ export { expireKeyHandler, listKeysHandler, manifestHandler, migrationsHandler, pingHandler, privacyHandler, registerKeyHandler };
@@ -245,4 +245,102 @@ export declare const ControlPlaneMigrationsResponse: z.ZodObject<{
245
245
  }, z.core.$strip>], "state">>;
246
246
  }, z.core.$strip>;
247
247
  export type ControlPlaneMigrationsResponse = z.output<typeof ControlPlaneMigrationsResponse>;
248
+ /**
249
+ * One marked field, as `GET {basePath}/privacy` reports it (#132).
250
+ *
251
+ * **The per-field record.** Since #390 it says whether the field rests encrypted beside `class`, so "which of
252
+ * your PHI fields are encrypted" is a derivation over this list rather than a second inventory to keep.
253
+ */
254
+ export declare const PrivacyFieldView: z.ZodObject<{
255
+ path: z.ZodString;
256
+ class: z.ZodString;
257
+ truncated: z.ZodOptional<z.ZodLiteral<true>>;
258
+ sameAs: z.ZodOptional<z.ZodString>;
259
+ encryptedAtRest: z.ZodOptional<z.ZodBoolean>;
260
+ unrotatable: z.ZodOptional<z.ZodLiteral<true>>;
261
+ }, z.core.$strip>;
262
+ export type PrivacyFieldView = z.output<typeof PrivacyFieldView>;
263
+ /** One storage schema holding at least one marked field. */
264
+ export declare const PrivacySchemaView: z.ZodObject<{
265
+ capability: z.ZodString;
266
+ storage: z.ZodEnum<{
267
+ d1: "d1";
268
+ kv: "kv";
269
+ }>;
270
+ container: z.ZodString;
271
+ name: z.ZodString;
272
+ fields: z.ZodArray<z.ZodObject<{
273
+ path: z.ZodString;
274
+ class: z.ZodString;
275
+ truncated: z.ZodOptional<z.ZodLiteral<true>>;
276
+ sameAs: z.ZodOptional<z.ZodString>;
277
+ encryptedAtRest: z.ZodOptional<z.ZodBoolean>;
278
+ unrotatable: z.ZodOptional<z.ZodLiteral<true>>;
279
+ }, z.core.$strip>>;
280
+ encrypted: z.ZodOptional<z.ZodArray<z.ZodObject<{
281
+ path: z.ZodString;
282
+ unrotatable: z.ZodOptional<z.ZodLiteral<true>>;
283
+ }, z.core.$strip>>>;
284
+ }, z.core.$strip>;
285
+ export type PrivacySchemaView = z.output<typeof PrivacySchemaView>;
286
+ /**
287
+ * `GET /control-plane/privacy` — the full inventory, behind `privacy:read` (#132).
288
+ *
289
+ * Derived at runtime from the schemas this Worker composed, walked once per isolate. The manifest's `privacy`
290
+ * block is the headline; this is the detail, and it names fields, which is why it has a scope of its own.
291
+ */
292
+ export declare const ControlPlanePrivacyResponse: z.ZodObject<{
293
+ environment: z.ZodString;
294
+ posture: z.ZodObject<{
295
+ declared: z.ZodNullable<z.ZodArray<z.ZodString>>;
296
+ observed: z.ZodNullable<z.ZodArray<z.ZodString>>;
297
+ counts: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodNumber>>;
298
+ observedNotDeclared: z.ZodNullable<z.ZodArray<z.ZodString>>;
299
+ declaredNotObserved: z.ZodNullable<z.ZodArray<z.ZodString>>;
300
+ consoleGate: z.ZodEnum<{
301
+ enforced: "enforced";
302
+ not_required: "not_required";
303
+ unattested: "unattested";
304
+ unverifiable: "unverifiable";
305
+ }>;
306
+ consoleGateReason: z.ZodNullable<z.ZodString>;
307
+ attestedAt: z.ZodNullable<z.ZodISODateTime>;
308
+ sources: z.ZodArray<z.ZodEnum<{
309
+ d1: "d1";
310
+ kv: "kv";
311
+ }>>;
312
+ retirement: z.ZodOptional<z.ZodObject<{
313
+ blocked: z.ZodBoolean;
314
+ reasons: z.ZodArray<z.ZodEnum<{
315
+ "incomplete-coverage": "incomplete-coverage";
316
+ stale: "stale";
317
+ unrotatable: "unrotatable";
318
+ }>>;
319
+ liveVersions: z.ZodNumber;
320
+ }, z.core.$strip>>;
321
+ }, z.core.$strip>;
322
+ schemasWalked: z.ZodNumber;
323
+ schemas: z.ZodArray<z.ZodObject<{
324
+ capability: z.ZodString;
325
+ storage: z.ZodEnum<{
326
+ d1: "d1";
327
+ kv: "kv";
328
+ }>;
329
+ container: z.ZodString;
330
+ name: z.ZodString;
331
+ fields: z.ZodArray<z.ZodObject<{
332
+ path: z.ZodString;
333
+ class: z.ZodString;
334
+ truncated: z.ZodOptional<z.ZodLiteral<true>>;
335
+ sameAs: z.ZodOptional<z.ZodString>;
336
+ encryptedAtRest: z.ZodOptional<z.ZodBoolean>;
337
+ unrotatable: z.ZodOptional<z.ZodLiteral<true>>;
338
+ }, z.core.$strip>>;
339
+ encrypted: z.ZodOptional<z.ZodArray<z.ZodObject<{
340
+ path: z.ZodString;
341
+ unrotatable: z.ZodOptional<z.ZodLiteral<true>>;
342
+ }, z.core.$strip>>>;
343
+ }, z.core.$strip>>;
344
+ }, z.core.$strip>;
345
+ export type ControlPlanePrivacyResponse = z.output<typeof ControlPlanePrivacyResponse>;
248
346
  //# sourceMappingURL=responses.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/responses.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;;;;GAkBG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,aAAa;;;;;;;;;;iBAcmF,CAAC;AAC9G,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;;iBAQwB,CAAC;AAC9D,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEjF,+FAA+F;AAC/F,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;iBAuBwE,CAAC;AAC9G,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEjF,mFAAmF;AACnF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;iBAQyB,CAAC;AAC1D,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEvE,gGAAgG;AAChG,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;iBAM6B,CAAC;AAC5D,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;iBAU+C,CAAC;AAC1E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D,iFAAiF;AACjF,eAAO,MAAM,0BAA0B;;;;;;;;;4BAyBkE,CAAC;AAC1G,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAErF,kFAAkF;AAClF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;iBAW8C,CAAC;AAC9E,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BA+BwE,CAAC;AAC3G,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO+D,CAAC;AAC3G,MAAM,MAAM,8BAA8B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,8BAA8B,CAAC,CAAC"}
1
+ {"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/responses.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;;;;;;;;;;;;;GAkBG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,aAAa;;;;;;;;;;iBAcmF,CAAC;AAC9G,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;;iBAQwB,CAAC;AAC9D,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEjF,+FAA+F;AAC/F,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;iBAuBwE,CAAC;AAC9G,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEjF,mFAAmF;AACnF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;iBAQyB,CAAC;AAC1D,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEvE,gGAAgG;AAChG,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;iBAM6B,CAAC;AAC5D,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;iBAU+C,CAAC;AAC1E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D,iFAAiF;AACjF,eAAO,MAAM,0BAA0B;;;;;;;;;4BAyBkE,CAAC;AAC1G,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAErF,kFAAkF;AAClF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;iBAW8C,CAAC;AAC9E,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4BA+BwE,CAAC;AAC3G,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE3E;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO+D,CAAC;AAC3G,MAAM,MAAM,8BAA8B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAE7F;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB;;;;;;;iBAmCiB,CAAC;AAC/C,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEjE,4DAA4D;AAC5D,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;iBA0BmC,CAAC;AAClE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAerC,CAAC;AACJ,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,2BAA2B,CAAC,CAAC"}
@@ -1,6 +1,8 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
+ import { PRIVACY_SOURCES } from "../../privacy/report.js";
3
4
  import { ConnectionPrincipal, ConnectionPurpose, ControlPlaneActorKind, Ed25519PublicJwk } from "../data/connection.js";
5
+ import { PrivacyPosture } from "../../privacy/posture.js";
4
6
  import { z } from "zod";
5
7
  //#region src/controlPlane/http/responses.ts
6
8
  /**
@@ -118,5 +120,43 @@ const ControlPlaneMigrationsResponse = z.object({
118
120
  environment: z.string().describe("The environment this Worker answers for."),
119
121
  databases: z.array(MigrationDatabaseView).describe("One entry per database this Worker binds, sorted by database name. One unreadable costs only itself.")
120
122
  }).describe("What is applied, pending and undeclared in each bound database, and which run put it there.");
123
+ /**
124
+ * One marked field, as `GET {basePath}/privacy` reports it (#132).
125
+ *
126
+ * **The per-field record.** Since #390 it says whether the field rests encrypted beside `class`, so "which of
127
+ * your PHI fields are encrypted" is a derivation over this list rather than a second inventory to keep.
128
+ */
129
+ const PrivacyFieldView = z.object({
130
+ path: z.string().describe("Where the field sits. A D1 column path (`email`, `address.line1`, `profile.phone` inside a JSON column), or a KV entry's part first (`key.email`, `value.token`, `metadata.ip`, or `value` for a whole marked value). An array element is `[]`, a record's value `*`."),
131
+ class: z.string().describe("The privacy class it is marked with: `secret`, `pii`, `phi`, `pci`, or an adopter's own."),
132
+ truncated: z.literal(true).optional().describe("Set where the class is assumed rather than found: a read ran out of its budget there, or the schema could not be read at all, so any class may be marked below it. Absent where the schema was read to the end."),
133
+ sameAs: z.string().optional().describe("Set on a reference: the path this field's position was first listed under, once the list reached its cap on paths. The class is marked somewhere beneath both, and the paths beneath this one are those beneath that one."),
134
+ encryptedAtRest: z.boolean().optional().describe("Whether the field rests encrypted (#390). Absent where the core cannot say — an assumed class, a reference, or a core older than encrypted fields — and never defaulted to false."),
135
+ unrotatable: z.literal(true).optional().describe("Set on an encrypted field whose storage is not D1: it can never be rotated, and it blocks retirement.")
136
+ }).describe("One marked field and its class.");
137
+ /** One storage schema holding at least one marked field. */
138
+ const PrivacySchemaView = z.object({
139
+ capability: z.string().describe("The capability that declared the schema, as the manifest's `capabilities[]` names it."),
140
+ storage: z.enum(PRIVACY_SOURCES).describe("Where it is stored: a `d1` table or a `kv` store."),
141
+ container: z.string().describe("The D1 database name or the KV namespace name it lives in."),
142
+ name: z.string().describe("The table name, or the KV store name."),
143
+ fields: z.array(PrivacyFieldView).describe("Every marked field in it, in schema order."),
144
+ encrypted: z.array(z.object({
145
+ path: z.string().describe("Where the encrypted field sits, on the same paths as `fields[].path`."),
146
+ unrotatable: z.literal(true).optional().describe("Set where the storage is not D1, so the field can never be rotated.")
147
+ }).describe("One encrypted field.")).optional().describe("Every field marked encrypted at rest, classed or not (#390). Absent on a Worker whose core predates encrypted fields.")
148
+ }).describe("One storage schema and the marked fields it holds.");
149
+ /**
150
+ * `GET /control-plane/privacy` — the full inventory, behind `privacy:read` (#132).
151
+ *
152
+ * Derived at runtime from the schemas this Worker composed, walked once per isolate. The manifest's `privacy`
153
+ * block is the headline; this is the detail, and it names fields, which is why it has a scope of its own.
154
+ */
155
+ const ControlPlanePrivacyResponse = z.object({
156
+ environment: z.string().describe("The environment this Worker answers for."),
157
+ posture: PrivacyPosture.describe("The declared and observed classes, the delta, and the console gate."),
158
+ schemasWalked: z.number().int().nonnegative().describe("How many storage schemas were walked, marked or not, across the sources `posture.sources` names."),
159
+ schemas: z.array(PrivacySchemaView).describe("Every schema holding a marked field, in composition order. A schema with none is not listed.")
160
+ }).describe("Every schema this Worker stores that holds a marked field, each field's path and class, and where it lives.");
121
161
  //#endregion
122
- export { ControlPlaneKeysResponse, ControlPlaneMigrationsResponse, ControlPlanePingResponse, ExpireKeyResponse, MigrationDatabaseView, MigrationGroupPositionView, MigrationGroupView, MigrationView, PublicKeyView, RegisterKeyResponse };
162
+ export { ControlPlaneKeysResponse, ControlPlaneMigrationsResponse, ControlPlanePingResponse, ControlPlanePrivacyResponse, ExpireKeyResponse, MigrationDatabaseView, MigrationGroupPositionView, MigrationGroupView, MigrationView, PrivacyFieldView, PrivacySchemaView, PublicKeyView, RegisterKeyResponse };
@@ -13,6 +13,7 @@ import { type ControlPlaneHandlerDeps } from "./handlers";
13
13
  * POST /control-plane/keys → register a key (keys:rotate)
14
14
  * POST /control-plane/keys/:keyId/expire → retire a superseded key (keys:rotate)
15
15
  * GET /control-plane/migrations → where each database stands (migrations:read)
16
+ * GET /control-plane/privacy → what each schema holds (privacy:read)
16
17
  *
17
18
  * Every one is `control-plane` and default-denied — with no connection registered they all answer 403,
18
19
  * and that is the shipped state of a Worker nobody has connected.
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/routes.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,EAAE,KAAK,UAAU,EAAwB,MAAM,yBAAyB,CAAC;AAGhF,OAAO,EACL,KAAK,uBAAuB,EAO7B,MAAM,YAAY,CAAC;AAUpB;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAwC3E;AAED,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,EACvB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,uBAAuB,GAC5B,IAAI,CA6EN"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/http/routes.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,EAAE,KAAK,UAAU,EAAwB,MAAM,yBAAyB,CAAC;AAShF,OAAO,EACL,KAAK,uBAAuB,EAQ7B,MAAM,YAAY,CAAC;AAWpB;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CA+C3E;AAED,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,IAAI,CAAC,YAAY,CAAC,EACvB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,uBAAuB,GAC5B,IAAI,CA2FN"}
@@ -1,12 +1,12 @@
1
1
  // SPDX-FileCopyrightText: 2026 Pithy
2
2
  // SPDX-License-Identifier: MIT
3
- import { ANY_VERIFIED_CALLER, KEYS_ROTATE_SCOPE, MANIFEST_READ_SCOPE, MIGRATIONS_READ_SCOPE } from "../scope/scope.js";
3
+ import { ANY_VERIFIED_CALLER, KEYS_ROTATE_SCOPE, MANIFEST_READ_SCOPE, MIGRATIONS_READ_SCOPE, PRIVACY_READ_SCOPE } from "../scope/scope.js";
4
4
  import { requireControlPlane } from "./guard.js";
5
5
  import { describeRoute } from "../../http/routeSpec.js";
6
6
  import { validate } from "../../http/validation.js";
7
7
  import { ControlPlaneManifest } from "../discovery/adminRoute.js";
8
- import { expireKeyHandler, listKeysHandler, manifestHandler, migrationsHandler, pingHandler, registerKeyHandler } from "./handlers.js";
9
- import { ControlPlaneKeysResponse, ControlPlaneMigrationsResponse, ControlPlanePingResponse, ExpireKeyResponse, RegisterKeyResponse } from "./responses.js";
8
+ import { expireKeyHandler, listKeysHandler, manifestHandler, migrationsHandler, pingHandler, privacyHandler, registerKeyHandler } from "./handlers.js";
9
+ import { ControlPlaneKeysResponse, ControlPlaneMigrationsResponse, ControlPlanePingResponse, ControlPlanePrivacyResponse, ExpireKeyResponse, RegisterKeyResponse } from "./responses.js";
10
10
  import { ExpireKeyParams, ExpireKeyRequest, RegisterKeyRequest } from "./schemas.js";
11
11
  //#region src/controlPlane/http/routes.ts
12
12
  /**
@@ -18,6 +18,7 @@ import { ExpireKeyParams, ExpireKeyRequest, RegisterKeyRequest } from "./schemas
18
18
  * POST /control-plane/keys → register a key (keys:rotate)
19
19
  * POST /control-plane/keys/:keyId/expire → retire a superseded key (keys:rotate)
20
20
  * GET /control-plane/migrations → where each database stands (migrations:read)
21
+ * GET /control-plane/privacy → what each schema holds (privacy:read)
21
22
  *
22
23
  * Every one is `control-plane` and default-denied — with no connection registered they all answer 403,
23
24
  * and that is the shipped state of a Worker nobody has connected.
@@ -80,6 +81,12 @@ function controlPlaneRouteDescriptors(basePath) {
80
81
  path: `${basePath}/migrations`,
81
82
  scope: MIGRATIONS_READ_SCOPE,
82
83
  summary: "What each bound database has applied, what is pending, what is undeclared, and which run put it there. Undeclared is relative to this Worker."
84
+ },
85
+ {
86
+ method: "GET",
87
+ path: `${basePath}/privacy`,
88
+ scope: PRIVACY_READ_SCOPE,
89
+ summary: "Every stored schema holding a privacy-marked field, each field's path and class, and where it lives. Derived from the running code."
83
90
  }
84
91
  ];
85
92
  }
@@ -121,6 +128,13 @@ function registerControlPlaneRoutes(app, basePath, deps) {
121
128
  scopes: [MIGRATIONS_READ_SCOPE],
122
129
  responses: { 200: ControlPlaneMigrationsResponse }
123
130
  }), requireControlPlane(MIGRATIONS_READ_SCOPE), migrationsHandler(deps));
131
+ app.get(`${basePath}/privacy`, describeRoute({
132
+ summary: "Report every stored field marked with a privacy class.",
133
+ description: "Every D1 table and KV store this Worker composed that holds a privacy-marked field, with each field's path, its class and where it is stored, beside the declared-and-observed posture. Derived at runtime from the deployed schemas. Read-only.",
134
+ verification: "control-plane",
135
+ scopes: [PRIVACY_READ_SCOPE],
136
+ responses: { 200: ControlPlanePrivacyResponse }
137
+ }), requireControlPlane(PRIVACY_READ_SCOPE), privacyHandler(deps));
124
138
  }
125
139
  //#endregion
126
140
  export { controlPlaneRouteDescriptors, registerControlPlaneRoutes };
@@ -56,13 +56,23 @@ export declare const KEYS_ROTATE_SCOPE: ControlPlaneScope;
56
56
  * manifest entry sit behind it too, so a connection without it gets no number rather than a zero.
57
57
  */
58
58
  export declare const MIGRATIONS_READ_SCOPE: ControlPlaneScope;
59
+ /**
60
+ * Read this Worker's full privacy inventory: every storage schema holding a marked field, each marked field's
61
+ * path, its class, and where it is stored (#132). Read-only — the one route behind it is a `GET`.
62
+ *
63
+ * **Its own scope rather than `manifest:read`**, because field names are the kind of thing an adopter may
64
+ * reasonably withhold from a client that has no business seeing them. The manifest's privacy block carries
65
+ * classes and counts only, so a connection without this one still sees the posture headline.
66
+ */
67
+ export declare const PRIVACY_READ_SCOPE: ControlPlaneScope;
59
68
  /**
60
69
  * The seam's base grant — what `pithy dashboard connect` grants whatever the Worker composes. Ping is absent
61
70
  * because it is not grantable; see {@link ANY_VERIFIED_CALLER}.
62
71
  *
63
- * **{@link MIGRATIONS_READ_SCOPE} is absent on purpose.** It is a read — every route requiring it is a `GET`
64
- * — so `connect` derives it from the seam's `adminRoutes` the way it derives every capability's reads. The
65
- * base exists for `keys:rotate`, which is not a read and would otherwise drop out.
72
+ * **{@link MIGRATIONS_READ_SCOPE} and {@link PRIVACY_READ_SCOPE} are absent on purpose.** Each is a read —
73
+ * every route requiring it is a `GET` — so `connect` derives it from the seam's `adminRoutes` the way it
74
+ * derives every capability's reads. The base exists for `keys:rotate`, which is not a read and would
75
+ * otherwise drop out.
66
76
  */
67
77
  export declare const SEAM_SCOPES: readonly ControlPlaneScope[];
68
78
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/scope/scope.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAexB;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,aAQ3B,CAAC;AACJ,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,EAAE,OAAO,MAA2D,CAAC;AAErG;;;;;;;GAOG;AACH,MAAM,MAAM,uBAAuB,GAAG,iBAAiB,GAAG,OAAO,mBAAmB,CAAC;AAErF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,iBAAmC,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,iBAAiC,CAAC;AAElE;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,iBAAqC,CAAC;AAE1E;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,iBAAiB,EAA6C,CAAC;AAElG;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAIrH"}
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../../src/controlPlane/scope/scope.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAexB;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,aAQ3B,CAAC;AACJ,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,mBAAmB,EAAE,OAAO,MAA2D,CAAC;AAErG;;;;;;;GAOG;AACH,MAAM,MAAM,uBAAuB,GAAG,iBAAiB,GAAG,OAAO,mBAAmB,CAAC;AAErF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,iBAAmC,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,iBAAiC,CAAC;AAElE;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,EAAE,iBAAqC,CAAC;AAE1E;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE,iBAAkC,CAAC;AAEpE;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,EAAE,SAAS,iBAAiB,EAA6C,CAAC;AAElG;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAIrH"}
@@ -47,12 +47,22 @@ const KEYS_ROTATE_SCOPE = "keys:rotate";
47
47
  */
48
48
  const MIGRATIONS_READ_SCOPE = "migrations:read";
49
49
  /**
50
+ * Read this Worker's full privacy inventory: every storage schema holding a marked field, each marked field's
51
+ * path, its class, and where it is stored (#132). Read-only — the one route behind it is a `GET`.
52
+ *
53
+ * **Its own scope rather than `manifest:read`**, because field names are the kind of thing an adopter may
54
+ * reasonably withhold from a client that has no business seeing them. The manifest's privacy block carries
55
+ * classes and counts only, so a connection without this one still sees the posture headline.
56
+ */
57
+ const PRIVACY_READ_SCOPE = "privacy:read";
58
+ /**
50
59
  * The seam's base grant — what `pithy dashboard connect` grants whatever the Worker composes. Ping is absent
51
60
  * because it is not grantable; see {@link ANY_VERIFIED_CALLER}.
52
61
  *
53
- * **{@link MIGRATIONS_READ_SCOPE} is absent on purpose.** It is a read — every route requiring it is a `GET`
54
- * — so `connect` derives it from the seam's `adminRoutes` the way it derives every capability's reads. The
55
- * base exists for `keys:rotate`, which is not a read and would otherwise drop out.
62
+ * **{@link MIGRATIONS_READ_SCOPE} and {@link PRIVACY_READ_SCOPE} are absent on purpose.** Each is a read —
63
+ * every route requiring it is a `GET` — so `connect` derives it from the seam's `adminRoutes` the way it
64
+ * derives every capability's reads. The base exists for `keys:rotate`, which is not a read and would
65
+ * otherwise drop out.
56
66
  */
57
67
  const SEAM_SCOPES = [MANIFEST_READ_SCOPE, KEYS_ROTATE_SCOPE];
58
68
  /**
@@ -73,4 +83,4 @@ function scopeCovers(required, presented, granted) {
73
83
  return granted.includes(required);
74
84
  }
75
85
  //#endregion
76
- export { ANY_VERIFIED_CALLER, ControlPlaneScope, KEYS_ROTATE_SCOPE, MANIFEST_READ_SCOPE, MIGRATIONS_READ_SCOPE, SEAM_SCOPES, scopeCovers };
86
+ export { ANY_VERIFIED_CALLER, ControlPlaneScope, KEYS_ROTATE_SCOPE, MANIFEST_READ_SCOPE, MIGRATIONS_READ_SCOPE, PRIVACY_READ_SCOPE, SEAM_SCOPES, scopeCovers };
@@ -1 +1 @@
1
- {"version":3,"file":"createBackend.d.ts","sourceRoot":"","sources":["../src/createBackend.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EAEpB,SAAS,EACV,MAAM,yBAAyB,CAAC;AAOjC,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMtF,OAAO,EAA8B,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAO9C,OAAO,EAA2B,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,oGAAoG;AACpG,MAAM,WAAW,oBAAoB,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,EAAE,GAAG,SAAS,UAAU;IAC9F,2EAA2E;IAC3E,YAAY,EAAE,IAAI,CAAC;IACnB;;;OAGG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC;IACV;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,iHAAiH;AACjH,KAAK,UAAU,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,IAAI;IACpD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,WAAW,CAAC,GAAG;QACtD,EAAE,EAAE,UAAU,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC,EAAE,EAAE,UAAU,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;QACzC,SAAS,EAAE,kBAAkB,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC;KAC3D,CAAC;CACH,CAAC;AAuCF,kCAAkC;AAClC,eAAO,MAAM,UAAU;;;iBAQ+B,CAAC;AACvD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,KAAK,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,EACxC,KAAK,CAAC,GAAG,SAAS,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EACrF,OAAO,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CA0MrF"}
1
+ {"version":3,"file":"createBackend.d.ts","sourceRoot":"","sources":["../src/createBackend.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,KAAK,EACV,UAAU,EACV,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EAEpB,SAAS,EACV,MAAM,yBAAyB,CAAC;AAOjC,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMtF,OAAO,EAA8B,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE9E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAQ9C,OAAO,EAA2B,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAGvF,oGAAoG;AACpG,MAAM,WAAW,oBAAoB,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,EAAE,GAAG,SAAS,UAAU;IAC9F,2EAA2E;IAC3E,YAAY,EAAE,IAAI,CAAC;IACnB;;;OAGG;IACH,GAAG,CAAC,EAAE,GAAG,CAAC;IACV;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,iHAAiH;AACjH,KAAK,UAAU,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,IAAI;IACpD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,WAAW,CAAC,GAAG;QACtD,EAAE,EAAE,UAAU,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC;QACtC,EAAE,EAAE,UAAU,CAAC,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;QACzC,SAAS,EAAE,kBAAkB,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC;KAC3D,CAAC;CACH,CAAC;AAuCF,kCAAkC;AAClC,eAAO,MAAM,UAAU;;;iBAQ+B,CAAC;AACvD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,KAAK,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,EACxC,KAAK,CAAC,GAAG,SAAS,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EACrF,OAAO,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CA6MrF"}
@@ -7,6 +7,7 @@ import { pithyCommandOf } from "./cli/command.js";
7
7
  import { BindingSpec } from "./capability/bindings.js";
8
8
  import { recordComposition } from "./capability/composition.js";
9
9
  import { validateBindings } from "./capability/validateBindings.js";
10
+ import { refuseEncryptedKvStores } from "./privacy/encryptedColumns.js";
10
11
  import { workerVersion } from "./worker/identity.js";
11
12
  import { describeRoute } from "./http/routeSpec.js";
12
13
  import { buildDbRegistry, composeDatabases } from "./data/databases.js";
@@ -99,6 +100,7 @@ function createBackend(options) {
99
100
  const messages = composeMessages(all);
100
101
  const defaultTranslator = bakedTranslator(catalogFor(messages, "en"));
101
102
  const namespaces = composeKv(all);
103
+ refuseEncryptedKvStores(all);
102
104
  const workflows = composeWorkflows(all);
103
105
  const required = dedupeBindings([
104
106
  ...all.flatMap((cap) => cap.requiredBindings),
@@ -23,7 +23,13 @@ export interface PithyEntrypoint {
23
23
  */
24
24
  email?: (message: ForwardableEmailMessage, env: Record<string, unknown>, ctx: ExecutionContext) => Promise<void>;
25
25
  /**
26
- * The Worker `scheduled` handler — starts every registered job that declares a `schedule`.
26
+ * The Worker `scheduled` handler — starts every registered job whose `schedule` is the cron that fired.
27
+ *
28
+ * A Worker carries one `scheduled` entry for all of its `triggers.crons`, and the runtime names the one
29
+ * that fired on `controller.cron`, verbatim as `wrangler.jsonc` declares it. Only the jobs whose
30
+ * `schedule` is that string start: a daily job composed beside an hourly one must not run every hour,
31
+ * and the hourly one must not run an extra time at the daily job's minute (#622). An event that names
32
+ * no cron, or one no job declares, starts nothing.
27
33
  *
28
34
  * **Present only when at least one job carries a cron.** The reason differs from `email`'s: an
29
35
  * inert `email()` silently *drops* mail, which is a correctness bug, whereas an inert `scheduled()`
@@ -1 +1 @@
1
- {"version":3,"file":"createEntrypoint.d.ts","sourceRoot":"","sources":["../src/createEntrypoint.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAC3F,OAAO,KAAK,EAAE,UAAU,EAA0B,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAAE,KAAK,oBAAoB,EAAiB,MAAM,iBAAiB,CAAC;AAK3E;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/G;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjH;;;;;;;;;;;;;;;;;;;OAmBG;IACH,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzG;AAkBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,EACxC,KAAK,CAAC,GAAG,SAAS,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EACrF,OAAO,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,eAAe,CAyD3D"}
1
+ {"version":3,"file":"createEntrypoint.d.ts","sourceRoot":"","sources":["../src/createEntrypoint.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAC3F,OAAO,KAAK,EAAE,UAAU,EAA0B,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAAE,KAAK,oBAAoB,EAAiB,MAAM,iBAAiB,CAAC;AAK3E;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/G;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjH;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzG;AAyBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,EACxC,KAAK,CAAC,GAAG,SAAS,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EACrF,OAAO,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,eAAe,CA2D3D"}
@@ -5,6 +5,12 @@ import { composeWorkflows, scheduledWorkflows } from "./workflow/register.js";
5
5
  import { createBackend } from "./createBackend.js";
6
6
  import { withLocalServices } from "./worker/serviceLoopback.js";
7
7
  //#region src/createEntrypoint.ts
8
+ /** The cron expression a `scheduled` event fired for, or `undefined` when the event names none. */
9
+ function firedCron(controller) {
10
+ if (typeof controller !== "object" || controller === null) return void 0;
11
+ const cron = controller.cron;
12
+ return typeof cron === "string" ? cron : void 0;
13
+ }
8
14
  /**
9
15
  * The raw MIME stream of a `ForwardableEmailMessage` is **single-use** — once one handler reads it,
10
16
  * the next sees an empty stream. When more than one capability handles inbound mail we buffer the
@@ -33,12 +39,16 @@ function createEntrypoint(options) {
33
39
  const registry = composeWorkflows(all);
34
40
  const scheduled = scheduledWorkflows(registry);
35
41
  const entrypoint = { fetch: (request, env, ctx) => app.fetch(request, withLocalServices(env), ctx) };
36
- if (scheduled.length > 0) entrypoint.scheduled = async (_controller, env) => {
42
+ if (scheduled.length > 0) entrypoint.scheduled = async (controller, env) => {
43
+ const cron = firedCron(controller);
37
44
  const failures = [];
38
- for (const entry of scheduled) try {
39
- await triggerWorkflow(withLocalServices(env), registry, entry.key, {});
40
- } catch (error) {
41
- failures.push(error);
45
+ for (const entry of scheduled) {
46
+ if (entry.spec.schedule !== cron) continue;
47
+ try {
48
+ await triggerWorkflow(withLocalServices(env), registry, entry.key, {});
49
+ } catch (error) {
50
+ failures.push(error);
51
+ }
42
52
  }
43
53
  if (failures[0] !== void 0) throw failures[0];
44
54
  };
@@ -0,0 +1,86 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { z } from "zod";
4
+ /**
5
+ * **The AES-256-GCM envelope: one primitive, two callers** (#390).
6
+ *
7
+ * It was `@pithy-sh/secrets`' own, ported from the CMS `secretsCrypto`, and it sealed that capability's rows and
8
+ * nothing else. Field encryption needs the same primitive inside an adopter's own Workers, and a capability may not
9
+ * depend on another's internals (principle 4), so it lives in core now and both callers import it from here. The
10
+ * format did not change: a row secrets sealed before the move opens after it, byte for byte.
11
+ *
12
+ * ## Two version axes
13
+ *
14
+ * `currentVersion` says which key seals new writes, and the `keyVersion` persisted on every envelope says which key
15
+ * opens it. A rotation adds a version, moves the pointer and keeps the old key until nothing is sealed under it, so
16
+ * a row opens under whichever version sealed it, for as long as that version is in the set. This is the
17
+ * *encryption-key* version. A secret's *value* version is a separate concern (`secrets/src/crypto/versionedValue.ts`).
18
+ *
19
+ * The config is the uniform `{ currentVersion, versions }` shape every versioned value uses, plus `lastRotatedAt`
20
+ * beside it as rotation metadata. Version keys are stringified integers, so reading the keys is a full-set read:
21
+ * every still-valid version, never the current one alone.
22
+ *
23
+ * ## The bound context is the caller's, and it is authenticated
24
+ *
25
+ * Every seal binds a **context** string as AES-GCM additional authenticated data. It is not encrypted; it is bound.
26
+ * Without it a ciphertext is valid under the key alone, so one lifted from one row and written into another opens
27
+ * cleanly, and every decision about which value a caller gets lives in the query above. With it, a moved ciphertext
28
+ * fails inside the primitive, however that query is later rewritten.
29
+ *
30
+ * The primitive takes the context whole, because what to bind is the caller's fact, not this module's:
31
+ *
32
+ * - `@pithy-sh/secrets` binds `pithy.secrets.v1:<stored name>` — exactly what it always bound.
33
+ * - A field marked `encrypted()` binds `pithy/enc/1:<declared name>:<field path>:<primary key>` (`../privacy/sealed`).
34
+ *
35
+ * Each prefix is versioned, so a later change to what is bound is a deliberate, visible break, and the two prefixes
36
+ * are distinct, so a secret's ciphertext never opens as a field and a field's never opens as a secret. **What is bound
37
+ * must be stable for the life of the ciphertext**: each caller states why its own context is.
38
+ *
39
+ * An empty context is refused on both halves. It binds nothing, and it would fail open rather than loudly.
40
+ */
41
+ /** The key set a seal draws from: which version is current, every still-valid key, and when it last rotated. */
42
+ export declare const EncryptionConfig: z.ZodObject<{
43
+ currentVersion: z.ZodString;
44
+ versions: z.ZodRecord<z.ZodString, z.ZodString>;
45
+ lastRotatedAt: z.ZodISODateTime;
46
+ }, z.core.$strip>;
47
+ export type EncryptionConfig = z.output<typeof EncryptionConfig>;
48
+ /**
49
+ * One AES-256-GCM envelope: base64 ciphertext, base64 IV, and the key version that sealed it.
50
+ *
51
+ * The bound context is deliberately not in here. It is supplied by the caller on the way back in, from the row's own
52
+ * identity, so a row that has been moved cannot carry its old context with it.
53
+ */
54
+ export declare const EncryptedEnvelope: z.ZodObject<{
55
+ encryptedValue: z.ZodString;
56
+ iv: z.ZodString;
57
+ keyVersion: z.ZodNumber;
58
+ }, z.core.$strip>;
59
+ export type EncryptedEnvelope = z.output<typeof EncryptedEnvelope>;
60
+ /** What the refusals name the key set by — its binding, so an operator knows which entry to inspect. */
61
+ export interface EnvelopeOptions {
62
+ /** The binding the key set was read from, e.g. `SECRETS_ENCRYPTION_KEYS`. Defaults to `the key set`. */
63
+ keys?: string;
64
+ }
65
+ /** Base64-encode raw bytes without spreading into `fromCharCode` (stack-safe, lint-clean). */
66
+ export declare function toBase64(bytes: Uint8Array): string;
67
+ /** Decode base64 to raw bytes. */
68
+ export declare function fromBase64(b64: string): Uint8Array<ArrayBuffer>;
69
+ /** The raw key bytes for `version`, or a refusal naming the key set. Never the key itself. */
70
+ export declare function keyBytes(config: EncryptionConfig, version: number, options?: EnvelopeOptions): Uint8Array<ArrayBuffer>;
71
+ /**
72
+ * Seal `plaintext` under the config's current key version, binding `context`. The IV is 12 fresh random bytes, so
73
+ * two seals of one value never share a ciphertext.
74
+ */
75
+ export declare function encryptValue(config: EncryptionConfig, context: string, plaintext: string, options?: EnvelopeOptions): Promise<EncryptedEnvelope>;
76
+ /**
77
+ * Open an envelope sealed under `context`. The key version may differ from `currentVersion` during a rotation window;
78
+ * the matching key must still be in the config.
79
+ *
80
+ * A missing key version, a tampered ciphertext, the wrong key, or a ciphertext sealed under another context all throw
81
+ * — never the plaintext or key material. GCM cannot tell those apart, and separating them for the caller would be an
82
+ * oracle: the `detail` names the context the open was *attempted under*, which is what an operator needs. Each caller
83
+ * binds a context composed from validated identifiers, so it is safe in a log; neither binds a value.
84
+ */
85
+ export declare function decryptValue(config: EncryptionConfig, context: string, envelope: EncryptedEnvelope, options?: EnvelopeOptions): Promise<string>;
86
+ //# sourceMappingURL=envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../../src/crypto/envelope.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,gHAAgH;AAChH,eAAO,MAAM,gBAAgB;;;;iBAsB1B,CAAC;AACJ,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEjE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB;;;;iBASqE,CAAC;AACpG,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEnE,wGAAwG;AACxG,MAAM,WAAW,eAAe;IAC9B,wGAAwG;IACxG,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,8FAA8F;AAC9F,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAIlD;AAED,kCAAkC;AAClC,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC,WAAW,CAAC,CAE/D;AAYD,8FAA8F;AAC9F,wBAAgB,QAAQ,CACtB,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,eAAoB,GAC5B,UAAU,CAAC,WAAW,CAAC,CAQzB;AAYD;;;GAGG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,iBAAiB,CAAC,CAW5B;AAED;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAkBjB"}