@pithy-sh/core 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/audit/composed.d.ts +18 -0
  2. package/dist/audit/composed.d.ts.map +1 -0
  3. package/dist/audit/composed.js +28 -0
  4. package/dist/audit/recorder.d.ts +10 -2
  5. package/dist/audit/recorder.d.ts.map +1 -1
  6. package/dist/capability/capability.d.ts +23 -0
  7. package/dist/capability/capability.d.ts.map +1 -1
  8. package/dist/capability/capability.js +2 -0
  9. package/dist/capability/purge.d.ts +122 -0
  10. package/dist/capability/purge.d.ts.map +1 -0
  11. package/dist/capability/purge.js +131 -0
  12. package/dist/cli/commandCatalog.generated.d.ts +54 -2
  13. package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
  14. package/dist/cli/commandCatalog.generated.js +61 -0
  15. package/dist/cli/commandFacts.d.ts +1 -0
  16. package/dist/cli/commandFacts.d.ts.map +1 -1
  17. package/dist/cli/commandFacts.js +1 -0
  18. package/dist/controlPlane/capability.d.ts.map +1 -1
  19. package/dist/controlPlane/capability.js +7 -0
  20. package/dist/controlPlane/data/connection.d.ts +2 -12
  21. package/dist/controlPlane/data/connection.d.ts.map +1 -1
  22. package/dist/controlPlane/data/connection.js +1 -1
  23. package/dist/controlPlane/discovery/adminRoute.d.ts +29 -0
  24. package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -1
  25. package/dist/controlPlane/discovery/adminRoute.js +3 -0
  26. package/dist/controlPlane/error/errors.d.ts +4 -3
  27. package/dist/controlPlane/error/errors.d.ts.map +1 -1
  28. package/dist/controlPlane/http/guard.d.ts.map +1 -1
  29. package/dist/controlPlane/http/guard.js +40 -1
  30. package/dist/controlPlane/http/handlers.d.ts +79 -0
  31. package/dist/controlPlane/http/handlers.d.ts.map +1 -1
  32. package/dist/controlPlane/http/handlers.js +37 -3
  33. package/dist/controlPlane/http/responses.d.ts +98 -0
  34. package/dist/controlPlane/http/responses.d.ts.map +1 -1
  35. package/dist/controlPlane/http/responses.js +41 -1
  36. package/dist/controlPlane/http/routes.d.ts +1 -0
  37. package/dist/controlPlane/http/routes.d.ts.map +1 -1
  38. package/dist/controlPlane/http/routes.js +17 -3
  39. package/dist/controlPlane/scope/scope.d.ts +13 -3
  40. package/dist/controlPlane/scope/scope.d.ts.map +1 -1
  41. package/dist/controlPlane/scope/scope.js +14 -4
  42. package/dist/createBackend.d.ts.map +1 -1
  43. package/dist/createBackend.js +2 -0
  44. package/dist/createEntrypoint.d.ts +7 -1
  45. package/dist/createEntrypoint.d.ts.map +1 -1
  46. package/dist/createEntrypoint.js +15 -5
  47. package/dist/crypto/envelope.d.ts +86 -0
  48. package/dist/crypto/envelope.d.ts.map +1 -0
  49. package/dist/crypto/envelope.js +129 -0
  50. package/dist/crypto/fieldKeys.d.ts +41 -0
  51. package/dist/crypto/fieldKeys.d.ts.map +1 -0
  52. package/dist/crypto/fieldKeys.js +56 -0
  53. package/dist/data/codecs.d.ts +23 -10
  54. package/dist/data/codecs.d.ts.map +1 -1
  55. package/dist/data/codecs.js +50 -36
  56. package/dist/error/extend.d.ts +6 -5
  57. package/dist/error/extend.d.ts.map +1 -1
  58. package/dist/error/http.d.ts +72 -0
  59. package/dist/error/http.d.ts.map +1 -1
  60. package/dist/error/payload.d.ts +144 -0
  61. package/dist/error/payload.d.ts.map +1 -1
  62. package/dist/error/payload.js +48 -0
  63. package/dist/error/pithyError.d.ts +18 -7
  64. package/dist/error/pithyError.d.ts.map +1 -1
  65. package/dist/error/pithyError.js +6 -0
  66. package/dist/http/validation.d.ts +11 -2
  67. package/dist/http/validation.d.ts.map +1 -1
  68. package/dist/http/validation.js +20 -3
  69. package/dist/index.d.ts +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +2 -2
  72. package/dist/logger/logger.d.ts +12 -5
  73. package/dist/logger/logger.d.ts.map +1 -1
  74. package/dist/logger/record.d.ts +42 -0
  75. package/dist/logger/record.d.ts.map +1 -1
  76. package/dist/privacy/blindIndex.d.ts +27 -0
  77. package/dist/privacy/blindIndex.d.ts.map +1 -0
  78. package/dist/privacy/blindIndex.js +57 -0
  79. package/dist/privacy/classes.d.ts +142 -0
  80. package/dist/privacy/classes.d.ts.map +1 -0
  81. package/dist/privacy/classes.js +209 -0
  82. package/dist/privacy/classify.d.ts +336 -0
  83. package/dist/privacy/classify.d.ts.map +1 -0
  84. package/dist/privacy/classify.js +1024 -0
  85. package/dist/privacy/encrypted.d.ts +210 -0
  86. package/dist/privacy/encrypted.d.ts.map +1 -0
  87. package/dist/privacy/encrypted.js +350 -0
  88. package/dist/privacy/encryptedColumns.d.ts +64 -0
  89. package/dist/privacy/encryptedColumns.d.ts.map +1 -0
  90. package/dist/privacy/encryptedColumns.js +134 -0
  91. package/dist/privacy/fieldRotation.d.ts +34 -0
  92. package/dist/privacy/fieldRotation.d.ts.map +1 -0
  93. package/dist/privacy/fieldRotation.js +186 -0
  94. package/dist/privacy/inventory.d.ts +12 -0
  95. package/dist/privacy/inventory.d.ts.map +1 -0
  96. package/dist/privacy/inventory.js +154 -0
  97. package/dist/privacy/posture.d.ts +114 -0
  98. package/dist/privacy/posture.d.ts.map +1 -0
  99. package/dist/privacy/posture.js +136 -0
  100. package/dist/privacy/redact.d.ts +7 -0
  101. package/dist/privacy/redact.d.ts.map +1 -0
  102. package/dist/privacy/redact.js +404 -0
  103. package/dist/privacy/report.d.ts +58 -0
  104. package/dist/privacy/report.d.ts.map +1 -0
  105. package/dist/privacy/report.js +15 -0
  106. package/dist/privacy/retirement.d.ts +68 -0
  107. package/dist/privacy/retirement.d.ts.map +1 -0
  108. package/dist/privacy/retirement.js +72 -0
  109. package/dist/privacy/sealed.d.ts +87 -0
  110. package/dist/privacy/sealed.d.ts.map +1 -0
  111. package/dist/privacy/sealed.js +221 -0
  112. package/dist/privacy/types.d.ts +559 -0
  113. package/dist/privacy/types.d.ts.map +1 -0
  114. package/dist/privacy/types.js +2 -0
  115. package/dist/version.generated.d.ts +1 -1
  116. package/dist/version.generated.js +1 -1
  117. package/package.json +1 -1
  118. package/src/audit/composed.ts +30 -0
  119. package/src/audit/recorder.ts +10 -2
  120. package/src/capability/capability.ts +26 -0
  121. package/src/capability/purge.ts +187 -0
  122. package/src/cli/commandCatalog.generated.ts +65 -0
  123. package/src/cli/commandFacts.ts +1 -0
  124. package/src/controlPlane/capability.ts +11 -0
  125. package/src/controlPlane/discovery/adminRoute.ts +11 -0
  126. package/src/controlPlane/error/errors.ts +4 -3
  127. package/src/controlPlane/http/guard.ts +60 -1
  128. package/src/controlPlane/http/handlers.ts +57 -4
  129. package/src/controlPlane/http/responses.ts +100 -0
  130. package/src/controlPlane/http/routes.ts +31 -1
  131. package/src/controlPlane/scope/scope.ts +14 -3
  132. package/src/controlPlane/token/base64url.ts +1 -1
  133. package/src/createBackend.ts +4 -0
  134. package/src/createEntrypoint.ts +17 -2
  135. package/src/crypto/envelope.ts +196 -0
  136. package/src/crypto/fieldKeys.ts +76 -0
  137. package/src/data/codecs.ts +69 -37
  138. package/src/error/extend.ts +6 -5
  139. package/src/error/payload.ts +110 -0
  140. package/src/error/pithyError.ts +21 -7
  141. package/src/http/validation.ts +35 -3
  142. package/src/index.ts +1 -0
  143. package/src/logger/logger.ts +12 -5
  144. package/src/privacy/blindIndex.ts +81 -0
  145. package/src/privacy/classes.ts +277 -0
  146. package/src/privacy/classify.ts +1447 -0
  147. package/src/privacy/encrypted.ts +513 -0
  148. package/src/privacy/encryptedColumns.ts +185 -0
  149. package/src/privacy/fieldRotation.ts +234 -0
  150. package/src/privacy/inventory.ts +147 -0
  151. package/src/privacy/posture.ts +205 -0
  152. package/src/privacy/redact.ts +423 -0
  153. package/src/privacy/report.ts +64 -0
  154. package/src/privacy/retirement.ts +102 -0
  155. package/src/privacy/sealed.ts +323 -0
  156. package/src/privacy/types.ts +804 -0
  157. package/src/version.generated.ts +1 -1
@@ -107,6 +107,20 @@ export declare const LogRecord: z.ZodObject<{
107
107
  detail: z.ZodOptional<z.ZodString>;
108
108
  code: z.ZodLiteral<"auth/session_not_fresh">;
109
109
  status: z.ZodLiteral<403>;
110
+ }, z.core.$strip>, z.ZodObject<{
111
+ message: z.ZodString;
112
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
113
+ action: z.ZodOptional<z.ZodString>;
114
+ detail: z.ZodOptional<z.ZodString>;
115
+ code: z.ZodLiteral<"auth/account_suspended">;
116
+ status: z.ZodLiteral<403>;
117
+ }, z.core.$strip>, z.ZodObject<{
118
+ message: z.ZodString;
119
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
120
+ action: z.ZodOptional<z.ZodString>;
121
+ detail: z.ZodOptional<z.ZodString>;
122
+ code: z.ZodLiteral<"auth/account_deleted">;
123
+ status: z.ZodLiteral<403>;
110
124
  }, z.core.$strip>, z.ZodObject<{
111
125
  message: z.ZodString;
112
126
  params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
@@ -919,6 +933,34 @@ export declare const LogRecord: z.ZodObject<{
919
933
  detail: z.ZodOptional<z.ZodString>;
920
934
  code: z.ZodLiteral<"organization/nomination_invalid">;
921
935
  status: z.ZodLiteral<400>;
936
+ }, z.core.$strip>, z.ZodObject<{
937
+ message: z.ZodString;
938
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
939
+ action: z.ZodOptional<z.ZodString>;
940
+ detail: z.ZodOptional<z.ZodString>;
941
+ code: z.ZodLiteral<"organization/last_owner">;
942
+ status: z.ZodLiteral<409>;
943
+ }, z.core.$strip>, z.ZodObject<{
944
+ message: z.ZodString;
945
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
946
+ action: z.ZodOptional<z.ZodString>;
947
+ detail: z.ZodOptional<z.ZodString>;
948
+ code: z.ZodLiteral<"organization/already_member">;
949
+ status: z.ZodLiteral<409>;
950
+ }, z.core.$strip>, z.ZodObject<{
951
+ message: z.ZodString;
952
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
953
+ action: z.ZodOptional<z.ZodString>;
954
+ detail: z.ZodOptional<z.ZodString>;
955
+ code: z.ZodLiteral<"organization/seat_limit_reached">;
956
+ status: z.ZodLiteral<409>;
957
+ }, z.core.$strip>, z.ZodObject<{
958
+ message: z.ZodString;
959
+ params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
960
+ action: z.ZodOptional<z.ZodString>;
961
+ detail: z.ZodOptional<z.ZodString>;
962
+ code: z.ZodLiteral<"organization/organization_suspended">;
963
+ status: z.ZodLiteral<403>;
922
964
  }, z.core.$strip>, z.ZodObject<{
923
965
  message: z.ZodString;
924
966
  params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>>;
@@ -1 +1 @@
1
- {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../src/logger/record.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;GAKG;AAEH,4GAA4G;AAC5G,eAAO,MAAM,QAAQ;;;;;EAE4F,CAAC;AAClH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEjD;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAgD,CAAC;AAEtG;;;;GAIG;AACH,eAAO,MAAM,SAAS,wCAEsF,CAAC;AAC7G,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAW+E,CAAC;AACtG,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC;AAmBnD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAMzD"}
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../../src/logger/record.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;GAKG;AAEH,4GAA4G;AAC5G,eAAO,MAAM,QAAQ;;;;;EAE4F,CAAC;AAClH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEjD;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAgD,CAAC;AAEtG;;;;GAIG;AACH,eAAO,MAAM,SAAS,wCAEsF,CAAC;AAC7G,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAW+E,CAAC;AACtG,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC;AAmBnD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAMzD"}
@@ -0,0 +1,27 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { type EncryptionConfig } from "../crypto/envelope";
4
+ import { type BlindIndexNormalizer } from "./encrypted";
5
+ /**
6
+ * **A blind index: a keyed hash an encrypted column can be found by** (#390).
7
+ *
8
+ * An encrypted column cannot answer `WHERE email = ?`. A sibling column holding `HMAC(key, normalize(value))` can,
9
+ * for equality and nothing else. One rule here is shared by the app Worker that seals, `sealed().match` that looks
10
+ * up, and the rotator that re-indexes inside the manager, so the three cannot hash one value two ways.
11
+ *
12
+ * - **The key is derived, on the field key's own version axis.** Each version's index key is HKDF-SHA256 over that
13
+ * version's field key, with the label `pithy/blind-index/<normalizer>`. One secret, one rotation.
14
+ * - **The value carries its version**: `<version>:<hmac>`, so the stale count reaches an index exactly as it reaches
15
+ * an envelope, by reading the prefix.
16
+ * - **The normalizer is in the label**, so changing a field's normalizer cannot silently miss against old hashes: they
17
+ * are hashes under a different key, and the change reads as a re-index.
18
+ */
19
+ /** The HKDF label one normalizer's index keys are derived under. */
20
+ export declare function blindIndexLabel(normalizer: BlindIndexNormalizer): string;
21
+ /** The HMAC key for one version and normalizer: HKDF-SHA256 over that version's field key. */
22
+ export declare function blindIndexKey(config: EncryptionConfig, version: number, normalizer: BlindIndexNormalizer): Promise<CryptoKey>;
23
+ /** The blind index of `value` under `version`: `<version>:<hmac>`. The value is normalized first. */
24
+ export declare function blindIndexValue(config: EncryptionConfig, version: number, normalizer: BlindIndexNormalizer, value: string): Promise<string>;
25
+ /** The version a blind-index value was written under, or `null` for a value that is not one. */
26
+ export declare function blindIndexVersion(value: string): number | null;
27
+ //# sourceMappingURL=blindIndex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blindIndex.d.ts","sourceRoot":"","sources":["../../src/privacy/blindIndex.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,gBAAgB,EAAY,MAAM,oBAAoB,CAAC;AAErE,OAAO,EAAE,KAAK,oBAAoB,EAAqB,MAAM,aAAa,CAAC;AAE3E;;;;;;;;;;;;;GAaG;AAEH,oEAAoE;AACpE,wBAAgB,eAAe,CAAC,UAAU,EAAE,oBAAoB,GAAG,MAAM,CAExE;AASD,8FAA8F;AAC9F,wBAAsB,aAAa,CACjC,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,oBAAoB,GAC/B,OAAO,CAAC,SAAS,CAAC,CAmBpB;AAED,qGAAqG;AACrG,wBAAsB,eAAe,CACnC,MAAM,EAAE,gBAAgB,EACxB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,oBAAoB,EAChC,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED,gGAAgG;AAChG,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG9D"}
@@ -0,0 +1,57 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { keyBytes } from "../crypto/envelope.js";
4
+ import { FIELD_KEYS_BINDING } from "../crypto/fieldKeys.js";
5
+ import { normalizeForIndex } from "./encrypted.js";
6
+ //#region src/privacy/blindIndex.ts
7
+ /**
8
+ * **A blind index: a keyed hash an encrypted column can be found by** (#390).
9
+ *
10
+ * An encrypted column cannot answer `WHERE email = ?`. A sibling column holding `HMAC(key, normalize(value))` can,
11
+ * for equality and nothing else. One rule here is shared by the app Worker that seals, `sealed().match` that looks
12
+ * up, and the rotator that re-indexes inside the manager, so the three cannot hash one value two ways.
13
+ *
14
+ * - **The key is derived, on the field key's own version axis.** Each version's index key is HKDF-SHA256 over that
15
+ * version's field key, with the label `pithy/blind-index/<normalizer>`. One secret, one rotation.
16
+ * - **The value carries its version**: `<version>:<hmac>`, so the stale count reaches an index exactly as it reaches
17
+ * an envelope, by reading the prefix.
18
+ * - **The normalizer is in the label**, so changing a field's normalizer cannot silently miss against old hashes: they
19
+ * are hashes under a different key, and the change reads as a re-index.
20
+ */
21
+ /** The HKDF label one normalizer's index keys are derived under. */
22
+ function blindIndexLabel(normalizer) {
23
+ return `pithy/blind-index/${normalizer}`;
24
+ }
25
+ /** Unpadded base64url, the alphabet a blind index is written in. */
26
+ function base64Url(bytes) {
27
+ let binary = "";
28
+ for (const byte of bytes) binary += String.fromCharCode(byte);
29
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
30
+ }
31
+ /** The HMAC key for one version and normalizer: HKDF-SHA256 over that version's field key. */
32
+ async function blindIndexKey(config, version, normalizer) {
33
+ const material = await crypto.subtle.importKey("raw", keyBytes(config, version, { keys: FIELD_KEYS_BINDING }), "HKDF", false, ["deriveBits"]);
34
+ const bits = await crypto.subtle.deriveBits({
35
+ name: "HKDF",
36
+ hash: "SHA-256",
37
+ salt: /* @__PURE__ */ new Uint8Array(0),
38
+ info: Uint8Array.from(new TextEncoder().encode(blindIndexLabel(normalizer)))
39
+ }, material, 256);
40
+ return crypto.subtle.importKey("raw", bits, {
41
+ name: "HMAC",
42
+ hash: "SHA-256"
43
+ }, false, ["sign"]);
44
+ }
45
+ /** The blind index of `value` under `version`: `<version>:<hmac>`. The value is normalized first. */
46
+ async function blindIndexValue(config, version, normalizer, value) {
47
+ const key = await blindIndexKey(config, version, normalizer);
48
+ const mac = await crypto.subtle.sign("HMAC", key, Uint8Array.from(new TextEncoder().encode(normalizeForIndex(normalizer, value))));
49
+ return `${version}:${base64Url(new Uint8Array(mac))}`;
50
+ }
51
+ /** The version a blind-index value was written under, or `null` for a value that is not one. */
52
+ function blindIndexVersion(value) {
53
+ const match = /^(\d+):/.exec(value);
54
+ return match ? Number(match[1]) : null;
55
+ }
56
+ //#endregion
57
+ export { blindIndexKey, blindIndexLabel, blindIndexValue, blindIndexVersion };
@@ -0,0 +1,142 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { z } from "zod";
4
+ /**
5
+ * **A field declares its own privacy class, and the declaration drives everything downstream** (#132).
6
+ *
7
+ * One marker, several consumers — the shape `@pithy-sh/vector` already uses, where `.meta({ filterable: true
8
+ * })` drives provisioning, the filter type, the drift check and the boot check. Here the marker drives the
9
+ * type gate on the three internal surfaces (the logger, `AuditEvent.metadata`, `PithyError.detail`), the
10
+ * masked view `redact` returns, the classification `classifyPrivacy` derives, and the inventory the control
11
+ * plane reports.
12
+ *
13
+ * ## What a marker does, in one call
14
+ *
15
+ * `pii(schema)` does two things. It attaches `.meta({ privacy: "pii" })`, which is **introspectable** — that is
16
+ * what `redact`, `classifyPrivacy` and the inventory read. And it applies Zod's `.brand()`, which changes the
17
+ * **inferred type** — that is what makes a log call carrying the value fail to compile. The brand lands on the
18
+ * field, not on the object, so extracting the field does not launder it: `user.email` is as branded as `user`.
19
+ *
20
+ * **A marker over another marker adds its class.** `pii(secret(x))` and `secret(pii(x))` are the same field: it
21
+ * carries both classes, in its `meta` (as a list, strongest first) and in its type (both brands). It is masked with the
22
+ * strongest one's placeholder and listed once per class. The same marker twice is one class.
23
+ *
24
+ * **The brand key is `privacy:<class>`, not the bare class name.** An adopter's own `.brand<"pii">()` for some
25
+ * unrelated purpose must not trip a gate it never opted into, and an adopter's `UserId` brand must not either:
26
+ * the gate reads a brand under the `privacy:` namespace and nothing else, which is the federated-key rule
27
+ * (`auth/invalid_token`, `pithy_auth_users`) applied one more time.
28
+ *
29
+ * **Nothing at runtime changes.** Zod's `.brand()` returns the same schema object — the brand exists only in
30
+ * the type — and `.meta()` returns a clone that parses exactly as its source. `Schema.parse(row)` returns the
31
+ * same data it did before the marker, and no column is added anywhere.
32
+ *
33
+ * ## The set is open
34
+ *
35
+ * Four classes ship named — `secret`, `pii`, `phi`, `pci` — and {@link definePrivacyClass} registers more
36
+ * (`genetic`, `biometric`, whatever a regulator names) without a change to core. A closed union is what #117
37
+ * records the cost of, and a healthcare adopter should not have to wait on us.
38
+ *
39
+ * ## Where `secret` stops and `@pithy-sh/secrets` starts
40
+ *
41
+ * Two different facts, kept apart on purpose. `@pithy-sh/secrets` decides **where a credential the Worker reads
42
+ * is stored** — its registry names a backend, and its reader redacts the value it hands back. `secret()` says
43
+ * **what a field in a data shape is** — a receipt, a magic-link token, a session id, a key hash — values that
44
+ * live in rows and payloads and that no secrets registry ever holds. Merging them would make core depend on a
45
+ * capability (principle 4) and would make a storage decision answer a classification question.
46
+ */
47
+ /** The `meta` key a marker writes and every reader reads. */
48
+ export declare const PRIVACY_META_KEY = "privacy";
49
+ /** The prefix of every privacy brand, so the gate never reads a brand that means something else. */
50
+ export declare const PRIVACY_BRAND_PREFIX = "privacy:";
51
+ /** A class name: a lowercase identifier. It becomes a brand key, a config value and a report column. */
52
+ export declare const PrivacyClassName: z.ZodString;
53
+ export type PrivacyClassName = z.output<typeof PrivacyClassName>;
54
+ /** The four classes the kit names, in the order every report lists them. */
55
+ export declare const BUILT_IN_PRIVACY_CLASSES: readonly ["secret", "pii", "phi", "pci"];
56
+ /** One of the four built-in class names. */
57
+ export type BuiltInPrivacyClass = (typeof BUILT_IN_PRIVACY_CLASSES)[number];
58
+ /** How a class masks a value: the string that stands in its place on every internal surface. */
59
+ export type PrivacyMask = (value: unknown) => string;
60
+ /** One registered class: its name, what it means, and how it masks. */
61
+ export interface PrivacyClass {
62
+ /** The class name — the `meta` value and the brand's suffix. */
63
+ readonly name: string;
64
+ /** What the class means, in one sentence an auditor reads. */
65
+ readonly description: string;
66
+ /** The masked form of a value of this class. */
67
+ readonly mask: PrivacyMask;
68
+ }
69
+ /**
70
+ * The brand a marker puts on a schema's output type. Zod's own brand machinery, under the `privacy:` key.
71
+ *
72
+ * `"out"` on purpose: the **input** side stays unbranded, so `z.input` — the SQLite row shape, the Kysely
73
+ * table type, a KV write's pre-parse value — is exactly what it was. A row read from D1 parses into the
74
+ * branded app shape, and `Schema.encode()` hands Kysely the plain row back.
75
+ */
76
+ export type Marked<S extends z.ZodType, C extends string> = z.core.$ZodBranded<S, `${typeof PRIVACY_BRAND_PREFIX}${C}`, "out">;
77
+ /** A marker for one class: wraps a schema and returns it marked. Carries its class name for a reader. */
78
+ export interface PrivacyMarker<C extends string> {
79
+ <S extends z.ZodType>(schema: S): Marked<S, C>;
80
+ /** The class this marker applies. */
81
+ readonly className: C;
82
+ }
83
+ /**
84
+ * Mask a primary account number to its first six and last four digits — `424242******4242`.
85
+ *
86
+ * **The one class whose mask is not a placeholder, because the masked form is the useful one.** First-six and
87
+ * last-four is the display PCI-DSS permits, and it is what an operator reading a log needs to match a
88
+ * complaint to a card without holding the card. Spaces and dashes are separators and are dropped; anything
89
+ * that is not 13 to 19 digits once they are is not a card number, and masks to `[pci]` rather than to a
90
+ * partial reveal of something else.
91
+ *
92
+ * Every payment rail Pithy ships is hosted, so no PAN ever reaches an adopter's Worker through the kit. The
93
+ * class is here for the adopter's own schemas.
94
+ */
95
+ export declare function maskPan(value: unknown): string;
96
+ /** The class a name refers to, or `undefined` for a name nothing registered. */
97
+ export declare function privacyClass(name: string): PrivacyClass | undefined;
98
+ /** Every registered class, built-ins first in their fixed order, then each adopter class in registration order. */
99
+ export declare function privacyClasses(): PrivacyClass[];
100
+ /**
101
+ * `names` from the strongest to the weakest, each once: `secret`, then `phi`, then `pci`, then each adopter class, then
102
+ * `pii`. An adopter registers a class because its data is more specific than `pii`, so it ranks above it; adopter
103
+ * classes rank among themselves by name. The order a marker records several classes in, whatever order they came.
104
+ */
105
+ export declare function rankPrivacyClasses(names: Iterable<string>): string[];
106
+ /**
107
+ * The strongest of `names`, the class a value is masked with when it carries several and is masked whole. See
108
+ * {@link rankPrivacyClasses}. `undefined` for none.
109
+ */
110
+ export declare function strongestPrivacyClass(names: Iterable<string>): string | undefined;
111
+ /**
112
+ * The classes a schema's own `privacy` meta records: one class as its name, several as a list, strongest first. Read
113
+ * from this exact schema, never through a wrapper. Empty where it records none.
114
+ */
115
+ export declare function ownPrivacyClasses(schema: z.ZodType): string[];
116
+ /** What {@link definePrivacyClass} takes. */
117
+ export interface PrivacyClassSpec<C extends string> {
118
+ /** The class name: a lowercase identifier, unique across the isolate. */
119
+ name: C;
120
+ /** What the class means. Defaults to a sentence naming it as adopter-registered. */
121
+ description?: string;
122
+ /** How a value masks. Defaults to the `[name]` placeholder. */
123
+ mask?: PrivacyMask;
124
+ }
125
+ /**
126
+ * Register a class of the adopter's own — `genetic`, `biometric`, whatever their regulator names — and get its
127
+ * marker back. Usable exactly like the built-ins: the same `meta`, the same brand, the same gate, the same
128
+ * inventory row.
129
+ *
130
+ * **A duplicate is refused at registration**, a built-in name included. Two modules each believing they own
131
+ * `genetic` would disagree about its mask, and the one that loaded second would win silently.
132
+ */
133
+ export declare function definePrivacyClass<const C extends string>(spec: PrivacyClassSpec<C>): PrivacyMarker<C>;
134
+ /** Mark a field as a credential or bearer artifact. See {@link BUILT_INS}. */
135
+ export declare const secret: PrivacyMarker<"secret">;
136
+ /** Mark a field as personal data under GDPR or CCPA. */
137
+ export declare const pii: PrivacyMarker<"pii">;
138
+ /** Mark a field as Protected Health Information under HIPAA. */
139
+ export declare const phi: PrivacyMarker<"phi">;
140
+ /** Mark a field as cardholder data under PCI-DSS. Masks to first six and last four. */
141
+ export declare const pci: PrivacyMarker<"pci">;
142
+ //# sourceMappingURL=classes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classes.d.ts","sourceRoot":"","sources":["../../src/privacy/classes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,209 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { ConflictError, ValidationError } from "../error/pithyError.js";
4
+ import { z } from "zod";
5
+ //#region src/privacy/classes.ts
6
+ /**
7
+ * **A field declares its own privacy class, and the declaration drives everything downstream** (#132).
8
+ *
9
+ * One marker, several consumers — the shape `@pithy-sh/vector` already uses, where `.meta({ filterable: true
10
+ * })` drives provisioning, the filter type, the drift check and the boot check. Here the marker drives the
11
+ * type gate on the three internal surfaces (the logger, `AuditEvent.metadata`, `PithyError.detail`), the
12
+ * masked view `redact` returns, the classification `classifyPrivacy` derives, and the inventory the control
13
+ * plane reports.
14
+ *
15
+ * ## What a marker does, in one call
16
+ *
17
+ * `pii(schema)` does two things. It attaches `.meta({ privacy: "pii" })`, which is **introspectable** — that is
18
+ * what `redact`, `classifyPrivacy` and the inventory read. And it applies Zod's `.brand()`, which changes the
19
+ * **inferred type** — that is what makes a log call carrying the value fail to compile. The brand lands on the
20
+ * field, not on the object, so extracting the field does not launder it: `user.email` is as branded as `user`.
21
+ *
22
+ * **A marker over another marker adds its class.** `pii(secret(x))` and `secret(pii(x))` are the same field: it
23
+ * carries both classes, in its `meta` (as a list, strongest first) and in its type (both brands). It is masked with the
24
+ * strongest one's placeholder and listed once per class. The same marker twice is one class.
25
+ *
26
+ * **The brand key is `privacy:<class>`, not the bare class name.** An adopter's own `.brand<"pii">()` for some
27
+ * unrelated purpose must not trip a gate it never opted into, and an adopter's `UserId` brand must not either:
28
+ * the gate reads a brand under the `privacy:` namespace and nothing else, which is the federated-key rule
29
+ * (`auth/invalid_token`, `pithy_auth_users`) applied one more time.
30
+ *
31
+ * **Nothing at runtime changes.** Zod's `.brand()` returns the same schema object — the brand exists only in
32
+ * the type — and `.meta()` returns a clone that parses exactly as its source. `Schema.parse(row)` returns the
33
+ * same data it did before the marker, and no column is added anywhere.
34
+ *
35
+ * ## The set is open
36
+ *
37
+ * Four classes ship named — `secret`, `pii`, `phi`, `pci` — and {@link definePrivacyClass} registers more
38
+ * (`genetic`, `biometric`, whatever a regulator names) without a change to core. A closed union is what #117
39
+ * records the cost of, and a healthcare adopter should not have to wait on us.
40
+ *
41
+ * ## Where `secret` stops and `@pithy-sh/secrets` starts
42
+ *
43
+ * Two different facts, kept apart on purpose. `@pithy-sh/secrets` decides **where a credential the Worker reads
44
+ * is stored** — its registry names a backend, and its reader redacts the value it hands back. `secret()` says
45
+ * **what a field in a data shape is** — a receipt, a magic-link token, a session id, a key hash — values that
46
+ * live in rows and payloads and that no secrets registry ever holds. Merging them would make core depend on a
47
+ * capability (principle 4) and would make a storage decision answer a classification question.
48
+ */
49
+ /** The `meta` key a marker writes and every reader reads. */
50
+ const PRIVACY_META_KEY = "privacy";
51
+ /** The prefix of every privacy brand, so the gate never reads a brand that means something else. */
52
+ const PRIVACY_BRAND_PREFIX = "privacy:";
53
+ /** A class name: a lowercase identifier. It becomes a brand key, a config value and a report column. */
54
+ const PrivacyClassName = z.string().regex(/^[a-z][a-z0-9_]*$/, "A privacy class name is a lowercase identifier: letters, digits and underscores.").describe("A privacy class name — `secret`, `pii`, `phi`, `pci`, or one an adopter registered with `definePrivacyClass`. A lowercase identifier.");
55
+ /** The four classes the kit names, in the order every report lists them. */
56
+ const BUILT_IN_PRIVACY_CLASSES = [
57
+ "secret",
58
+ "pii",
59
+ "phi",
60
+ "pci"
61
+ ];
62
+ /** The placeholder every class but `pci` masks to: `[pii]`, `[phi]`, `[genetic]`. */
63
+ function placeholder(name) {
64
+ return () => `[${name}]`;
65
+ }
66
+ /** The shortest and longest primary account numbers ISO/IEC 7812 issues. */
67
+ const PAN_DIGITS = {
68
+ min: 13,
69
+ max: 19
70
+ };
71
+ /**
72
+ * Mask a primary account number to its first six and last four digits — `424242******4242`.
73
+ *
74
+ * **The one class whose mask is not a placeholder, because the masked form is the useful one.** First-six and
75
+ * last-four is the display PCI-DSS permits, and it is what an operator reading a log needs to match a
76
+ * complaint to a card without holding the card. Spaces and dashes are separators and are dropped; anything
77
+ * that is not 13 to 19 digits once they are is not a card number, and masks to `[pci]` rather than to a
78
+ * partial reveal of something else.
79
+ *
80
+ * Every payment rail Pithy ships is hosted, so no PAN ever reaches an adopter's Worker through the kit. The
81
+ * class is here for the adopter's own schemas.
82
+ */
83
+ function maskPan(value) {
84
+ if (typeof value !== "string") return "[pci]";
85
+ const digits = value.replace(/[\s-]/g, "");
86
+ if (!/^\d+$/.test(digits) || digits.length < PAN_DIGITS.min || digits.length > PAN_DIGITS.max) return "[pci]";
87
+ return `${digits.slice(0, 6)}${"*".repeat(digits.length - 10)}${digits.slice(-4)}`;
88
+ }
89
+ /** The built-ins, stated once. The descriptions are what `pithy privacy` and the inventory docs read. */
90
+ const BUILT_INS = {
91
+ secret: {
92
+ name: "secret",
93
+ description: "A credential or a bearer artifact — a session id, a magic-link token, a receipt, a key. The harm is impersonation, not exposure.",
94
+ mask: placeholder("secret")
95
+ },
96
+ pii: {
97
+ name: "pii",
98
+ description: "Personal data that identifies a person under GDPR or CCPA — an email address, a name, a phone number, a postal address.",
99
+ mask: placeholder("pii")
100
+ },
101
+ phi: {
102
+ name: "phi",
103
+ description: "Protected Health Information under HIPAA — a diagnosis, a treatment, a result, any health record tied to a person.",
104
+ mask: placeholder("phi")
105
+ },
106
+ pci: {
107
+ name: "pci",
108
+ description: "Cardholder data under PCI-DSS — a primary account number. Masks to its first six and last four digits.",
109
+ mask: maskPan
110
+ }
111
+ };
112
+ /**
113
+ * Every registered class, by name. Module state, deliberately: a class is declared once, at module load, the
114
+ * way a capability's error codes are — and an isolate holds one registry, so a class registered by the
115
+ * adopter's schema module is known to the logger, the inventory and `redact` in the same isolate.
116
+ */
117
+ const registry = new Map(Object.entries(BUILT_INS));
118
+ /** The class a name refers to, or `undefined` for a name nothing registered. */
119
+ function privacyClass(name) {
120
+ return registry.get(name);
121
+ }
122
+ /** Every registered class, built-ins first in their fixed order, then each adopter class in registration order. */
123
+ function privacyClasses() {
124
+ return [...registry.values()];
125
+ }
126
+ /** The built-ins from the most harmful disclosure to the least: a credential, then health, then card, then identity. */
127
+ const STRENGTH = [
128
+ "secret",
129
+ "phi",
130
+ "pci"
131
+ ];
132
+ /**
133
+ * `names` from the strongest to the weakest, each once: `secret`, then `phi`, then `pci`, then each adopter class, then
134
+ * `pii`. An adopter registers a class because its data is more specific than `pii`, so it ranks above it; adopter
135
+ * classes rank among themselves by name. The order a marker records several classes in, whatever order they came.
136
+ */
137
+ function rankPrivacyClasses(names) {
138
+ const all = [...new Set(names)];
139
+ const builtIn = STRENGTH.filter((name) => all.includes(name));
140
+ const adopter = all.filter((name) => name !== "pii" && !STRENGTH.includes(name)).sort();
141
+ return [
142
+ ...builtIn,
143
+ ...adopter,
144
+ ...all.includes("pii") ? ["pii"] : []
145
+ ];
146
+ }
147
+ /**
148
+ * The strongest of `names`, the class a value is masked with when it carries several and is masked whole. See
149
+ * {@link rankPrivacyClasses}. `undefined` for none.
150
+ */
151
+ function strongestPrivacyClass(names) {
152
+ return rankPrivacyClasses(names)[0];
153
+ }
154
+ /**
155
+ * The classes a schema's own `privacy` meta records: one class as its name, several as a list, strongest first. Read
156
+ * from this exact schema, never through a wrapper. Empty where it records none.
157
+ */
158
+ function ownPrivacyClasses(schema) {
159
+ const value = schema.meta()?.[PRIVACY_META_KEY];
160
+ if (typeof value === "string") return [value];
161
+ if (Array.isArray(value)) return rankPrivacyClasses(value.filter((one) => typeof one === "string"));
162
+ return [];
163
+ }
164
+ /** Mark a schema with a class: the introspectable `meta`, and the type-level brand. */
165
+ function marker(name) {
166
+ const mark = (schema) => {
167
+ const classes = rankPrivacyClasses([...ownPrivacyClasses(schema), name]);
168
+ const recorded = classes.length === 1 ? name : classes;
169
+ return schema.meta({
170
+ ...schema.meta() ?? {},
171
+ [PRIVACY_META_KEY]: recorded
172
+ }).brand();
173
+ };
174
+ return Object.assign(mark, { className: name });
175
+ }
176
+ /**
177
+ * Register a class of the adopter's own — `genetic`, `biometric`, whatever their regulator names — and get its
178
+ * marker back. Usable exactly like the built-ins: the same `meta`, the same brand, the same gate, the same
179
+ * inventory row.
180
+ *
181
+ * **A duplicate is refused at registration**, a built-in name included. Two modules each believing they own
182
+ * `genetic` would disagree about its mask, and the one that loaded second would win silently.
183
+ */
184
+ function definePrivacyClass(spec) {
185
+ if (!PrivacyClassName.safeParse(spec.name).success) throw new ValidationError({
186
+ message: `"${spec.name}" is not a privacy class name.`,
187
+ action: "Name the class with a lowercase identifier — letters, digits and underscores, e.g. `genetic`."
188
+ });
189
+ if (registry.has(spec.name)) throw new ConflictError({
190
+ message: `The privacy class "${spec.name}" is already registered.`,
191
+ action: `Register "${spec.name}" once, in one module, and import its marker everywhere else.`
192
+ });
193
+ registry.set(spec.name, {
194
+ name: spec.name,
195
+ description: spec.description ?? `An adopter-registered privacy class, "${spec.name}".`,
196
+ mask: spec.mask ?? placeholder(spec.name)
197
+ });
198
+ return marker(spec.name);
199
+ }
200
+ /** Mark a field as a credential or bearer artifact. See {@link BUILT_INS}. */
201
+ const secret = marker("secret");
202
+ /** Mark a field as personal data under GDPR or CCPA. */
203
+ const pii = marker("pii");
204
+ /** Mark a field as Protected Health Information under HIPAA. */
205
+ const phi = marker("phi");
206
+ /** Mark a field as cardholder data under PCI-DSS. Masks to first six and last four. */
207
+ const pci = marker("pci");
208
+ //#endregion
209
+ export { BUILT_IN_PRIVACY_CLASSES, PRIVACY_BRAND_PREFIX, PRIVACY_META_KEY, PrivacyClassName, definePrivacyClass, maskPan, ownPrivacyClasses, pci, phi, pii, privacyClass, privacyClasses, rankPrivacyClasses, secret, strongestPrivacyClass };