@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.
- package/dist/audit/composed.d.ts +18 -0
- package/dist/audit/composed.d.ts.map +1 -0
- package/dist/audit/composed.js +28 -0
- package/dist/audit/recorder.d.ts +10 -2
- package/dist/audit/recorder.d.ts.map +1 -1
- package/dist/capability/capability.d.ts +23 -0
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/capability.js +2 -0
- package/dist/capability/purge.d.ts +122 -0
- package/dist/capability/purge.d.ts.map +1 -0
- package/dist/capability/purge.js +131 -0
- package/dist/cli/commandCatalog.generated.d.ts +54 -2
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
- package/dist/cli/commandCatalog.generated.js +61 -0
- package/dist/cli/commandFacts.d.ts +1 -0
- package/dist/cli/commandFacts.d.ts.map +1 -1
- package/dist/cli/commandFacts.js +1 -0
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +7 -0
- package/dist/controlPlane/data/connection.d.ts +2 -12
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +1 -1
- package/dist/controlPlane/discovery/adminRoute.d.ts +29 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -1
- package/dist/controlPlane/discovery/adminRoute.js +3 -0
- package/dist/controlPlane/error/errors.d.ts +4 -3
- package/dist/controlPlane/error/errors.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +40 -1
- package/dist/controlPlane/http/handlers.d.ts +79 -0
- package/dist/controlPlane/http/handlers.d.ts.map +1 -1
- package/dist/controlPlane/http/handlers.js +37 -3
- package/dist/controlPlane/http/responses.d.ts +98 -0
- package/dist/controlPlane/http/responses.d.ts.map +1 -1
- package/dist/controlPlane/http/responses.js +41 -1
- package/dist/controlPlane/http/routes.d.ts +1 -0
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +17 -3
- package/dist/controlPlane/scope/scope.d.ts +13 -3
- package/dist/controlPlane/scope/scope.d.ts.map +1 -1
- package/dist/controlPlane/scope/scope.js +14 -4
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +2 -0
- package/dist/createEntrypoint.d.ts +7 -1
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +15 -5
- package/dist/crypto/envelope.d.ts +86 -0
- package/dist/crypto/envelope.d.ts.map +1 -0
- package/dist/crypto/envelope.js +129 -0
- package/dist/crypto/fieldKeys.d.ts +41 -0
- package/dist/crypto/fieldKeys.d.ts.map +1 -0
- package/dist/crypto/fieldKeys.js +56 -0
- package/dist/data/codecs.d.ts +23 -10
- package/dist/data/codecs.d.ts.map +1 -1
- package/dist/data/codecs.js +50 -36
- package/dist/error/extend.d.ts +6 -5
- package/dist/error/extend.d.ts.map +1 -1
- package/dist/error/http.d.ts +72 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +144 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +48 -0
- package/dist/error/pithyError.d.ts +18 -7
- package/dist/error/pithyError.d.ts.map +1 -1
- package/dist/error/pithyError.js +6 -0
- package/dist/http/validation.d.ts +11 -2
- package/dist/http/validation.d.ts.map +1 -1
- package/dist/http/validation.js +20 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/logger/logger.d.ts +12 -5
- package/dist/logger/logger.d.ts.map +1 -1
- package/dist/logger/record.d.ts +42 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/privacy/blindIndex.d.ts +27 -0
- package/dist/privacy/blindIndex.d.ts.map +1 -0
- package/dist/privacy/blindIndex.js +57 -0
- package/dist/privacy/classes.d.ts +142 -0
- package/dist/privacy/classes.d.ts.map +1 -0
- package/dist/privacy/classes.js +209 -0
- package/dist/privacy/classify.d.ts +336 -0
- package/dist/privacy/classify.d.ts.map +1 -0
- package/dist/privacy/classify.js +1024 -0
- package/dist/privacy/encrypted.d.ts +210 -0
- package/dist/privacy/encrypted.d.ts.map +1 -0
- package/dist/privacy/encrypted.js +350 -0
- package/dist/privacy/encryptedColumns.d.ts +64 -0
- package/dist/privacy/encryptedColumns.d.ts.map +1 -0
- package/dist/privacy/encryptedColumns.js +134 -0
- package/dist/privacy/fieldRotation.d.ts +34 -0
- package/dist/privacy/fieldRotation.d.ts.map +1 -0
- package/dist/privacy/fieldRotation.js +186 -0
- package/dist/privacy/inventory.d.ts +12 -0
- package/dist/privacy/inventory.d.ts.map +1 -0
- package/dist/privacy/inventory.js +154 -0
- package/dist/privacy/posture.d.ts +114 -0
- package/dist/privacy/posture.d.ts.map +1 -0
- package/dist/privacy/posture.js +136 -0
- package/dist/privacy/redact.d.ts +7 -0
- package/dist/privacy/redact.d.ts.map +1 -0
- package/dist/privacy/redact.js +404 -0
- package/dist/privacy/report.d.ts +58 -0
- package/dist/privacy/report.d.ts.map +1 -0
- package/dist/privacy/report.js +15 -0
- package/dist/privacy/retirement.d.ts +68 -0
- package/dist/privacy/retirement.d.ts.map +1 -0
- package/dist/privacy/retirement.js +72 -0
- package/dist/privacy/sealed.d.ts +87 -0
- package/dist/privacy/sealed.d.ts.map +1 -0
- package/dist/privacy/sealed.js +221 -0
- package/dist/privacy/types.d.ts +559 -0
- package/dist/privacy/types.d.ts.map +1 -0
- package/dist/privacy/types.js +2 -0
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +1 -1
- package/src/audit/composed.ts +30 -0
- package/src/audit/recorder.ts +10 -2
- package/src/capability/capability.ts +26 -0
- package/src/capability/purge.ts +187 -0
- package/src/cli/commandCatalog.generated.ts +65 -0
- package/src/cli/commandFacts.ts +1 -0
- package/src/controlPlane/capability.ts +11 -0
- package/src/controlPlane/discovery/adminRoute.ts +11 -0
- package/src/controlPlane/error/errors.ts +4 -3
- package/src/controlPlane/http/guard.ts +60 -1
- package/src/controlPlane/http/handlers.ts +57 -4
- package/src/controlPlane/http/responses.ts +100 -0
- package/src/controlPlane/http/routes.ts +31 -1
- package/src/controlPlane/scope/scope.ts +14 -3
- package/src/controlPlane/token/base64url.ts +1 -1
- package/src/createBackend.ts +4 -0
- package/src/createEntrypoint.ts +17 -2
- package/src/crypto/envelope.ts +196 -0
- package/src/crypto/fieldKeys.ts +76 -0
- package/src/data/codecs.ts +69 -37
- package/src/error/extend.ts +6 -5
- package/src/error/payload.ts +110 -0
- package/src/error/pithyError.ts +21 -7
- package/src/http/validation.ts +35 -3
- package/src/index.ts +1 -0
- package/src/logger/logger.ts +12 -5
- package/src/privacy/blindIndex.ts +81 -0
- package/src/privacy/classes.ts +277 -0
- package/src/privacy/classify.ts +1447 -0
- package/src/privacy/encrypted.ts +513 -0
- package/src/privacy/encryptedColumns.ts +185 -0
- package/src/privacy/fieldRotation.ts +234 -0
- package/src/privacy/inventory.ts +147 -0
- package/src/privacy/posture.ts +205 -0
- package/src/privacy/redact.ts +423 -0
- package/src/privacy/report.ts +64 -0
- package/src/privacy/retirement.ts +102 -0
- package/src/privacy/sealed.ts +323 -0
- package/src/privacy/types.ts +804 -0
- 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 {
|
|
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:
|
|
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;
|
|
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;
|
|
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}
|
|
64
|
-
* — so `connect` derives it from the seam's `adminRoutes` the way it
|
|
65
|
-
* base exists for `keys:rotate`, which is not a read and would
|
|
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}
|
|
54
|
-
* — so `connect` derives it from the seam's `adminRoutes` the way it
|
|
55
|
-
* base exists for `keys:rotate`, which is not a read and would
|
|
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;
|
|
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"}
|
package/dist/createBackend.js
CHANGED
|
@@ -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
|
|
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
|
|
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"}
|
package/dist/createEntrypoint.js
CHANGED
|
@@ -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 (
|
|
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)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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"}
|