@pithy-sh/core 0.10.0 → 0.12.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/access/access.d.ts +172 -0
- package/dist/access/access.d.ts.map +1 -0
- package/dist/access/access.js +147 -0
- package/dist/access/acting.d.ts +49 -0
- package/dist/access/acting.d.ts.map +1 -0
- package/dist/access/acting.js +21 -0
- package/dist/access/policyCheck.d.ts +33 -0
- package/dist/access/policyCheck.d.ts.map +1 -0
- package/dist/access/policyCheck.js +48 -0
- package/dist/access/usage.d.ts +83 -0
- package/dist/access/usage.d.ts.map +1 -0
- package/dist/access/usage.js +104 -0
- package/dist/access/visibility.d.ts +66 -0
- package/dist/access/visibility.d.ts.map +1 -0
- package/dist/access/visibility.js +86 -0
- package/dist/audit/auditEvent.d.ts +5 -0
- package/dist/audit/auditEvent.d.ts.map +1 -1
- package/dist/audit/auditEvent.js +2 -0
- package/dist/audit/recorder.d.ts +10 -2
- package/dist/audit/recorder.d.ts.map +1 -1
- package/dist/capability/bindings.d.ts +47 -2
- package/dist/capability/bindings.d.ts.map +1 -1
- package/dist/capability/bindings.js +117 -7
- package/dist/capability/capability.d.ts +110 -28
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/capability.js +2 -0
- package/dist/capability/manifest.d.ts +2 -1
- package/dist/capability/manifest.d.ts.map +1 -1
- package/dist/capability/manifest.js +1 -1
- package/dist/capability/purge.d.ts +80 -0
- package/dist/capability/purge.d.ts.map +1 -0
- package/dist/capability/purge.js +76 -0
- package/dist/capability/settings.d.ts +86 -2
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/settings.js +80 -1
- package/dist/cli/commandCatalog.generated.d.ts +238 -24
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
- package/dist/cli/commandCatalog.generated.js +251 -22
- package/dist/cli/commandFacts.d.ts +2 -1
- package/dist/cli/commandFacts.d.ts.map +1 -1
- package/dist/cli/commandFacts.js +2 -0
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +19 -2
- package/dist/controlPlane/context.d.ts +5 -0
- package/dist/controlPlane/context.d.ts.map +1 -1
- package/dist/controlPlane/context.js +3 -1
- package/dist/controlPlane/data/connection.d.ts +49 -12
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +56 -2
- 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/cors.d.ts +4 -4
- package/dist/controlPlane/http/cors.js +4 -4
- package/dist/controlPlane/http/guard.d.ts +11 -0
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +17 -1
- package/dist/controlPlane/http/handlers.d.ts +139 -0
- package/dist/controlPlane/http/handlers.d.ts.map +1 -1
- package/dist/controlPlane/http/handlers.js +67 -4
- package/dist/controlPlane/http/readiness.d.ts +33 -0
- package/dist/controlPlane/http/readiness.d.ts.map +1 -0
- package/dist/controlPlane/http/readiness.js +39 -0
- package/dist/controlPlane/http/responses.d.ts +245 -0
- package/dist/controlPlane/http/responses.d.ts.map +1 -1
- package/dist/controlPlane/http/responses.js +101 -4
- package/dist/controlPlane/http/routes.d.ts +2 -0
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +31 -3
- package/dist/controlPlane/http/verify.d.ts.map +1 -1
- package/dist/controlPlane/http/verify.js +2 -1
- package/dist/controlPlane/migrationState/migrationState.d.ts +102 -0
- package/dist/controlPlane/migrationState/migrationState.d.ts.map +1 -0
- package/dist/controlPlane/migrationState/migrationState.js +142 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts +21 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0003_connection_principal.js +33 -0
- package/dist/controlPlane/scope/scope.d.ts +23 -2
- package/dist/controlPlane/scope/scope.d.ts.map +1 -1
- package/dist/controlPlane/scope/scope.js +24 -3
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +9 -2
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +5 -3
- 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/entitlement/allowance.d.ts +25 -0
- package/dist/entitlement/allowance.d.ts.map +1 -0
- package/dist/entitlement/allowance.js +80 -0
- package/dist/entitlement/entitlement.d.ts +41 -0
- package/dist/entitlement/entitlement.d.ts.map +1 -1
- package/dist/entitlement/meter.d.ts +57 -0
- package/dist/entitlement/meter.d.ts.map +1 -0
- package/dist/entitlement/meter.js +13 -0
- package/dist/error/extend.d.ts +6 -5
- package/dist/error/extend.d.ts.map +1 -1
- package/dist/error/http.d.ts +168 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +336 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +116 -4
- 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/authContext.d.ts +7 -0
- package/dist/http/authContext.d.ts.map +1 -1
- package/dist/http/authContext.js +8 -1
- package/dist/http/scopeGates.d.ts +20 -0
- package/dist/http/scopeGates.d.ts.map +1 -0
- package/dist/http/scopeGates.js +36 -0
- 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 +98 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/migrations/capabilitySets.d.ts +23 -0
- package/dist/migrations/capabilitySets.d.ts.map +1 -0
- package/dist/migrations/capabilitySets.js +74 -0
- package/dist/migrations/registry.d.ts +26 -0
- package/dist/migrations/registry.d.ts.map +1 -1
- package/dist/migrations/registry.js +38 -7
- package/dist/migrations/state.d.ts +60 -0
- package/dist/migrations/state.d.ts.map +1 -0
- package/dist/migrations/state.js +46 -0
- package/dist/naming/feature.d.ts +27 -5
- package/dist/naming/feature.d.ts.map +1 -1
- package/dist/naming/feature.js +36 -6
- package/dist/naming/limits.d.ts +47 -3
- package/dist/naming/limits.d.ts.map +1 -1
- package/dist/naming/limits.js +47 -3
- package/dist/naming/provisionScope.d.ts +6 -0
- package/dist/naming/provisionScope.d.ts.map +1 -1
- package/dist/naming/provisionScope.js +8 -3
- package/dist/naming/resourceNames.d.ts +21 -2
- package/dist/naming/resourceNames.d.ts.map +1 -1
- package/dist/naming/resourceNames.js +21 -3
- 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/seed/writeD1.d.ts +7 -1
- package/dist/seed/writeD1.d.ts.map +1 -1
- package/dist/seed/writeD1.js +9 -3
- package/dist/seed/writeKv.d.ts +2 -0
- package/dist/seed/writeKv.d.ts.map +1 -1
- package/dist/seed/writeKv.js +6 -2
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/worker/serviceLoopback.d.ts +36 -0
- package/dist/worker/serviceLoopback.d.ts.map +1 -0
- package/dist/worker/serviceLoopback.js +74 -0
- package/dist/workflow/faults.d.ts +12 -1
- package/dist/workflow/faults.d.ts.map +1 -1
- package/dist/workflow/faults.js +22 -15
- package/package.json +2 -2
- package/src/access/access.ts +298 -0
- package/src/access/acting.ts +59 -0
- package/src/access/policyCheck.ts +77 -0
- package/src/access/usage.ts +141 -0
- package/src/access/visibility.ts +109 -0
- package/src/audit/auditEvent.ts +4 -0
- package/src/audit/recorder.ts +10 -2
- package/src/capability/bindings.ts +159 -13
- package/src/capability/capability.ts +114 -29
- package/src/capability/purge.ts +124 -0
- package/src/capability/settings.ts +146 -2
- package/src/cli/commandCatalog.generated.ts +271 -22
- package/src/cli/commandFacts.ts +2 -0
- package/src/controlPlane/capability.ts +36 -1
- package/src/controlPlane/context.ts +4 -0
- package/src/controlPlane/data/connection.ts +90 -0
- package/src/controlPlane/discovery/adminRoute.ts +11 -0
- package/src/controlPlane/error/errors.ts +4 -3
- package/src/controlPlane/http/cors.ts +4 -4
- package/src/controlPlane/http/guard.ts +21 -0
- package/src/controlPlane/http/handlers.ts +98 -4
- package/src/controlPlane/http/readiness.ts +50 -0
- package/src/controlPlane/http/responses.ts +240 -2
- package/src/controlPlane/http/routes.ts +55 -1
- package/src/controlPlane/http/verify.ts +2 -0
- package/src/controlPlane/migrationState/migrationState.ts +200 -0
- package/src/controlPlane/migrations/0003_connection_principal.ts +37 -0
- package/src/controlPlane/scope/scope.ts +25 -2
- package/src/controlPlane/token/base64url.ts +1 -1
- package/src/createBackend.ts +28 -6
- package/src/createEntrypoint.ts +9 -3
- package/src/crypto/envelope.ts +196 -0
- package/src/crypto/fieldKeys.ts +76 -0
- package/src/data/codecs.ts +69 -37
- package/src/entitlement/allowance.ts +85 -0
- package/src/entitlement/entitlement.ts +38 -0
- package/src/entitlement/meter.ts +69 -0
- package/src/error/extend.ts +6 -5
- package/src/error/payload.ts +253 -4
- package/src/error/pithyError.ts +21 -7
- package/src/http/authContext.ts +13 -1
- package/src/http/scopeGates.ts +34 -0
- package/src/index.ts +2 -0
- package/src/logger/logger.ts +12 -5
- package/src/migrations/capabilitySets.ts +87 -0
- package/src/migrations/registry.ts +46 -2
- package/src/migrations/state.ts +94 -0
- package/src/naming/feature.ts +46 -7
- package/src/naming/limits.ts +35 -3
- package/src/naming/provisionScope.ts +19 -1
- package/src/naming/resourceNames.ts +52 -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/seed/writeD1.ts +13 -4
- package/src/seed/writeKv.ts +6 -2
- package/src/version.generated.ts +1 -1
- package/src/worker/serviceLoopback.ts +105 -0
- package/src/workflow/faults.ts +51 -24
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { EncryptionConfig } from "./envelope";
|
|
4
|
+
/**
|
|
5
|
+
* **The field key: what an app Worker seals its encrypted fields with** (#390).
|
|
6
|
+
*
|
|
7
|
+
* `PITHY_FIELD_KEYS` is an {@link EncryptionConfig}, the same versioned shape as secrets' master key, and it is a
|
|
8
|
+
* **different key on purpose**. Field encryption seals and opens inside the adopter's own Workers, so every Worker that
|
|
9
|
+
* holds an encrypted field must hold the key it uses. Were that the master key `SECRETS_ENCRYPTION_KEYS`, every such
|
|
10
|
+
* Worker would hold the key to the whole secrets vault, and one compromised would open it. A key of its own limits
|
|
11
|
+
* that to the fields: a Worker that holds an encrypted field and composes no `secrets` is bound this key and never
|
|
12
|
+
* the master key.
|
|
13
|
+
*
|
|
14
|
+
* It is not a second mechanism: `pithy secrets provision` mints it beside the master key, in the same format, binds
|
|
15
|
+
* it to every Worker that holds an encrypted field, and the manager's at-rest cron rotates it every
|
|
16
|
+
* {@link FIELD_KEY_ROTATION_DAYS} days.
|
|
17
|
+
*/
|
|
18
|
+
/** The binding an app Worker reads its field key through. Fixed across environments; only the store entry is scoped. */
|
|
19
|
+
export declare const FIELD_KEYS_BINDING = "PITHY_FIELD_KEYS";
|
|
20
|
+
/** How often the manager rotates the field key, in days. */
|
|
21
|
+
export declare const FIELD_KEY_ROTATION_DAYS = 30;
|
|
22
|
+
/** A Secrets Store binding: `.get()` resolves the plaintext inside the Worker. */
|
|
23
|
+
export interface FieldKeyBinding {
|
|
24
|
+
get(): Promise<string>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Where a field key comes from: the Worker's `env.PITHY_FIELD_KEYS` binding (a Secrets Store binding deployed, a JSON
|
|
28
|
+
* string from `.dev.vars` locally), or a config a caller already resolved.
|
|
29
|
+
*/
|
|
30
|
+
export type FieldKeys = FieldKeyBinding | string | EncryptionConfig;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a field key to its config. A missing binding, text that is not JSON, and JSON that is not a key set are each
|
|
33
|
+
* refused, and **nothing read is ever echoed**: what failed to parse is the key set.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveFieldKeys(keys: FieldKeys | undefined): Promise<EncryptionConfig>;
|
|
36
|
+
/**
|
|
37
|
+
* Every live version in a key set, newest first, as numbers. A version key that is not an integer is refused rather
|
|
38
|
+
* than sorted as text, where `"10"` sorts before `"9"`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function liveVersions(config: EncryptionConfig): number[];
|
|
41
|
+
//# sourceMappingURL=fieldKeys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldKeys.d.ts","sourceRoot":"","sources":["../../src/crypto/fieldKeys.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;GAaG;AAEH,wHAAwH;AACxH,eAAO,MAAM,kBAAkB,qBAAqB,CAAC;AAErD,4DAA4D;AAC5D,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,eAAe,GAAG,MAAM,GAAG,gBAAgB,CAAC;AAEpE;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAoB7F;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAM/D"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { InternalError } from "../error/pithyError.js";
|
|
4
|
+
import { remedy } from "../cli/command.js";
|
|
5
|
+
import { EncryptionConfig } from "./envelope.js";
|
|
6
|
+
//#region src/crypto/fieldKeys.ts
|
|
7
|
+
/**
|
|
8
|
+
* **The field key: what an app Worker seals its encrypted fields with** (#390).
|
|
9
|
+
*
|
|
10
|
+
* `PITHY_FIELD_KEYS` is an {@link EncryptionConfig}, the same versioned shape as secrets' master key, and it is a
|
|
11
|
+
* **different key on purpose**. Field encryption seals and opens inside the adopter's own Workers, so every Worker that
|
|
12
|
+
* holds an encrypted field must hold the key it uses. Were that the master key `SECRETS_ENCRYPTION_KEYS`, every such
|
|
13
|
+
* Worker would hold the key to the whole secrets vault, and one compromised would open it. A key of its own limits
|
|
14
|
+
* that to the fields: a Worker that holds an encrypted field and composes no `secrets` is bound this key and never
|
|
15
|
+
* the master key.
|
|
16
|
+
*
|
|
17
|
+
* It is not a second mechanism: `pithy secrets provision` mints it beside the master key, in the same format, binds
|
|
18
|
+
* it to every Worker that holds an encrypted field, and the manager's at-rest cron rotates it every
|
|
19
|
+
* {@link FIELD_KEY_ROTATION_DAYS} days.
|
|
20
|
+
*/
|
|
21
|
+
/** The binding an app Worker reads its field key through. Fixed across environments; only the store entry is scoped. */
|
|
22
|
+
const FIELD_KEYS_BINDING = "PITHY_FIELD_KEYS";
|
|
23
|
+
/** How often the manager rotates the field key, in days. */
|
|
24
|
+
const FIELD_KEY_ROTATION_DAYS = 30;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a field key to its config. A missing binding, text that is not JSON, and JSON that is not a key set are each
|
|
27
|
+
* refused, and **nothing read is ever echoed**: what failed to parse is the key set.
|
|
28
|
+
*/
|
|
29
|
+
async function resolveFieldKeys(keys) {
|
|
30
|
+
let raw = keys;
|
|
31
|
+
if (keys === void 0 || keys === null) throw new InternalError({
|
|
32
|
+
message: "This Worker holds an encrypted field and no field key.",
|
|
33
|
+
action: `Bind ${FIELD_KEYS_BINDING}. ${remedy("secrets provision").say((command) => `${command} binds it to every Worker with an encrypted field.`)}`,
|
|
34
|
+
detail: `${FIELD_KEYS_BINDING} binding is not configured`
|
|
35
|
+
});
|
|
36
|
+
if (typeof keys === "object" && "get" in keys && typeof keys.get === "function") raw = await keys.get();
|
|
37
|
+
if (typeof raw === "string") try {
|
|
38
|
+
raw = JSON.parse(raw);
|
|
39
|
+
} catch {
|
|
40
|
+
throw new InternalError({ detail: `${FIELD_KEYS_BINDING} is not valid JSON` });
|
|
41
|
+
}
|
|
42
|
+
const parsed = EncryptionConfig.safeParse(raw);
|
|
43
|
+
if (!parsed.success) throw new InternalError({ detail: `${FIELD_KEYS_BINDING} is not a valid EncryptionConfig` });
|
|
44
|
+
return parsed.data;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Every live version in a key set, newest first, as numbers. A version key that is not an integer is refused rather
|
|
48
|
+
* than sorted as text, where `"10"` sorts before `"9"`.
|
|
49
|
+
*/
|
|
50
|
+
function liveVersions(config) {
|
|
51
|
+
const versions = Object.keys(config.versions).map(Number);
|
|
52
|
+
if (!versions.every((version) => Number.isInteger(version))) throw new InternalError({ detail: `${FIELD_KEYS_BINDING}: a version key is not a stringified integer` });
|
|
53
|
+
return versions.sort((left, right) => right - left);
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { FIELD_KEYS_BINDING, FIELD_KEY_ROTATION_DAYS, liveVersions, resolveFieldKeys };
|
package/dist/data/codecs.d.ts
CHANGED
|
@@ -8,17 +8,29 @@ export declare const SQLiteDate: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.
|
|
|
8
8
|
/** JsonDate: Date ↔ ISO string, for dates nested inside `sqliteJson` payloads. */
|
|
9
9
|
export declare const JsonDate: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
|
|
10
10
|
/**
|
|
11
|
-
* sqliteJson: T ↔ JSON string, validated by `schema` on both sides. Decode
|
|
12
|
-
*
|
|
13
|
-
* against `schema` before it reaches the app.
|
|
14
|
-
*
|
|
15
|
-
* **Both `JSON` functions throw**, and both were reachable. `JSON.parse` throws a
|
|
16
|
-
* `SyntaxError` on any string that is not a JSON document — which a column holding
|
|
17
|
-
* text an older writer put there, or a payload a customer's Worker returned, can be.
|
|
18
|
-
* `JSON.stringify` throws a `TypeError` on a `BigInt` and on a cycle, either of
|
|
19
|
-
* which a `schema` can legitimately admit. Both become an issue (#358).
|
|
11
|
+
* sqliteJson: T ↔ JSON string, validated by `schema` on both sides. Decode accepts a JSON string or an
|
|
12
|
+
* already-parsed value; the result is validated against `schema` before it reaches the app.
|
|
20
13
|
*/
|
|
21
|
-
export declare function sqliteJson<T extends z.ZodType>(schema: T):
|
|
14
|
+
export declare function sqliteJson<T extends z.ZodType>(schema: T): SqliteJson<T>;
|
|
15
|
+
/**
|
|
16
|
+
* Names the type `sqliteJson` returns, and nothing else. A type only: no value carries it. `Redacted<T>` reads a JSON
|
|
17
|
+
* column's fields through it, as `redact` reads them through {@link isSqliteJson}, so an adopter's codec of the same
|
|
18
|
+
* shape is opaque to both.
|
|
19
|
+
*/
|
|
20
|
+
declare const kitJsonColumn: unique symbol;
|
|
21
|
+
/** What `sqliteJson(schema)` returns: the kit's JSON column over `schema`. Clones keep it, as they return `this`. */
|
|
22
|
+
export type SqliteJson<T extends z.ZodType> = z.ZodCodec<z.ZodUnion<[z.ZodString, T]>, T> & {
|
|
23
|
+
readonly [kitJsonColumn]: T;
|
|
24
|
+
};
|
|
25
|
+
/** The schema a kit JSON column decodes to, or `never` for any other node. */
|
|
26
|
+
export type SqliteJsonSchema<S> = S extends {
|
|
27
|
+
readonly [kitJsonColumn]: infer T;
|
|
28
|
+
} ? T : never;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a schema is a JSON column `sqliteJson` built, or a clone of one (`.describe()`, `.meta()`), read by its
|
|
31
|
+
* decode. `redact` walks such a column's fields one by one, and treats every other codec as opaque.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isSqliteJson(schema: z.ZodType): boolean;
|
|
22
34
|
export declare const MAX_TIMEZONE_LENGTH = 64;
|
|
23
35
|
/**
|
|
24
36
|
* Trim, validate, and canonicalize an IANA timezone; anything invalid becomes
|
|
@@ -33,4 +45,5 @@ export declare function isValidIanaTimezone(value: string): boolean;
|
|
|
33
45
|
* to `undefined` on both decode and encode rather than throwing.
|
|
34
46
|
*/
|
|
35
47
|
export declare const IanaTimezone: z.ZodCodec<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodOptional<z.ZodString>>;
|
|
48
|
+
export {};
|
|
36
49
|
//# sourceMappingURL=codecs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codecs.d.ts","sourceRoot":"","sources":["../../src/data/codecs.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA+CxB,+EAA+E;AAC/E,eAAO,MAAM,aAAa,8GAGxB,CAAC;AAoBH,6EAA6E;AAC7E,eAAO,MAAM,UAAU,mFAGrB,CAAC;AAEH,kFAAkF;AAClF,eAAO,MAAM,QAAQ,mFAGnB,CAAC;
|
|
1
|
+
{"version":3,"file":"codecs.d.ts","sourceRoot":"","sources":["../../src/data/codecs.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA+CxB,+EAA+E;AAC/E,eAAO,MAAM,aAAa,8GAGxB,CAAC;AAoBH,6EAA6E;AAC7E,eAAO,MAAM,UAAU,mFAGrB,CAAC;AAEH,kFAAkF;AAClF,eAAO,MAAM,QAAQ,mFAGnB,CAAC;AA0CH;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAKxE;AAED;;;;GAIG;AACH,OAAO,CAAC,MAAM,aAAa,EAAE,OAAO,MAAM,CAAC;AAE3C,qHAAqH;AACrH,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IAC1F,QAAQ,CAAC,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;CAC7B,CAAC;AAEF,8EAA8E;AAC9E,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,QAAQ,CAAC,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,KAAK,CAAC;AAE9F;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,GAAG,OAAO,CAGvD;AAED,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CASxE;AAED,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,mFAGvB,CAAC"}
|
package/dist/data/codecs.js
CHANGED
|
@@ -64,47 +64,61 @@ const JsonDate = z.codec(z.union([
|
|
|
64
64
|
encode: (value) => value.toISOString()
|
|
65
65
|
});
|
|
66
66
|
/**
|
|
67
|
-
* sqliteJson:
|
|
68
|
-
*
|
|
69
|
-
* against `schema` before it reaches the app.
|
|
67
|
+
* sqliteJson's decode: a JSON string is parsed, an already-parsed value passes through. One function for every
|
|
68
|
+
* column, so `isSqliteJson` can tell the kit's codec from any other by its decode.
|
|
70
69
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
* **`JSON.parse` throws**, and it was reachable: a column holding text an older writer put there, or a payload a
|
|
71
|
+
* customer's Worker returned, need not be a JSON document. That becomes an issue (#358).
|
|
72
|
+
*/
|
|
73
|
+
function decodeJson(value, payload) {
|
|
74
|
+
if (typeof value !== "string") return value;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(value);
|
|
77
|
+
} catch {
|
|
78
|
+
payload.issues.push({
|
|
79
|
+
code: "invalid_format",
|
|
80
|
+
format: "json",
|
|
81
|
+
input: value,
|
|
82
|
+
message: "Not valid JSON."
|
|
83
|
+
});
|
|
84
|
+
return z.NEVER;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* sqliteJson's encode. **`JSON.stringify` throws** on a `BigInt` and on a cycle, either of which a schema can
|
|
89
|
+
* legitimately admit, so that becomes an issue too (#358).
|
|
90
|
+
*/
|
|
91
|
+
function encodeJson(value, payload) {
|
|
92
|
+
try {
|
|
93
|
+
const json = JSON.stringify(value);
|
|
94
|
+
if (typeof json === "string") return json;
|
|
95
|
+
} catch {}
|
|
96
|
+
payload.issues.push({
|
|
97
|
+
code: "invalid_format",
|
|
98
|
+
format: "json",
|
|
99
|
+
input: void 0,
|
|
100
|
+
message: "Not serializable as JSON."
|
|
101
|
+
});
|
|
102
|
+
return z.NEVER;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* sqliteJson: T ↔ JSON string, validated by `schema` on both sides. Decode accepts a JSON string or an
|
|
106
|
+
* already-parsed value; the result is validated against `schema` before it reaches the app.
|
|
76
107
|
*/
|
|
77
108
|
function sqliteJson(schema) {
|
|
78
109
|
return z.codec(z.union([z.string(), schema]), schema, {
|
|
79
|
-
decode:
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
return JSON.parse(value);
|
|
83
|
-
} catch {
|
|
84
|
-
payload.issues.push({
|
|
85
|
-
code: "invalid_format",
|
|
86
|
-
format: "json",
|
|
87
|
-
input: value,
|
|
88
|
-
message: "Not valid JSON."
|
|
89
|
-
});
|
|
90
|
-
return z.NEVER;
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
encode: (value, payload) => {
|
|
94
|
-
try {
|
|
95
|
-
const json = JSON.stringify(value);
|
|
96
|
-
if (typeof json === "string") return json;
|
|
97
|
-
} catch {}
|
|
98
|
-
payload.issues.push({
|
|
99
|
-
code: "invalid_format",
|
|
100
|
-
format: "json",
|
|
101
|
-
input: void 0,
|
|
102
|
-
message: "Not serializable as JSON."
|
|
103
|
-
});
|
|
104
|
-
return z.NEVER;
|
|
105
|
-
}
|
|
110
|
+
decode: decodeJson,
|
|
111
|
+
encode: encodeJson
|
|
106
112
|
});
|
|
107
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Whether a schema is a JSON column `sqliteJson` built, or a clone of one (`.describe()`, `.meta()`), read by its
|
|
116
|
+
* decode. `redact` walks such a column's fields one by one, and treats every other codec as opaque.
|
|
117
|
+
*/
|
|
118
|
+
function isSqliteJson(schema) {
|
|
119
|
+
const def = schema._zod.def;
|
|
120
|
+
return def.type === "pipe" && def.transform === decodeJson;
|
|
121
|
+
}
|
|
108
122
|
const MAX_TIMEZONE_LENGTH = 64;
|
|
109
123
|
/**
|
|
110
124
|
* Trim, validate, and canonicalize an IANA timezone; anything invalid becomes
|
|
@@ -134,4 +148,4 @@ const IanaTimezone = z.codec(z.string().nullish(), z.string().optional(), {
|
|
|
134
148
|
encode: (value) => normalizeIanaTimezone(value)
|
|
135
149
|
});
|
|
136
150
|
//#endregion
|
|
137
|
-
export { IanaTimezone, JsonDate, MAX_TIMEZONE_LENGTH, SQLiteBoolean, SQLiteDate, isValidIanaTimezone, normalizeIanaTimezone, sqliteJson };
|
|
151
|
+
export { IanaTimezone, JsonDate, MAX_TIMEZONE_LENGTH, SQLiteBoolean, SQLiteDate, isSqliteJson, isValidIanaTimezone, normalizeIanaTimezone, sqliteJson };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { Context } from "hono";
|
|
4
|
+
import type { PithyHonoEnv } from "../capability/capability";
|
|
5
|
+
import type { MeterLimit } from "./entitlement";
|
|
6
|
+
/** How many of `name` the caller's holder may have: a whole number, or `null` for unlimited. */
|
|
7
|
+
export declare function quota(c: Context<PithyHonoEnv>, name: string): Promise<number | null>;
|
|
8
|
+
/** Whether `name` is on for the caller's holder. Always a boolean. */
|
|
9
|
+
export declare function feature(c: Context<PithyHonoEnv>, name: string): Promise<boolean>;
|
|
10
|
+
/**
|
|
11
|
+
* The ceiling a capability enforces on a meter it counts itself, for the current caller.
|
|
12
|
+
*
|
|
13
|
+
* **No provider, or one that binds nothing to this meter, is `{ enforced: false }`** — and the capability
|
|
14
|
+
* keeps its own configured ceiling. Enforcement is opt-in per meter because a cap that arrives by surprise
|
|
15
|
+
* breaks an adopter who was already over it.
|
|
16
|
+
*
|
|
17
|
+
* **A provider that throws refuses, and logs.** Storage already says which way round to err: over-counting
|
|
18
|
+
* briefly and reclaiming is the right way round, and under-counting has no recovery. Treating a failed read
|
|
19
|
+
* as unenforced would hand a customer an unbounded allowance for as long as the read kept failing. The cost
|
|
20
|
+
* is real and is stated rather than discovered: our bug becomes a refused write for a paying customer.
|
|
21
|
+
* Enforcement being opt-in is what bounds it — an adopter who would rather over-serve leaves the meter
|
|
22
|
+
* advisory.
|
|
23
|
+
*/
|
|
24
|
+
export declare function meterLimit(c: Context<PithyHonoEnv>, meter: string): Promise<MeterLimit>;
|
|
25
|
+
//# sourceMappingURL=allowance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowance.d.ts","sourceRoot":"","sources":["../../src/entitlement/allowance.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAqChD,gGAAgG;AAChG,wBAAsB,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAI1F;AAED,sEAAsE;AACtE,wBAAsB,OAAO,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAItF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAa7F"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { InternalError, messageOf } from "../error/pithyError.js";
|
|
4
|
+
//#region src/entitlement/allowance.ts
|
|
5
|
+
/**
|
|
6
|
+
* The allowance readers — how much the current caller may have, and whether a switch is on.
|
|
7
|
+
*
|
|
8
|
+
* const seats = await quota(c, "seats"); // number, or null for unlimited
|
|
9
|
+
* if (seats !== null && (await countMembers(orgId)) >= seats) throw new SeatLimitReached(…);
|
|
10
|
+
* if (await feature(c, "auditExport")) { … } // boolean, never null
|
|
11
|
+
*
|
|
12
|
+
* **Neither takes an entitlement key, and that is the point.** A route does not know which tier the
|
|
13
|
+
* caller's holder is on, and during an upgrade overlap it holds two. The provider resolves across every
|
|
14
|
+
* key the holder has and answers the higher, so an adopter compares numbers and never ranks tiers.
|
|
15
|
+
*
|
|
16
|
+
* **Read, never enforced.** A quota denies nothing on its own and a feature set to `false` denies
|
|
17
|
+
* nothing either — `requireEntitlement` still gates on the key alone. What a number means for a request
|
|
18
|
+
* is the adopter's code to decide, because only the adopter can count what it owns.
|
|
19
|
+
*
|
|
20
|
+
* **They live in core for the reason `requireEntitlement` does.** A reader that arrived with
|
|
21
|
+
* `@pithy-sh/payments` would be absent when payments is, and a route reading an allowance must then fail
|
|
22
|
+
* rather than read something. So with no provider composed — or one that declares no allowances — both
|
|
23
|
+
* throw `core/internal` naming the gap. Never `0`, which is a paywall shut for a reason nobody can see;
|
|
24
|
+
* never unlimited, which is a paywall that does not exist.
|
|
25
|
+
*/
|
|
26
|
+
/** The refusal when nothing on the request can answer an allowance. The adopter's wiring, so `core/internal`. */
|
|
27
|
+
function noProvider(c, kind, name) {
|
|
28
|
+
const provider = c.var.entitlements?.provider ?? null;
|
|
29
|
+
return new InternalError({
|
|
30
|
+
message: `No ${kind} "${name}" can be resolved here.`,
|
|
31
|
+
action: "Compose a payments capability that declares its entitlements in this Worker's pithy.config.ts.",
|
|
32
|
+
detail: provider === null ? `no entitlement provider is composed, so ${kind}("${name}") has nothing to read.` : `${provider} resolves entitlements and declares no allowances, so ${kind}("${name}") has nothing to read.`
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/** How many of `name` the caller's holder may have: a whole number, or `null` for unlimited. */
|
|
36
|
+
async function quota(c, name) {
|
|
37
|
+
const resolver = c.var.entitlements;
|
|
38
|
+
if (!resolver?.quota) throw noProvider(c, "quota", name);
|
|
39
|
+
return resolver.quota(name);
|
|
40
|
+
}
|
|
41
|
+
/** Whether `name` is on for the caller's holder. Always a boolean. */
|
|
42
|
+
async function feature(c, name) {
|
|
43
|
+
const resolver = c.var.entitlements;
|
|
44
|
+
if (!resolver?.feature) throw noProvider(c, "feature", name);
|
|
45
|
+
return resolver.feature(name);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The ceiling a capability enforces on a meter it counts itself, for the current caller.
|
|
49
|
+
*
|
|
50
|
+
* **No provider, or one that binds nothing to this meter, is `{ enforced: false }`** — and the capability
|
|
51
|
+
* keeps its own configured ceiling. Enforcement is opt-in per meter because a cap that arrives by surprise
|
|
52
|
+
* breaks an adopter who was already over it.
|
|
53
|
+
*
|
|
54
|
+
* **A provider that throws refuses, and logs.** Storage already says which way round to err: over-counting
|
|
55
|
+
* briefly and reclaiming is the right way round, and under-counting has no recovery. Treating a failed read
|
|
56
|
+
* as unenforced would hand a customer an unbounded allowance for as long as the read kept failing. The cost
|
|
57
|
+
* is real and is stated rather than discovered: our bug becomes a refused write for a paying customer.
|
|
58
|
+
* Enforcement being opt-in is what bounds it — an adopter who would rather over-serve leaves the meter
|
|
59
|
+
* advisory.
|
|
60
|
+
*/
|
|
61
|
+
async function meterLimit(c, meter) {
|
|
62
|
+
const resolver = c.var.entitlements;
|
|
63
|
+
if (!resolver?.meterLimit) return { enforced: false };
|
|
64
|
+
try {
|
|
65
|
+
return await resolver.meterLimit(meter);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
c.var.log?.error("meter limit resolution failed", {
|
|
68
|
+
provider: resolver.provider,
|
|
69
|
+
meter,
|
|
70
|
+
cause: messageOf(error)
|
|
71
|
+
});
|
|
72
|
+
throw new InternalError({
|
|
73
|
+
message: "Usage could not be checked, so nothing was written.",
|
|
74
|
+
action: "Read the preceding log line for why the allowance read failed.",
|
|
75
|
+
detail: `${resolver.provider} threw resolving the ${meter} meter's ceiling; refusing rather than serving unbounded.`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { feature, meterLimit, quota };
|
|
@@ -66,7 +66,48 @@ export interface EntitlementResolver {
|
|
|
66
66
|
* See {@link EntitlementHolder} for why this is a label and a tenant rather than the holder itself.
|
|
67
67
|
*/
|
|
68
68
|
holder?(): Promise<EntitlementHolder | undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* How many of `name` the current caller's holder may have — a whole number, or `null` for unlimited.
|
|
71
|
+
* Optional: a provider with no allowances omits it, and {@link import("./allowance").quota} refuses
|
|
72
|
+
* rather than inventing a number. A provider throws for a name it does not declare, never answers 0.
|
|
73
|
+
*/
|
|
74
|
+
quota?(name: string): Promise<number | null>;
|
|
75
|
+
/** Whether `name` is on for the current caller's holder. Never null. Optional, as {@link quota} is. */
|
|
76
|
+
feature?(name: string): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* The ceiling a capability that counts its own rows enforces for `meter` — storage's bytes, media's
|
|
79
|
+
* items — or `{ enforced: false }` where the provider binds no ceiling to it. Optional, and absent is
|
|
80
|
+
* `{ enforced: false }`: a capability composed with no provider keeps its own configured behavior.
|
|
81
|
+
*/
|
|
82
|
+
meterLimit?(meter: string): Promise<MeterLimit>;
|
|
69
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* What a provider says about one meter for the current caller: whether it binds a ceiling at all, and if
|
|
86
|
+
* it does, what the ceiling is and whose rows it covers. `limit: null` is unlimited — an enforced meter on
|
|
87
|
+
* a subject whose plan or override says so.
|
|
88
|
+
*
|
|
89
|
+
* Two states rather than a nullable number, because "this meter is advisory here" and "this subject is
|
|
90
|
+
* unlimited" are different facts and a capability must fall back to its own configured ceiling on the
|
|
91
|
+
* first and never on the second.
|
|
92
|
+
*
|
|
93
|
+
* **An enforced ceiling says which rows it is the whole of**, because a ceiling counted against the wrong
|
|
94
|
+
* rows is a different allowance. Under organization billing the plan's 10 GB is the company's: counted
|
|
95
|
+
* against one member's files it becomes 10 GB per member, and a paid quota is bypassed by an invitation.
|
|
96
|
+
* So `tenant` is the organization the ceiling was resolved for, exactly as {@link EntitlementHolder.tenant}
|
|
97
|
+
* is, and a capability sums every row it holds for that tenant, whoever wrote it. `null` means the holder
|
|
98
|
+
* is not a tenant: the ceiling is the caller's own, and the caller's rows are what count.
|
|
99
|
+
*
|
|
100
|
+
* It is a counting key, never a gate. A capability that would write a row for a different tenant than the
|
|
101
|
+
* one the ceiling covers refuses the write — that row would sit outside the sum, and under-counting has no
|
|
102
|
+
* recovery.
|
|
103
|
+
*/
|
|
104
|
+
export type MeterLimit = {
|
|
105
|
+
readonly enforced: false;
|
|
106
|
+
} | {
|
|
107
|
+
readonly enforced: true;
|
|
108
|
+
readonly limit: number | null;
|
|
109
|
+
readonly tenant: string | null;
|
|
110
|
+
};
|
|
70
111
|
/**
|
|
71
112
|
* How a resolver's holder appears in a denial's `detail` and on the denial's audit row — and **nothing a
|
|
72
113
|
* gate may compare**.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entitlement.d.ts","sourceRoot":"","sources":["../../src/entitlement/entitlement.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2BxB,eAAO,MAAM,cAAc,aAUxB,CAAC;AACJ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE7D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW;;;;;iBAqBuF,CAAC;AAChH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,WAAW,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAGpF;AAED,2GAA2G;AAC3G,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,EAAE,GAAG,EAAE,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAMnG;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,mBAAmB;IAClC,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,wGAAwG;IACxG,IAAI,IAAI,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAC;IACxC;;;;;OAKG;IACH,MAAM,CAAC,IAAI,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"entitlement.d.ts","sourceRoot":"","sources":["../../src/entitlement/entitlement.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2BxB,eAAO,MAAM,cAAc,aAUxB,CAAC;AACJ,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE7D;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW;;;;;iBAqBuF,CAAC;AAChH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,WAAW,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAGpF;AAED,2GAA2G;AAC3G,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,EAAE,GAAG,EAAE,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAMnG;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,mBAAmB;IAClC,yFAAyF;IACzF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,wGAAwG;IACxG,IAAI,IAAI,OAAO,CAAC,SAAS,WAAW,EAAE,CAAC,CAAC;IACxC;;;;;OAKG;IACH,MAAM,CAAC,IAAI,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IAClD;;;;OAIG;IACH,KAAK,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,uGAAuG;IACvG,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC;;;;OAIG;IACH,UAAU,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACjD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,UAAU,GAClB;IAAE,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;CAAE,GAC5B;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,iBAAiB;IAChC,4FAA4F;IAC5F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,sGAAsG;IACtG,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,EAAE,mBAGnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { Capability } from "../capability/capability";
|
|
4
|
+
import type { Logger } from "../logger/logger";
|
|
5
|
+
/**
|
|
6
|
+
* The usage-meter seam — how a capability that writes the rows a meter counts asks whether one more is
|
|
7
|
+
* allowed, when there is no request to ask through.
|
|
8
|
+
*
|
|
9
|
+
* `@pithy-sh/email` enqueues from a route, from a Workflow step and from a cron, and only the first has a
|
|
10
|
+
* `c.var.entitlements`. So the meter it charges is found the way the ledger is found — among the composed
|
|
11
|
+
* capabilities, by a structural predicate — and it is handed the holder explicitly. Nothing here imports the
|
|
12
|
+
* provider: an email capability composed with no payments finds no meter and enqueues as it always did.
|
|
13
|
+
*
|
|
14
|
+
* **A meter refuses; it never bills.** `charge` either records the use or throws the provider's own refusal
|
|
15
|
+
* (`payments/quota_exceeded`, 402). What it counted is never reported to a store — the kit does not sell
|
|
16
|
+
* overage — so this seam has no way to read a count back out.
|
|
17
|
+
*/
|
|
18
|
+
/** Who a charge is against: the holder pair, exactly as the provider keys it. */
|
|
19
|
+
export interface MeterSubject {
|
|
20
|
+
/** The kind of holder — `user` or `organization` under `@pithy-sh/payments`. */
|
|
21
|
+
readonly subjectType: string;
|
|
22
|
+
/** The holder's id. */
|
|
23
|
+
readonly subjectId: string;
|
|
24
|
+
}
|
|
25
|
+
/** One charge against one meter. */
|
|
26
|
+
export interface MeterCharge {
|
|
27
|
+
/** Who uses it. */
|
|
28
|
+
readonly subject: MeterSubject;
|
|
29
|
+
/** Which meter — `emails`. */
|
|
30
|
+
readonly meter: string;
|
|
31
|
+
/** How many. A whole number from 1. */
|
|
32
|
+
readonly amount: number;
|
|
33
|
+
/** The clock. Defaults to now. */
|
|
34
|
+
readonly now?: Date;
|
|
35
|
+
/** Where a refused or failed charge is logged. Optional; the provider logs through its own when absent. */
|
|
36
|
+
readonly log?: Logger;
|
|
37
|
+
}
|
|
38
|
+
/** The meter a provider exposes. */
|
|
39
|
+
export interface UsageMeter {
|
|
40
|
+
/** The capability answering — `payments`. */
|
|
41
|
+
readonly provider: string;
|
|
42
|
+
/**
|
|
43
|
+
* Record `amount` of `meter` against `subject`, or refuse. An advisory meter records nothing and refuses
|
|
44
|
+
* nothing. **A charge that cannot be checked refuses**, because under-counting has no recovery.
|
|
45
|
+
*/
|
|
46
|
+
charge(env: Record<string, unknown>, charge: MeterCharge): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** A composed capability that provides a meter. */
|
|
49
|
+
export interface UsageMeterProvider extends Capability {
|
|
50
|
+
/** The meter. */
|
|
51
|
+
readonly usageMeter: UsageMeter;
|
|
52
|
+
}
|
|
53
|
+
/** Whether a composed capability provides a usage meter. Structural, so no provider package is imported. */
|
|
54
|
+
export declare function isUsageMeterProvider(capability: Capability): capability is UsageMeterProvider;
|
|
55
|
+
/** The composed usage meter, or undefined when nothing composed provides one. */
|
|
56
|
+
export declare function composedUsageMeter(capabilities: readonly Capability[]): UsageMeter | undefined;
|
|
57
|
+
//# sourceMappingURL=meter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meter.d.ts","sourceRoot":"","sources":["../../src/entitlement/meter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;;;;;;;;;;GAYG;AAEH,iFAAiF;AACjF,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,uBAAuB;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,oCAAoC;AACpC,MAAM,WAAW,WAAW;IAC1B,mBAAmB;IACnB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAC/B,8BAA8B;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uCAAuC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kCAAkC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC;IACpB,2GAA2G;IAC3G,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,oCAAoC;AACpC,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E;AAED,mDAAmD;AACnD,MAAM,WAAW,kBAAmB,SAAQ,UAAU;IACpD,iBAAiB;IACjB,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;CACjC;AAED,4GAA4G;AAC5G,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,IAAI,kBAAkB,CAG7F;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,SAAS,UAAU,EAAE,GAAG,UAAU,GAAG,SAAS,CAE9F"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
//#region src/entitlement/meter.ts
|
|
4
|
+
/** Whether a composed capability provides a usage meter. Structural, so no provider package is imported. */
|
|
5
|
+
function isUsageMeterProvider(capability) {
|
|
6
|
+
return typeof capability.usageMeter?.charge === "function";
|
|
7
|
+
}
|
|
8
|
+
/** The composed usage meter, or undefined when nothing composed provides one. */
|
|
9
|
+
function composedUsageMeter(capabilities) {
|
|
10
|
+
return capabilities.find(isUsageMeterProvider)?.usageMeter;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { composedUsageMeter, isUsageMeterProvider };
|
package/dist/error/extend.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import type { MessageParams } from "../i18n/catalog";
|
|
4
|
+
import type { UnmarkedString } from "../privacy/types";
|
|
4
5
|
import type { ErrorPayload, KitErrorDomain } from "./payload";
|
|
5
6
|
import { ExtendedErrorPayload } from "./payload";
|
|
6
7
|
/**
|
|
@@ -84,11 +85,11 @@ export declare function defineErrorPayload<const Code extends `${string}/${strin
|
|
|
84
85
|
/** The HTTP status this code maps to. A 4xx or a 5xx — an error is not a success. */
|
|
85
86
|
status: number;
|
|
86
87
|
/** Public, safe-to-expose summary. Sent to clients and shown in the terminal. */
|
|
87
|
-
message:
|
|
88
|
-
/** Operator remediation hint. Becomes the CLI action line. Never sent to a client. */
|
|
89
|
-
action?:
|
|
90
|
-
/** Internal context for logs + audit. NEVER serialized to clients — the HTTP codec strips it. */
|
|
91
|
-
detail?:
|
|
88
|
+
message: UnmarkedString;
|
|
89
|
+
/** Operator remediation hint. Becomes the CLI action line. Never sent to a client. Never a marked value (#132). */
|
|
90
|
+
action?: UnmarkedString;
|
|
91
|
+
/** Internal context for logs + audit. NEVER serialized to clients — the HTTP codec strips it. Never a marked value (#132). */
|
|
92
|
+
detail?: UnmarkedString;
|
|
92
93
|
/**
|
|
93
94
|
* Values a translating client interpolates into its own wording for this code. Client-facing:
|
|
94
95
|
* these cross the boundary with `message`, unlike `action` and `detail`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extend.d.ts","sourceRoot":"","sources":["../../src/error/extend.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAMjD;;;;GAIG;AACH,KAAK,cAAc,CAAC,IAAI,SAAS,MAAM,IAAI,IAAI,SAAS,GAAG,cAAc,IAAI,MAAM,EAAE,GACjF;IAAE,IAAI,EAAE,2EAA2E,CAAA;CAAE,GACrF,OAAO,CAAC;AAEZ;;;;;GAKG;AACH,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACrG,oBAAoB,GAAG;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GACrC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC;AAE1C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,IAAI,SAAS,GAAG,MAAM,IAAI,MAAM,EAAE,EAClE,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,IAAI,GACT,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,CAEjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,IAAI,SAAS,GAAG,MAAM,IAAI,MAAM,EAAE,EACzE,OAAO,EAAE;IACP,uGAAuG;IACvG,IAAI,EAAE,IAAI,CAAC;IACX,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"extend.d.ts","sourceRoot":"","sources":["../../src/error/extend.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AAMjD;;;;GAIG;AACH,KAAK,cAAc,CAAC,IAAI,SAAS,MAAM,IAAI,IAAI,SAAS,GAAG,cAAc,IAAI,MAAM,EAAE,GACjF;IAAE,IAAI,EAAE,2EAA2E,CAAA;CAAE,GACrF,OAAO,CAAC;AAEZ;;;;;GAKG;AACH,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACrG,oBAAoB,GAAG;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,GACrC,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC,CAAC;AAE1C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,KAAK,CAAC,IAAI,SAAS,GAAG,MAAM,IAAI,MAAM,EAAE,EAClE,OAAO,EAAE,YAAY,EACrB,IAAI,EAAE,IAAI,GACT,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,CAEjC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,IAAI,SAAS,GAAG,MAAM,IAAI,MAAM,EAAE,EACzE,OAAO,EAAE;IACP,uGAAuG;IACvG,IAAI,EAAE,IAAI,CAAC;IACX,qFAAqF;IACrF,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,OAAO,EAAE,cAAc,CAAC;IACxB,mHAAmH;IACnH,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,8HAA8H;IAC9H,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,GAAG,cAAc,CAAC,IAAI,CAAC,GACvB,cAAc,CAAC,IAAI,CAAC,CAiBtB"}
|