@metamask-previews/kyc-controller 0.0.0-preview-823dcff → 0.0.0-preview-e57e5c3dc

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 (155) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +10 -2
  3. package/dist/KycController-method-action-types.cjs +7 -0
  4. package/dist/KycController-method-action-types.cjs.map +1 -0
  5. package/dist/KycController-method-action-types.d.cts +168 -0
  6. package/dist/KycController-method-action-types.d.cts.map +1 -0
  7. package/dist/KycController-method-action-types.d.mts +168 -0
  8. package/dist/KycController-method-action-types.d.mts.map +1 -0
  9. package/dist/KycController-method-action-types.mjs +6 -0
  10. package/dist/KycController-method-action-types.mjs.map +1 -0
  11. package/dist/KycController.cjs +1085 -0
  12. package/dist/KycController.cjs.map +1 -0
  13. package/dist/KycController.d.cts +255 -0
  14. package/dist/KycController.d.cts.map +1 -0
  15. package/dist/KycController.d.mts +255 -0
  16. package/dist/KycController.d.mts.map +1 -0
  17. package/dist/KycController.mjs +1080 -0
  18. package/dist/KycController.mjs.map +1 -0
  19. package/dist/KycService-method-action-types.cjs +7 -0
  20. package/dist/KycService-method-action-types.cjs.map +1 -0
  21. package/dist/KycService-method-action-types.d.cts +121 -0
  22. package/dist/KycService-method-action-types.d.cts.map +1 -0
  23. package/dist/KycService-method-action-types.d.mts +121 -0
  24. package/dist/KycService-method-action-types.d.mts.map +1 -0
  25. package/dist/KycService-method-action-types.mjs +6 -0
  26. package/dist/KycService-method-action-types.mjs.map +1 -0
  27. package/dist/KycService.cjs +417 -0
  28. package/dist/KycService.cjs.map +1 -0
  29. package/dist/KycService.d.cts +307 -0
  30. package/dist/KycService.d.cts.map +1 -0
  31. package/dist/KycService.d.mts +307 -0
  32. package/dist/KycService.d.mts.map +1 -0
  33. package/dist/KycService.mjs +413 -0
  34. package/dist/KycService.mjs.map +1 -0
  35. package/dist/countryCodes.cjs +274 -0
  36. package/dist/countryCodes.cjs.map +1 -0
  37. package/dist/countryCodes.d.cts +18 -0
  38. package/dist/countryCodes.d.cts.map +1 -0
  39. package/dist/countryCodes.d.mts +18 -0
  40. package/dist/countryCodes.d.mts.map +1 -0
  41. package/dist/countryCodes.mjs +270 -0
  42. package/dist/countryCodes.mjs.map +1 -0
  43. package/dist/crypto.cjs +154 -0
  44. package/dist/crypto.cjs.map +1 -0
  45. package/dist/crypto.d.cts +84 -0
  46. package/dist/crypto.d.cts.map +1 -0
  47. package/dist/crypto.d.mts +84 -0
  48. package/dist/crypto.d.mts.map +1 -0
  49. package/dist/crypto.mjs +149 -0
  50. package/dist/crypto.mjs.map +1 -0
  51. package/dist/encoding.cjs +40 -0
  52. package/dist/encoding.cjs.map +1 -0
  53. package/dist/encoding.d.cts +24 -0
  54. package/dist/encoding.d.cts.map +1 -0
  55. package/dist/encoding.d.mts +24 -0
  56. package/dist/encoding.d.mts.map +1 -0
  57. package/dist/encoding.mjs +35 -0
  58. package/dist/encoding.mjs.map +1 -0
  59. package/dist/index.cjs +34 -10
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.d.cts +18 -7
  62. package/dist/index.d.cts.map +1 -1
  63. package/dist/index.d.mts +18 -7
  64. package/dist/index.d.mts.map +1 -1
  65. package/dist/index.mjs +11 -9
  66. package/dist/index.mjs.map +1 -1
  67. package/dist/selectors.cjs +30 -0
  68. package/dist/selectors.cjs.map +1 -0
  69. package/dist/selectors.d.cts +24 -0
  70. package/dist/selectors.d.cts.map +1 -0
  71. package/dist/selectors.d.mts +24 -0
  72. package/dist/selectors.d.mts.map +1 -0
  73. package/dist/selectors.mjs +24 -0
  74. package/dist/selectors.mjs.map +1 -0
  75. package/dist/types.cjs +10 -0
  76. package/dist/types.cjs.map +1 -0
  77. package/dist/types.d.cts +124 -0
  78. package/dist/types.d.cts.map +1 -0
  79. package/dist/types.d.mts +124 -0
  80. package/dist/types.d.mts.map +1 -0
  81. package/dist/types.mjs +9 -0
  82. package/dist/types.mjs.map +1 -0
  83. package/dist/ukyc/constants.cjs +75 -0
  84. package/dist/ukyc/constants.cjs.map +1 -0
  85. package/dist/ukyc/constants.d.cts +69 -0
  86. package/dist/ukyc/constants.d.cts.map +1 -0
  87. package/dist/ukyc/constants.d.mts +69 -0
  88. package/dist/ukyc/constants.d.mts.map +1 -0
  89. package/dist/ukyc/constants.mjs +72 -0
  90. package/dist/ukyc/constants.mjs.map +1 -0
  91. package/dist/ukyc/deriveClientMaterial.cjs +65 -0
  92. package/dist/ukyc/deriveClientMaterial.cjs.map +1 -0
  93. package/dist/ukyc/deriveClientMaterial.d.cts +54 -0
  94. package/dist/ukyc/deriveClientMaterial.d.cts.map +1 -0
  95. package/dist/ukyc/deriveClientMaterial.d.mts +54 -0
  96. package/dist/ukyc/deriveClientMaterial.d.mts.map +1 -0
  97. package/dist/ukyc/deriveClientMaterial.mjs +60 -0
  98. package/dist/ukyc/deriveClientMaterial.mjs.map +1 -0
  99. package/dist/ukyc/jwtChain.cjs +54 -0
  100. package/dist/ukyc/jwtChain.cjs.map +1 -0
  101. package/dist/ukyc/jwtChain.d.cts +40 -0
  102. package/dist/ukyc/jwtChain.d.cts.map +1 -0
  103. package/dist/ukyc/jwtChain.d.mts +40 -0
  104. package/dist/ukyc/jwtChain.d.mts.map +1 -0
  105. package/dist/ukyc/jwtChain.mjs +50 -0
  106. package/dist/ukyc/jwtChain.mjs.map +1 -0
  107. package/dist/ukyc/localUserSecret.cjs +98 -0
  108. package/dist/ukyc/localUserSecret.cjs.map +1 -0
  109. package/dist/ukyc/localUserSecret.d.cts +65 -0
  110. package/dist/ukyc/localUserSecret.d.cts.map +1 -0
  111. package/dist/ukyc/localUserSecret.d.mts +65 -0
  112. package/dist/ukyc/localUserSecret.d.mts.map +1 -0
  113. package/dist/ukyc/localUserSecret.mjs +92 -0
  114. package/dist/ukyc/localUserSecret.mjs.map +1 -0
  115. package/dist/ukyc/storageAccessToken.cjs +139 -0
  116. package/dist/ukyc/storageAccessToken.cjs.map +1 -0
  117. package/dist/ukyc/storageAccessToken.d.cts +99 -0
  118. package/dist/ukyc/storageAccessToken.d.cts.map +1 -0
  119. package/dist/ukyc/storageAccessToken.d.mts +99 -0
  120. package/dist/ukyc/storageAccessToken.d.mts.map +1 -0
  121. package/dist/ukyc/storageAccessToken.mjs +133 -0
  122. package/dist/ukyc/storageAccessToken.mjs.map +1 -0
  123. package/dist/ukyc/testToken.cjs +61 -0
  124. package/dist/ukyc/testToken.cjs.map +1 -0
  125. package/dist/ukyc/testToken.d.cts +50 -0
  126. package/dist/ukyc/testToken.d.cts.map +1 -0
  127. package/dist/ukyc/testToken.d.mts +50 -0
  128. package/dist/ukyc/testToken.d.mts.map +1 -0
  129. package/dist/ukyc/testToken.mjs +57 -0
  130. package/dist/ukyc/testToken.mjs.map +1 -0
  131. package/dist/ukyc/wrapEncryptionKey.cjs +28 -0
  132. package/dist/ukyc/wrapEncryptionKey.cjs.map +1 -0
  133. package/dist/ukyc/wrapEncryptionKey.d.cts +34 -0
  134. package/dist/ukyc/wrapEncryptionKey.d.cts.map +1 -0
  135. package/dist/ukyc/wrapEncryptionKey.d.mts +34 -0
  136. package/dist/ukyc/wrapEncryptionKey.d.mts.map +1 -0
  137. package/dist/ukyc/wrapEncryptionKey.mjs +25 -0
  138. package/dist/ukyc/wrapEncryptionKey.mjs.map +1 -0
  139. package/dist/ukyc/wrapUserKey.cjs +80 -0
  140. package/dist/ukyc/wrapUserKey.cjs.map +1 -0
  141. package/dist/ukyc/wrapUserKey.d.cts +26 -0
  142. package/dist/ukyc/wrapUserKey.d.cts.map +1 -0
  143. package/dist/ukyc/wrapUserKey.d.mts +26 -0
  144. package/dist/ukyc/wrapUserKey.d.mts.map +1 -0
  145. package/dist/ukyc/wrapUserKey.mjs +76 -0
  146. package/dist/ukyc/wrapUserKey.mjs.map +1 -0
  147. package/dist/ukyc/wrappedRelayPayload.cjs +32 -0
  148. package/dist/ukyc/wrappedRelayPayload.cjs.map +1 -0
  149. package/dist/ukyc/wrappedRelayPayload.d.cts +35 -0
  150. package/dist/ukyc/wrappedRelayPayload.d.cts.map +1 -0
  151. package/dist/ukyc/wrappedRelayPayload.d.mts +35 -0
  152. package/dist/ukyc/wrappedRelayPayload.d.mts.map +1 -0
  153. package/dist/ukyc/wrappedRelayPayload.mjs +28 -0
  154. package/dist/ukyc/wrappedRelayPayload.mjs.map +1 -0
  155. package/package.json +24 -3
@@ -0,0 +1,92 @@
1
+ import { base64ToBytes, bytesToBase64 } from "@metamask/utils";
2
+ import { randomBytes } from "@noble/hashes/utils";
3
+ import { UKYC_LOCAL_USER_SECRET_PATH, UKYC_LOCAL_USER_SECRET_SIZE_BYTES } from "./constants.mjs";
4
+ /**
5
+ * In-flight `getOrCreateLocalUserSecret` calls, keyed by entropy source.
6
+ * Deduplicates concurrent enrollments in a single client session so we never
7
+ * generate and persist two competing `local_user_secret`s for the same source.
8
+ */
9
+ const inFlightCreations = new Map();
10
+ /**
11
+ * Loads the persisted `local_user_secret` from Encrypted User Storage, if one
12
+ * exists.
13
+ *
14
+ * @param store - The Encrypted User Storage adapter.
15
+ * @param entropySourceId - Optional HD keyring entropy source id, used to scope
16
+ * the secret to a specific SRP in multi-SRP wallets. Defaults to the primary SRP.
17
+ * @returns The decoded `local_user_secret` bytes, or `null` if none has been
18
+ * enrolled.
19
+ */
20
+ export async function loadLocalUserSecret(store, entropySourceId) {
21
+ const stored = await store.get(UKYC_LOCAL_USER_SECRET_PATH, entropySourceId);
22
+ if (!stored) {
23
+ return null;
24
+ }
25
+ const localUserSecret = base64ToBytes(stored);
26
+ if (localUserSecret.length !== UKYC_LOCAL_USER_SECRET_SIZE_BYTES) {
27
+ throw new Error(`UKYC: stored local_user_secret has unexpected length ${localUserSecret.length}, expected ${UKYC_LOCAL_USER_SECRET_SIZE_BYTES}.`);
28
+ }
29
+ return localUserSecret;
30
+ }
31
+ /**
32
+ * Persists a freshly generated `local_user_secret` to Encrypted User Storage.
33
+ *
34
+ * @param store - The Encrypted User Storage adapter.
35
+ * @param localUserSecret - The `local_user_secret` bytes to persist.
36
+ * @param entropySourceId - Optional HD keyring entropy source id.
37
+ */
38
+ async function persistLocalUserSecret(store, localUserSecret, entropySourceId) {
39
+ await store.set(UKYC_LOCAL_USER_SECRET_PATH, bytesToBase64(localUserSecret), entropySourceId);
40
+ }
41
+ /**
42
+ * Creates the UKYC `local_user_secret` if it does not already exist, otherwise
43
+ * loads the existing one. This is the single entry point used on UKYC
44
+ * enrollment.
45
+ *
46
+ * The operation is idempotent and safe against concurrent callers in the same
47
+ * session: repeated or parallel calls resolve to the same `local_user_secret`
48
+ * and never generate more than one secret for a given entropy source.
49
+ *
50
+ * @param store - The Encrypted User Storage adapter.
51
+ * @param entropySourceId - Optional HD keyring entropy source id, used to scope
52
+ * the secret to a specific SRP in multi-SRP wallets. Defaults to the primary SRP.
53
+ * @returns The `local_user_secret` bytes (existing or newly created).
54
+ */
55
+ export async function getOrCreateLocalUserSecret(store, entropySourceId) {
56
+ const cacheKey = entropySourceId ?? '';
57
+ const pending = inFlightCreations.get(cacheKey);
58
+ if (pending) {
59
+ return pending;
60
+ }
61
+ const creation = (async () => {
62
+ const existing = await loadLocalUserSecret(store, entropySourceId);
63
+ if (existing) {
64
+ return existing;
65
+ }
66
+ const localUserSecret = randomBytes(UKYC_LOCAL_USER_SECRET_SIZE_BYTES);
67
+ await persistLocalUserSecret(store, localUserSecret, entropySourceId);
68
+ // Re-read after persisting so that all callers converge on whatever value
69
+ // actually landed in storage (defends against a competing write that may
70
+ // have won the race, e.g. from another device syncing the same feature).
71
+ return ((await loadLocalUserSecret(store, entropySourceId)) ?? localUserSecret);
72
+ })();
73
+ inFlightCreations.set(cacheKey, creation);
74
+ try {
75
+ return await creation;
76
+ }
77
+ finally {
78
+ inFlightCreations.delete(cacheKey);
79
+ }
80
+ }
81
+ /**
82
+ * Whether a `local_user_secret` has already been enrolled for the given entropy
83
+ * source.
84
+ *
85
+ * @param store - The Encrypted User Storage adapter.
86
+ * @param entropySourceId - Optional HD keyring entropy source id.
87
+ * @returns `true` if a `local_user_secret` exists in Encrypted User Storage.
88
+ */
89
+ export async function hasLocalUserSecret(store, entropySourceId) {
90
+ return (await loadLocalUserSecret(store, entropySourceId)) !== null;
91
+ }
92
+ //# sourceMappingURL=localUserSecret.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localUserSecret.mjs","sourceRoot":"","sources":["../../src/ukyc/localUserSecret.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,wBAAwB;AAC/D,OAAO,EAAE,WAAW,EAAE,4BAA4B;AAElD,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,EAClC,wBAAuB;AAiCxB;;;;GAIG;AACH,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAA+B,CAAC;AAEjE;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,KAA+B,EAC/B,eAAwB;IAExB,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,2BAA2B,EAAE,eAAe,CAAC,CAAC;IAE7E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,eAAe,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IAE9C,IAAI,eAAe,CAAC,MAAM,KAAK,iCAAiC,EAAE,CAAC;QACjE,MAAM,IAAI,KAAK,CACb,wDAAwD,eAAe,CAAC,MAAM,cAAc,iCAAiC,GAAG,CACjI,CAAC;IACJ,CAAC;IAED,OAAO,eAAe,CAAC;AACzB,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,sBAAsB,CACnC,KAA+B,EAC/B,eAA2B,EAC3B,eAAwB;IAExB,MAAM,KAAK,CAAC,GAAG,CACb,2BAA2B,EAC3B,aAAa,CAAC,eAAe,CAAC,EAC9B,eAAe,CAChB,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,0BAA0B,CAC9C,KAA+B,EAC/B,eAAwB;IAExB,MAAM,QAAQ,GAAG,eAAe,IAAI,EAAE,CAAC;IAEvC,MAAM,OAAO,GAAG,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAChD,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,KAAK,IAAyB,EAAE;QAChD,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;QACnE,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,MAAM,eAAe,GAAG,WAAW,CAAC,iCAAiC,CAAC,CAAC;QACvE,MAAM,sBAAsB,CAAC,KAAK,EAAE,eAAe,EAAE,eAAe,CAAC,CAAC;QAEtE,0EAA0E;QAC1E,yEAAyE;QACzE,yEAAyE;QACzE,OAAO,CACL,CAAC,MAAM,mBAAmB,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,IAAI,eAAe,CACvE,CAAC;IACJ,CAAC,CAAC,EAAE,CAAC;IAEL,iBAAiB,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAE1C,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC;IACxB,CAAC;YAAS,CAAC;QACT,iBAAiB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAA+B,EAC/B,eAAwB;IAExB,OAAO,CAAC,MAAM,mBAAmB,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,KAAK,IAAI,CAAC;AACtE,CAAC","sourcesContent":["import { base64ToBytes, bytesToBase64 } from '@metamask/utils';\nimport { randomBytes } from '@noble/hashes/utils';\n\nimport {\n UKYC_LOCAL_USER_SECRET_PATH,\n UKYC_LOCAL_USER_SECRET_SIZE_BYTES,\n} from './constants.js';\n\n/**\n * Orchestrates creation and loading of the UKYC `local_user_secret`.\n *\n * The `local_user_secret` is the root secret for all UKYC client-derived\n * material. It is generated once, on first enrollment, and persisted to\n * MetaMask Encrypted User Storage. It is never transmitted off the device, not\n * even to the idOS Relay. Every subsequent value (`storage_id`,\n * `data_encryption_key`, `signing_key`, `relay_tunnel_key`) is derived from it\n * via HKDF — see `deriveClientMaterial`.\n *\n * This module is platform-agnostic: the Encrypted User Storage backing is\n * injected as a {@link UkycLocalUserSecretStore} so the controller (which owns\n * the messenger) supplies the concrete `UserStorageController` calls.\n */\n\n/**\n * The Encrypted User Storage operations this module needs. On MetaMask clients\n * these are backed by `UserStorageController:performGetStorage` /\n * `performSetStorage`.\n */\nexport type UkycLocalUserSecretStore = {\n /**\n * Reads the base64 string stored at `path`, or `null` if none exists.\n */\n get: (path: string, entropySourceId?: string) => Promise<string | null>;\n /**\n * Writes the base64 string `value` at `path`.\n */\n set: (path: string, value: string, entropySourceId?: string) => Promise<void>;\n};\n\n/**\n * In-flight `getOrCreateLocalUserSecret` calls, keyed by entropy source.\n * Deduplicates concurrent enrollments in a single client session so we never\n * generate and persist two competing `local_user_secret`s for the same source.\n */\nconst inFlightCreations = new Map<string, Promise<Uint8Array>>();\n\n/**\n * Loads the persisted `local_user_secret` from Encrypted User Storage, if one\n * exists.\n *\n * @param store - The Encrypted User Storage adapter.\n * @param entropySourceId - Optional HD keyring entropy source id, used to scope\n * the secret to a specific SRP in multi-SRP wallets. Defaults to the primary SRP.\n * @returns The decoded `local_user_secret` bytes, or `null` if none has been\n * enrolled.\n */\nexport async function loadLocalUserSecret(\n store: UkycLocalUserSecretStore,\n entropySourceId?: string,\n): Promise<Uint8Array | null> {\n const stored = await store.get(UKYC_LOCAL_USER_SECRET_PATH, entropySourceId);\n\n if (!stored) {\n return null;\n }\n\n const localUserSecret = base64ToBytes(stored);\n\n if (localUserSecret.length !== UKYC_LOCAL_USER_SECRET_SIZE_BYTES) {\n throw new Error(\n `UKYC: stored local_user_secret has unexpected length ${localUserSecret.length}, expected ${UKYC_LOCAL_USER_SECRET_SIZE_BYTES}.`,\n );\n }\n\n return localUserSecret;\n}\n\n/**\n * Persists a freshly generated `local_user_secret` to Encrypted User Storage.\n *\n * @param store - The Encrypted User Storage adapter.\n * @param localUserSecret - The `local_user_secret` bytes to persist.\n * @param entropySourceId - Optional HD keyring entropy source id.\n */\nasync function persistLocalUserSecret(\n store: UkycLocalUserSecretStore,\n localUserSecret: Uint8Array,\n entropySourceId?: string,\n): Promise<void> {\n await store.set(\n UKYC_LOCAL_USER_SECRET_PATH,\n bytesToBase64(localUserSecret),\n entropySourceId,\n );\n}\n\n/**\n * Creates the UKYC `local_user_secret` if it does not already exist, otherwise\n * loads the existing one. This is the single entry point used on UKYC\n * enrollment.\n *\n * The operation is idempotent and safe against concurrent callers in the same\n * session: repeated or parallel calls resolve to the same `local_user_secret`\n * and never generate more than one secret for a given entropy source.\n *\n * @param store - The Encrypted User Storage adapter.\n * @param entropySourceId - Optional HD keyring entropy source id, used to scope\n * the secret to a specific SRP in multi-SRP wallets. Defaults to the primary SRP.\n * @returns The `local_user_secret` bytes (existing or newly created).\n */\nexport async function getOrCreateLocalUserSecret(\n store: UkycLocalUserSecretStore,\n entropySourceId?: string,\n): Promise<Uint8Array> {\n const cacheKey = entropySourceId ?? '';\n\n const pending = inFlightCreations.get(cacheKey);\n if (pending) {\n return pending;\n }\n\n const creation = (async (): Promise<Uint8Array> => {\n const existing = await loadLocalUserSecret(store, entropySourceId);\n if (existing) {\n return existing;\n }\n\n const localUserSecret = randomBytes(UKYC_LOCAL_USER_SECRET_SIZE_BYTES);\n await persistLocalUserSecret(store, localUserSecret, entropySourceId);\n\n // Re-read after persisting so that all callers converge on whatever value\n // actually landed in storage (defends against a competing write that may\n // have won the race, e.g. from another device syncing the same feature).\n return (\n (await loadLocalUserSecret(store, entropySourceId)) ?? localUserSecret\n );\n })();\n\n inFlightCreations.set(cacheKey, creation);\n\n try {\n return await creation;\n } finally {\n inFlightCreations.delete(cacheKey);\n }\n}\n\n/**\n * Whether a `local_user_secret` has already been enrolled for the given entropy\n * source.\n *\n * @param store - The Encrypted User Storage adapter.\n * @param entropySourceId - Optional HD keyring entropy source id.\n * @returns `true` if a `local_user_secret` exists in Encrypted User Storage.\n */\nexport async function hasLocalUserSecret(\n store: UkycLocalUserSecretStore,\n entropySourceId?: string,\n): Promise<boolean> {\n return (await loadLocalUserSecret(store, entropySourceId)) !== null;\n}\n"]}
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.encodeStorageAccessTokenForHeader = exports.signStorageAccessToken = exports.canonicalizeJson = void 0;
4
+ const utils_1 = require("@metamask/utils");
5
+ const ed25519_1 = require("@noble/curves/ed25519");
6
+ const constants_js_1 = require("./constants.cjs");
7
+ const encoding_js_1 = require("../encoding.cjs");
8
+ /**
9
+ * Serializes a JSON value to RFC 8785 (JCS) canonical form.
10
+ *
11
+ * Scope note: this implementation covers the JSON shapes used by UKYC storage
12
+ * payloads — objects, arrays, strings, integers, booleans, and null. Object
13
+ * members are sorted by their UTF-16 code units (matching JS default string
14
+ * ordering, which is what JCS requires) and `undefined` members are dropped.
15
+ * Non-finite and non-integer numbers are rejected, since the payloads never
16
+ * contain them and correct JCS number formatting for the general case is
17
+ * intentionally out of scope here.
18
+ *
19
+ * @param value - The value to canonicalize.
20
+ * @returns The canonical JSON string.
21
+ */
22
+ function canonicalizeJson(value) {
23
+ if (value === null) {
24
+ return 'null';
25
+ }
26
+ if (typeof value === 'boolean') {
27
+ return value ? 'true' : 'false';
28
+ }
29
+ if (typeof value === 'number') {
30
+ if (!Number.isInteger(value)) {
31
+ throw new Error('UKYC: cannot canonicalize a non-integer number for JCS.');
32
+ }
33
+ return JSON.stringify(value);
34
+ }
35
+ if (typeof value === 'string') {
36
+ return JSON.stringify(value);
37
+ }
38
+ if (Array.isArray(value)) {
39
+ return `[${value.map((item) => canonicalizeJson(item)).join(',')}]`;
40
+ }
41
+ const entries = Object.keys(value)
42
+ .sort()
43
+ .reduce((acc, key) => {
44
+ const child = value[key];
45
+ if (child !== undefined) {
46
+ acc.push(`${JSON.stringify(key)}:${canonicalizeJson(child)}`);
47
+ }
48
+ return acc;
49
+ }, []);
50
+ return `{${entries.join(',')}}`;
51
+ }
52
+ exports.canonicalizeJson = canonicalizeJson;
53
+ /**
54
+ * Formats a date as RFC 3339 UTC with whole-second precision (e.g.
55
+ * `2026-07-07T00:00:00Z`). `Date.prototype.toISOString` always emits
56
+ * milliseconds (`...:00.000Z`); the `storage_access_token` wire format omits
57
+ * fractional seconds, so the sub-second component is truncated (not rounded).
58
+ *
59
+ * @param date - The date to format.
60
+ * @returns The RFC 3339 timestamp without fractional seconds.
61
+ */
62
+ function toRfc3339Seconds(date) {
63
+ return `${date.toISOString().slice(0, 19)}Z`;
64
+ }
65
+ /**
66
+ * Builds and signs a `storage_access_token`.
67
+ *
68
+ * @param params - See {@link SignStorageAccessTokenParams}.
69
+ * @returns The signed token envelope.
70
+ */
71
+ function signStorageAccessToken(params) {
72
+ const { material, operations, presenter = 'client', sessionId, issuedAt = new Date(), expiresAt, } = params;
73
+ assertValidOperations(operations);
74
+ if (expiresAt.getTime() <= issuedAt.getTime()) {
75
+ throw new Error('UKYC: storage_access_token expires_at must be after issued_at.');
76
+ }
77
+ const isDelete = operations.includes('delete');
78
+ if (presenter === 'idos-relay' && isDelete) {
79
+ throw new Error('UKYC: a delete-scoped storage_access_token cannot be delegated to the Relay.');
80
+ }
81
+ if (presenter === 'idos-relay' && !sessionId) {
82
+ throw new Error('UKYC: a Relay-presented storage_access_token requires a session_id.');
83
+ }
84
+ const payload = {
85
+ version: constants_js_1.UKYC_STORAGE_ACCESS_TOKEN_VERSION,
86
+ aud: [...constants_js_1.UKYC_STORAGE_ACCESS_TOKEN_AUDIENCES],
87
+ storage_id: (0, encoding_js_1.toBase64Url)(material.storageId),
88
+ signing_public_key: (0, encoding_js_1.toBase64Url)(material.signingPublicKey),
89
+ operations,
90
+ presenter,
91
+ issued_at: toRfc3339Seconds(issuedAt),
92
+ expires_at: toRfc3339Seconds(expiresAt),
93
+ };
94
+ // Only bind session_id for Relay-presented tokens; omit the key entirely for
95
+ // client-presented tokens so it does not appear in the canonicalized payload.
96
+ if (presenter === 'idos-relay') {
97
+ payload.session_id = sessionId;
98
+ }
99
+ const message = (0, utils_1.stringToBytes)(canonicalizeJson(payload));
100
+ const signature = ed25519_1.ed25519.sign(message, material.signingKey);
101
+ return {
102
+ payload,
103
+ signature: (0, encoding_js_1.toBase64Url)(signature),
104
+ };
105
+ }
106
+ exports.signStorageAccessToken = signStorageAccessToken;
107
+ /**
108
+ * Serializes a signed token to a compact string suitable for header transport
109
+ * (`Authorization: AccessToken <token>`, see `UKYC_CAPABILITY_AUTH_SCHEME`). The
110
+ * complete envelope is base64url-encoded; the private `signing_key` is never
111
+ * included.
112
+ *
113
+ * @param token - The signed token envelope.
114
+ * @returns The base64url-encoded envelope string.
115
+ */
116
+ function encodeStorageAccessTokenForHeader(token) {
117
+ return (0, encoding_js_1.toBase64Url)((0, utils_1.stringToBytes)(JSON.stringify(token)));
118
+ }
119
+ exports.encodeStorageAccessTokenForHeader = encodeStorageAccessTokenForHeader;
120
+ /**
121
+ * Validates that an operations list is one storage understands: a non-empty set
122
+ * of `read`/`write`, or exactly `['delete']`. `delete` is never combined with
123
+ * other operations.
124
+ *
125
+ * @param operations - The requested operations.
126
+ */
127
+ function assertValidOperations(operations) {
128
+ if (operations.length === 0) {
129
+ throw new Error('UKYC: storage_access_token requires at least one operation.');
130
+ }
131
+ const unique = new Set(operations);
132
+ if (unique.size !== operations.length) {
133
+ throw new Error('UKYC: storage_access_token operations must be unique.');
134
+ }
135
+ if (unique.has('delete') && operations.length > 1) {
136
+ throw new Error('UKYC: a delete-scoped storage_access_token must contain only "delete".');
137
+ }
138
+ }
139
+ //# sourceMappingURL=storageAccessToken.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storageAccessToken.cjs","sourceRoot":"","sources":["../../src/ukyc/storageAccessToken.ts"],"names":[],"mappings":";;;AAAA,2CAAgD;AAChD,mDAAgD;AAEhD,kDAGwB;AAExB,iDAA6C;AAiF7C;;;;;;;;;;;;;GAaG;AACH,SAAgB,gBAAgB,CAAC,KAAgB;IAC/C,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IAClC,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,yDAAyD,CAC1D,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACtE,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;SAC/B,IAAI,EAAE;SACN,MAAM,CAAW,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC,EAAE,EAAE,CAAC,CAAC;IAET,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAClC,CAAC;AArCD,4CAqCC;AAED;;;;;;;;GAQG;AACH,SAAS,gBAAgB,CAAC,IAAU;IAClC,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC;AAC/C,CAAC;AAED;;;;;GAKG;AACH,SAAgB,sBAAsB,CACpC,MAAoC;IAEpC,MAAM,EACJ,QAAQ,EACR,UAAU,EACV,SAAS,GAAG,QAAQ,EACpB,SAAS,EACT,QAAQ,GAAG,IAAI,IAAI,EAAE,EACrB,SAAS,GACV,GAAG,MAAM,CAAC;IAEX,qBAAqB,CAAC,UAAU,CAAC,CAAC;IAElC,IAAI,SAAS,CAAC,OAAO,EAAE,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,gEAAgE,CACjE,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAE/C,IAAI,SAAS,KAAK,YAAY,IAAI,QAAQ,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CACb,8EAA8E,CAC/E,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,KAAK,YAAY,IAAI,CAAC,SAAS,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CACb,qEAAqE,CACtE,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAkC;QAC7C,OAAO,EAAE,gDAAiC;QAC1C,GAAG,EAAE,CAAC,GAAG,kDAAmC,CAAC;QAC7C,UAAU,EAAE,IAAA,yBAAW,EAAC,QAAQ,CAAC,SAAS,CAAC;QAC3C,kBAAkB,EAAE,IAAA,yBAAW,EAAC,QAAQ,CAAC,gBAAgB,CAAC;QAC1D,UAAU;QACV,SAAS;QACT,SAAS,EAAE,gBAAgB,CAAC,QAAQ,CAAC;QACrC,UAAU,EAAE,gBAAgB,CAAC,SAAS,CAAC;KACxC,CAAC;IAEF,6EAA6E;IAC7E,8EAA8E;IAC9E,IAAI,SAAS,KAAK,YAAY,EAAE,CAAC;QAC/B,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IACjC,CAAC;IAED,MAAM,OAAO,GAAG,IAAA,qBAAa,EAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC;IACzD,MAAM,SAAS,GAAG,iBAAO,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;IAE7D,OAAO;QACL,OAAO;QACP,SAAS,EAAE,IAAA,yBAAW,EAAC,SAAS,CAAC;KAClC,CAAC;AACJ,CAAC;AA1DD,wDA0DC;AAED;;;;;;;;GAQG;AACH,SAAgB,iCAAiC,CAC/C,KAA6B;IAE7B,OAAO,IAAA,yBAAW,EAAC,IAAA,qBAAa,EAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC;AAJD,8EAIC;AAED;;;;;;GAMG;AACH,SAAS,qBAAqB,CAAC,UAAkC;IAC/D,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CACb,6DAA6D,CAC9D,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IAEnC,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC,MAAM,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IAC3E,CAAC;IAED,IAAI,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,KAAK,CACb,wEAAwE,CACzE,CAAC;IACJ,CAAC;AACH,CAAC","sourcesContent":["import { stringToBytes } from '@metamask/utils';\nimport { ed25519 } from '@noble/curves/ed25519';\n\nimport {\n UKYC_STORAGE_ACCESS_TOKEN_AUDIENCES,\n UKYC_STORAGE_ACCESS_TOKEN_VERSION,\n} from './constants.js';\nimport type { UkycClientMaterial } from './deriveClientMaterial.js';\nimport { toBase64Url } from '../encoding.js';\n\n/**\n * Mints `storage_access_token` capabilities — the client-signed, scoped,\n * session-bound proofs that authorize UKYC storage operations. See the\n * architecture doc, section \"Storage Authentication\".\n *\n * The token is Ed25519 over RFC 8785 (JCS) canonical JSON of the payload. Only\n * the client holds the private `signing_key`, so only the client can mint a\n * token; a `read`/`write`-scoped token may then be handed to the Relay to\n * present, but `delete` is never delegated.\n */\n\n/**\n * Storage operations a `storage_access_token` can authorize.\n */\nexport type UkycStorageOperation = 'read' | 'write' | 'delete';\n\n/**\n * Who presents the token to UKYC storage. The Relay may only present\n * `read`/`write` tokens; `delete` is always client-presented.\n */\nexport type UkycTokenPresenter = 'client' | 'idos-relay';\n\n/**\n * The signed `storage_access_token` payload. Field names are snake_case because\n * they are canonicalized and hashed exactly as they appear on the wire.\n */\nexport type UkycStorageAccessTokenPayload = {\n version: number;\n /** Every verifier that may accept the token, e.g. UKYC Storage and idOS Kwil. */\n aud: string[];\n // Wire-shape fields are snake_case; they are canonicalized and signed exactly\n // as they appear on the wire.\n /* eslint-disable @typescript-eslint/naming-convention */\n storage_id: string;\n signing_public_key: string;\n operations: UkycStorageOperation[];\n presenter: UkycTokenPresenter;\n /** UKYC session id. Required (and only present) when presenter is `idos-relay`. */\n session_id?: string;\n issued_at: string;\n expires_at: string;\n /* eslint-enable @typescript-eslint/naming-convention */\n};\n\n/**\n * The on-the-wire envelope: the payload plus its detached Ed25519 signature\n * (base64url) over the JCS canonicalization of the payload.\n */\nexport type UkycStorageAccessToken = {\n payload: UkycStorageAccessTokenPayload;\n signature: string;\n};\n\n/**\n * Inputs for minting a `storage_access_token`.\n */\nexport type SignStorageAccessTokenParams = {\n /** Client material derived from `local_user_secret`. */\n material: UkycClientMaterial;\n /** Operations the token authorizes. `delete` must be the sole operation. */\n operations: UkycStorageOperation[];\n /** Who will present the token. Defaults to `client`. */\n presenter?: UkycTokenPresenter;\n /** UKYC session id. Required when presenter is `idos-relay`. */\n sessionId?: string;\n /** Token issue time. Defaults to now. */\n issuedAt?: Date;\n /** Token expiry. Must be strictly after `issuedAt`. */\n expiresAt: Date;\n};\n\ntype JsonValue =\n | null\n | boolean\n | number\n | string\n | JsonValue[]\n | { [key: string]: JsonValue | undefined };\n\n/**\n * Serializes a JSON value to RFC 8785 (JCS) canonical form.\n *\n * Scope note: this implementation covers the JSON shapes used by UKYC storage\n * payloads — objects, arrays, strings, integers, booleans, and null. Object\n * members are sorted by their UTF-16 code units (matching JS default string\n * ordering, which is what JCS requires) and `undefined` members are dropped.\n * Non-finite and non-integer numbers are rejected, since the payloads never\n * contain them and correct JCS number formatting for the general case is\n * intentionally out of scope here.\n *\n * @param value - The value to canonicalize.\n * @returns The canonical JSON string.\n */\nexport function canonicalizeJson(value: JsonValue): string {\n if (value === null) {\n return 'null';\n }\n\n if (typeof value === 'boolean') {\n return value ? 'true' : 'false';\n }\n\n if (typeof value === 'number') {\n if (!Number.isInteger(value)) {\n throw new Error(\n 'UKYC: cannot canonicalize a non-integer number for JCS.',\n );\n }\n return JSON.stringify(value);\n }\n\n if (typeof value === 'string') {\n return JSON.stringify(value);\n }\n\n if (Array.isArray(value)) {\n return `[${value.map((item) => canonicalizeJson(item)).join(',')}]`;\n }\n\n const entries = Object.keys(value)\n .sort()\n .reduce<string[]>((acc, key) => {\n const child = value[key];\n if (child !== undefined) {\n acc.push(`${JSON.stringify(key)}:${canonicalizeJson(child)}`);\n }\n return acc;\n }, []);\n\n return `{${entries.join(',')}}`;\n}\n\n/**\n * Formats a date as RFC 3339 UTC with whole-second precision (e.g.\n * `2026-07-07T00:00:00Z`). `Date.prototype.toISOString` always emits\n * milliseconds (`...:00.000Z`); the `storage_access_token` wire format omits\n * fractional seconds, so the sub-second component is truncated (not rounded).\n *\n * @param date - The date to format.\n * @returns The RFC 3339 timestamp without fractional seconds.\n */\nfunction toRfc3339Seconds(date: Date): string {\n return `${date.toISOString().slice(0, 19)}Z`;\n}\n\n/**\n * Builds and signs a `storage_access_token`.\n *\n * @param params - See {@link SignStorageAccessTokenParams}.\n * @returns The signed token envelope.\n */\nexport function signStorageAccessToken(\n params: SignStorageAccessTokenParams,\n): UkycStorageAccessToken {\n const {\n material,\n operations,\n presenter = 'client',\n sessionId,\n issuedAt = new Date(),\n expiresAt,\n } = params;\n\n assertValidOperations(operations);\n\n if (expiresAt.getTime() <= issuedAt.getTime()) {\n throw new Error(\n 'UKYC: storage_access_token expires_at must be after issued_at.',\n );\n }\n\n const isDelete = operations.includes('delete');\n\n if (presenter === 'idos-relay' && isDelete) {\n throw new Error(\n 'UKYC: a delete-scoped storage_access_token cannot be delegated to the Relay.',\n );\n }\n\n if (presenter === 'idos-relay' && !sessionId) {\n throw new Error(\n 'UKYC: a Relay-presented storage_access_token requires a session_id.',\n );\n }\n\n const payload: UkycStorageAccessTokenPayload = {\n version: UKYC_STORAGE_ACCESS_TOKEN_VERSION,\n aud: [...UKYC_STORAGE_ACCESS_TOKEN_AUDIENCES],\n storage_id: toBase64Url(material.storageId),\n signing_public_key: toBase64Url(material.signingPublicKey),\n operations,\n presenter,\n issued_at: toRfc3339Seconds(issuedAt),\n expires_at: toRfc3339Seconds(expiresAt),\n };\n\n // Only bind session_id for Relay-presented tokens; omit the key entirely for\n // client-presented tokens so it does not appear in the canonicalized payload.\n if (presenter === 'idos-relay') {\n payload.session_id = sessionId;\n }\n\n const message = stringToBytes(canonicalizeJson(payload));\n const signature = ed25519.sign(message, material.signingKey);\n\n return {\n payload,\n signature: toBase64Url(signature),\n };\n}\n\n/**\n * Serializes a signed token to a compact string suitable for header transport\n * (`Authorization: AccessToken <token>`, see `UKYC_CAPABILITY_AUTH_SCHEME`). The\n * complete envelope is base64url-encoded; the private `signing_key` is never\n * included.\n *\n * @param token - The signed token envelope.\n * @returns The base64url-encoded envelope string.\n */\nexport function encodeStorageAccessTokenForHeader(\n token: UkycStorageAccessToken,\n): string {\n return toBase64Url(stringToBytes(JSON.stringify(token)));\n}\n\n/**\n * Validates that an operations list is one storage understands: a non-empty set\n * of `read`/`write`, or exactly `['delete']`. `delete` is never combined with\n * other operations.\n *\n * @param operations - The requested operations.\n */\nfunction assertValidOperations(operations: UkycStorageOperation[]): void {\n if (operations.length === 0) {\n throw new Error(\n 'UKYC: storage_access_token requires at least one operation.',\n );\n }\n\n const unique = new Set(operations);\n\n if (unique.size !== operations.length) {\n throw new Error('UKYC: storage_access_token operations must be unique.');\n }\n\n if (unique.has('delete') && operations.length > 1) {\n throw new Error(\n 'UKYC: a delete-scoped storage_access_token must contain only \"delete\".',\n );\n }\n}\n"]}
@@ -0,0 +1,99 @@
1
+ import type { UkycClientMaterial } from "./deriveClientMaterial.cjs";
2
+ /**
3
+ * Mints `storage_access_token` capabilities — the client-signed, scoped,
4
+ * session-bound proofs that authorize UKYC storage operations. See the
5
+ * architecture doc, section "Storage Authentication".
6
+ *
7
+ * The token is Ed25519 over RFC 8785 (JCS) canonical JSON of the payload. Only
8
+ * the client holds the private `signing_key`, so only the client can mint a
9
+ * token; a `read`/`write`-scoped token may then be handed to the Relay to
10
+ * present, but `delete` is never delegated.
11
+ */
12
+ /**
13
+ * Storage operations a `storage_access_token` can authorize.
14
+ */
15
+ export type UkycStorageOperation = 'read' | 'write' | 'delete';
16
+ /**
17
+ * Who presents the token to UKYC storage. The Relay may only present
18
+ * `read`/`write` tokens; `delete` is always client-presented.
19
+ */
20
+ export type UkycTokenPresenter = 'client' | 'idos-relay';
21
+ /**
22
+ * The signed `storage_access_token` payload. Field names are snake_case because
23
+ * they are canonicalized and hashed exactly as they appear on the wire.
24
+ */
25
+ export type UkycStorageAccessTokenPayload = {
26
+ version: number;
27
+ /** Every verifier that may accept the token, e.g. UKYC Storage and idOS Kwil. */
28
+ aud: string[];
29
+ storage_id: string;
30
+ signing_public_key: string;
31
+ operations: UkycStorageOperation[];
32
+ presenter: UkycTokenPresenter;
33
+ /** UKYC session id. Required (and only present) when presenter is `idos-relay`. */
34
+ session_id?: string;
35
+ issued_at: string;
36
+ expires_at: string;
37
+ };
38
+ /**
39
+ * The on-the-wire envelope: the payload plus its detached Ed25519 signature
40
+ * (base64url) over the JCS canonicalization of the payload.
41
+ */
42
+ export type UkycStorageAccessToken = {
43
+ payload: UkycStorageAccessTokenPayload;
44
+ signature: string;
45
+ };
46
+ /**
47
+ * Inputs for minting a `storage_access_token`.
48
+ */
49
+ export type SignStorageAccessTokenParams = {
50
+ /** Client material derived from `local_user_secret`. */
51
+ material: UkycClientMaterial;
52
+ /** Operations the token authorizes. `delete` must be the sole operation. */
53
+ operations: UkycStorageOperation[];
54
+ /** Who will present the token. Defaults to `client`. */
55
+ presenter?: UkycTokenPresenter;
56
+ /** UKYC session id. Required when presenter is `idos-relay`. */
57
+ sessionId?: string;
58
+ /** Token issue time. Defaults to now. */
59
+ issuedAt?: Date;
60
+ /** Token expiry. Must be strictly after `issuedAt`. */
61
+ expiresAt: Date;
62
+ };
63
+ type JsonValue = null | boolean | number | string | JsonValue[] | {
64
+ [key: string]: JsonValue | undefined;
65
+ };
66
+ /**
67
+ * Serializes a JSON value to RFC 8785 (JCS) canonical form.
68
+ *
69
+ * Scope note: this implementation covers the JSON shapes used by UKYC storage
70
+ * payloads — objects, arrays, strings, integers, booleans, and null. Object
71
+ * members are sorted by their UTF-16 code units (matching JS default string
72
+ * ordering, which is what JCS requires) and `undefined` members are dropped.
73
+ * Non-finite and non-integer numbers are rejected, since the payloads never
74
+ * contain them and correct JCS number formatting for the general case is
75
+ * intentionally out of scope here.
76
+ *
77
+ * @param value - The value to canonicalize.
78
+ * @returns The canonical JSON string.
79
+ */
80
+ export declare function canonicalizeJson(value: JsonValue): string;
81
+ /**
82
+ * Builds and signs a `storage_access_token`.
83
+ *
84
+ * @param params - See {@link SignStorageAccessTokenParams}.
85
+ * @returns The signed token envelope.
86
+ */
87
+ export declare function signStorageAccessToken(params: SignStorageAccessTokenParams): UkycStorageAccessToken;
88
+ /**
89
+ * Serializes a signed token to a compact string suitable for header transport
90
+ * (`Authorization: AccessToken <token>`, see `UKYC_CAPABILITY_AUTH_SCHEME`). The
91
+ * complete envelope is base64url-encoded; the private `signing_key` is never
92
+ * included.
93
+ *
94
+ * @param token - The signed token envelope.
95
+ * @returns The base64url-encoded envelope string.
96
+ */
97
+ export declare function encodeStorageAccessTokenForHeader(token: UkycStorageAccessToken): string;
98
+ export {};
99
+ //# sourceMappingURL=storageAccessToken.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storageAccessToken.d.cts","sourceRoot":"","sources":["../../src/ukyc/storageAccessToken.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mCAAkC;AAGpE;;;;;;;;;GASG;AAEH;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE/D;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEzD;;;GAGG;AACH,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,GAAG,EAAE,MAAM,EAAE,CAAC;IAId,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACnC,SAAS,EAAE,kBAAkB,CAAC;IAC9B,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CAEpB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,6BAA6B,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC,wDAAwD;IACxD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,4EAA4E;IAC5E,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACnC,wDAAwD;IACxD,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,uDAAuD;IACvD,SAAS,EAAE,IAAI,CAAC;CACjB,CAAC;AAEF,KAAK,SAAS,GACV,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAA;CAAE,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAqCzD;AAeD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,4BAA4B,GACnC,sBAAsB,CAwDxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,sBAAsB,GAC5B,MAAM,CAER"}
@@ -0,0 +1,99 @@
1
+ import type { UkycClientMaterial } from "./deriveClientMaterial.mjs";
2
+ /**
3
+ * Mints `storage_access_token` capabilities — the client-signed, scoped,
4
+ * session-bound proofs that authorize UKYC storage operations. See the
5
+ * architecture doc, section "Storage Authentication".
6
+ *
7
+ * The token is Ed25519 over RFC 8785 (JCS) canonical JSON of the payload. Only
8
+ * the client holds the private `signing_key`, so only the client can mint a
9
+ * token; a `read`/`write`-scoped token may then be handed to the Relay to
10
+ * present, but `delete` is never delegated.
11
+ */
12
+ /**
13
+ * Storage operations a `storage_access_token` can authorize.
14
+ */
15
+ export type UkycStorageOperation = 'read' | 'write' | 'delete';
16
+ /**
17
+ * Who presents the token to UKYC storage. The Relay may only present
18
+ * `read`/`write` tokens; `delete` is always client-presented.
19
+ */
20
+ export type UkycTokenPresenter = 'client' | 'idos-relay';
21
+ /**
22
+ * The signed `storage_access_token` payload. Field names are snake_case because
23
+ * they are canonicalized and hashed exactly as they appear on the wire.
24
+ */
25
+ export type UkycStorageAccessTokenPayload = {
26
+ version: number;
27
+ /** Every verifier that may accept the token, e.g. UKYC Storage and idOS Kwil. */
28
+ aud: string[];
29
+ storage_id: string;
30
+ signing_public_key: string;
31
+ operations: UkycStorageOperation[];
32
+ presenter: UkycTokenPresenter;
33
+ /** UKYC session id. Required (and only present) when presenter is `idos-relay`. */
34
+ session_id?: string;
35
+ issued_at: string;
36
+ expires_at: string;
37
+ };
38
+ /**
39
+ * The on-the-wire envelope: the payload plus its detached Ed25519 signature
40
+ * (base64url) over the JCS canonicalization of the payload.
41
+ */
42
+ export type UkycStorageAccessToken = {
43
+ payload: UkycStorageAccessTokenPayload;
44
+ signature: string;
45
+ };
46
+ /**
47
+ * Inputs for minting a `storage_access_token`.
48
+ */
49
+ export type SignStorageAccessTokenParams = {
50
+ /** Client material derived from `local_user_secret`. */
51
+ material: UkycClientMaterial;
52
+ /** Operations the token authorizes. `delete` must be the sole operation. */
53
+ operations: UkycStorageOperation[];
54
+ /** Who will present the token. Defaults to `client`. */
55
+ presenter?: UkycTokenPresenter;
56
+ /** UKYC session id. Required when presenter is `idos-relay`. */
57
+ sessionId?: string;
58
+ /** Token issue time. Defaults to now. */
59
+ issuedAt?: Date;
60
+ /** Token expiry. Must be strictly after `issuedAt`. */
61
+ expiresAt: Date;
62
+ };
63
+ type JsonValue = null | boolean | number | string | JsonValue[] | {
64
+ [key: string]: JsonValue | undefined;
65
+ };
66
+ /**
67
+ * Serializes a JSON value to RFC 8785 (JCS) canonical form.
68
+ *
69
+ * Scope note: this implementation covers the JSON shapes used by UKYC storage
70
+ * payloads — objects, arrays, strings, integers, booleans, and null. Object
71
+ * members are sorted by their UTF-16 code units (matching JS default string
72
+ * ordering, which is what JCS requires) and `undefined` members are dropped.
73
+ * Non-finite and non-integer numbers are rejected, since the payloads never
74
+ * contain them and correct JCS number formatting for the general case is
75
+ * intentionally out of scope here.
76
+ *
77
+ * @param value - The value to canonicalize.
78
+ * @returns The canonical JSON string.
79
+ */
80
+ export declare function canonicalizeJson(value: JsonValue): string;
81
+ /**
82
+ * Builds and signs a `storage_access_token`.
83
+ *
84
+ * @param params - See {@link SignStorageAccessTokenParams}.
85
+ * @returns The signed token envelope.
86
+ */
87
+ export declare function signStorageAccessToken(params: SignStorageAccessTokenParams): UkycStorageAccessToken;
88
+ /**
89
+ * Serializes a signed token to a compact string suitable for header transport
90
+ * (`Authorization: AccessToken <token>`, see `UKYC_CAPABILITY_AUTH_SCHEME`). The
91
+ * complete envelope is base64url-encoded; the private `signing_key` is never
92
+ * included.
93
+ *
94
+ * @param token - The signed token envelope.
95
+ * @returns The base64url-encoded envelope string.
96
+ */
97
+ export declare function encodeStorageAccessTokenForHeader(token: UkycStorageAccessToken): string;
98
+ export {};
99
+ //# sourceMappingURL=storageAccessToken.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storageAccessToken.d.mts","sourceRoot":"","sources":["../../src/ukyc/storageAccessToken.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mCAAkC;AAGpE;;;;;;;;;GASG;AAEH;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE/D;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,YAAY,CAAC;AAEzD;;;GAGG;AACH,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,GAAG,EAAE,MAAM,EAAE,CAAC;IAId,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACnC,SAAS,EAAE,kBAAkB,CAAC;IAC9B,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CAEpB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,6BAA6B,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC,wDAAwD;IACxD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,4EAA4E;IAC5E,UAAU,EAAE,oBAAoB,EAAE,CAAC;IACnC,wDAAwD;IACxD,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,QAAQ,CAAC,EAAE,IAAI,CAAC;IAChB,uDAAuD;IACvD,SAAS,EAAE,IAAI,CAAC;CACjB,CAAC;AAEF,KAAK,SAAS,GACV,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAAA;CAAE,CAAC;AAE7C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAqCzD;AAeD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,4BAA4B,GACnC,sBAAsB,CAwDxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,sBAAsB,GAC5B,MAAM,CAER"}
@@ -0,0 +1,133 @@
1
+ import { stringToBytes } from "@metamask/utils";
2
+ import { ed25519 } from "@noble/curves/ed25519";
3
+ import { UKYC_STORAGE_ACCESS_TOKEN_AUDIENCES, UKYC_STORAGE_ACCESS_TOKEN_VERSION } from "./constants.mjs";
4
+ import { toBase64Url } from "../encoding.mjs";
5
+ /**
6
+ * Serializes a JSON value to RFC 8785 (JCS) canonical form.
7
+ *
8
+ * Scope note: this implementation covers the JSON shapes used by UKYC storage
9
+ * payloads — objects, arrays, strings, integers, booleans, and null. Object
10
+ * members are sorted by their UTF-16 code units (matching JS default string
11
+ * ordering, which is what JCS requires) and `undefined` members are dropped.
12
+ * Non-finite and non-integer numbers are rejected, since the payloads never
13
+ * contain them and correct JCS number formatting for the general case is
14
+ * intentionally out of scope here.
15
+ *
16
+ * @param value - The value to canonicalize.
17
+ * @returns The canonical JSON string.
18
+ */
19
+ export function canonicalizeJson(value) {
20
+ if (value === null) {
21
+ return 'null';
22
+ }
23
+ if (typeof value === 'boolean') {
24
+ return value ? 'true' : 'false';
25
+ }
26
+ if (typeof value === 'number') {
27
+ if (!Number.isInteger(value)) {
28
+ throw new Error('UKYC: cannot canonicalize a non-integer number for JCS.');
29
+ }
30
+ return JSON.stringify(value);
31
+ }
32
+ if (typeof value === 'string') {
33
+ return JSON.stringify(value);
34
+ }
35
+ if (Array.isArray(value)) {
36
+ return `[${value.map((item) => canonicalizeJson(item)).join(',')}]`;
37
+ }
38
+ const entries = Object.keys(value)
39
+ .sort()
40
+ .reduce((acc, key) => {
41
+ const child = value[key];
42
+ if (child !== undefined) {
43
+ acc.push(`${JSON.stringify(key)}:${canonicalizeJson(child)}`);
44
+ }
45
+ return acc;
46
+ }, []);
47
+ return `{${entries.join(',')}}`;
48
+ }
49
+ /**
50
+ * Formats a date as RFC 3339 UTC with whole-second precision (e.g.
51
+ * `2026-07-07T00:00:00Z`). `Date.prototype.toISOString` always emits
52
+ * milliseconds (`...:00.000Z`); the `storage_access_token` wire format omits
53
+ * fractional seconds, so the sub-second component is truncated (not rounded).
54
+ *
55
+ * @param date - The date to format.
56
+ * @returns The RFC 3339 timestamp without fractional seconds.
57
+ */
58
+ function toRfc3339Seconds(date) {
59
+ return `${date.toISOString().slice(0, 19)}Z`;
60
+ }
61
+ /**
62
+ * Builds and signs a `storage_access_token`.
63
+ *
64
+ * @param params - See {@link SignStorageAccessTokenParams}.
65
+ * @returns The signed token envelope.
66
+ */
67
+ export function signStorageAccessToken(params) {
68
+ const { material, operations, presenter = 'client', sessionId, issuedAt = new Date(), expiresAt, } = params;
69
+ assertValidOperations(operations);
70
+ if (expiresAt.getTime() <= issuedAt.getTime()) {
71
+ throw new Error('UKYC: storage_access_token expires_at must be after issued_at.');
72
+ }
73
+ const isDelete = operations.includes('delete');
74
+ if (presenter === 'idos-relay' && isDelete) {
75
+ throw new Error('UKYC: a delete-scoped storage_access_token cannot be delegated to the Relay.');
76
+ }
77
+ if (presenter === 'idos-relay' && !sessionId) {
78
+ throw new Error('UKYC: a Relay-presented storage_access_token requires a session_id.');
79
+ }
80
+ const payload = {
81
+ version: UKYC_STORAGE_ACCESS_TOKEN_VERSION,
82
+ aud: [...UKYC_STORAGE_ACCESS_TOKEN_AUDIENCES],
83
+ storage_id: toBase64Url(material.storageId),
84
+ signing_public_key: toBase64Url(material.signingPublicKey),
85
+ operations,
86
+ presenter,
87
+ issued_at: toRfc3339Seconds(issuedAt),
88
+ expires_at: toRfc3339Seconds(expiresAt),
89
+ };
90
+ // Only bind session_id for Relay-presented tokens; omit the key entirely for
91
+ // client-presented tokens so it does not appear in the canonicalized payload.
92
+ if (presenter === 'idos-relay') {
93
+ payload.session_id = sessionId;
94
+ }
95
+ const message = stringToBytes(canonicalizeJson(payload));
96
+ const signature = ed25519.sign(message, material.signingKey);
97
+ return {
98
+ payload,
99
+ signature: toBase64Url(signature),
100
+ };
101
+ }
102
+ /**
103
+ * Serializes a signed token to a compact string suitable for header transport
104
+ * (`Authorization: AccessToken <token>`, see `UKYC_CAPABILITY_AUTH_SCHEME`). The
105
+ * complete envelope is base64url-encoded; the private `signing_key` is never
106
+ * included.
107
+ *
108
+ * @param token - The signed token envelope.
109
+ * @returns The base64url-encoded envelope string.
110
+ */
111
+ export function encodeStorageAccessTokenForHeader(token) {
112
+ return toBase64Url(stringToBytes(JSON.stringify(token)));
113
+ }
114
+ /**
115
+ * Validates that an operations list is one storage understands: a non-empty set
116
+ * of `read`/`write`, or exactly `['delete']`. `delete` is never combined with
117
+ * other operations.
118
+ *
119
+ * @param operations - The requested operations.
120
+ */
121
+ function assertValidOperations(operations) {
122
+ if (operations.length === 0) {
123
+ throw new Error('UKYC: storage_access_token requires at least one operation.');
124
+ }
125
+ const unique = new Set(operations);
126
+ if (unique.size !== operations.length) {
127
+ throw new Error('UKYC: storage_access_token operations must be unique.');
128
+ }
129
+ if (unique.has('delete') && operations.length > 1) {
130
+ throw new Error('UKYC: a delete-scoped storage_access_token must contain only "delete".');
131
+ }
132
+ }
133
+ //# sourceMappingURL=storageAccessToken.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storageAccessToken.mjs","sourceRoot":"","sources":["../../src/ukyc/storageAccessToken.ts"],"names":[],"mappings":"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","sourcesContent":["import { stringToBytes } from '@metamask/utils';\nimport { ed25519 } from '@noble/curves/ed25519';\n\nimport {\n UKYC_STORAGE_ACCESS_TOKEN_AUDIENCES,\n UKYC_STORAGE_ACCESS_TOKEN_VERSION,\n} from './constants.js';\nimport type { UkycClientMaterial } from './deriveClientMaterial.js';\nimport { toBase64Url } from '../encoding.js';\n\n/**\n * Mints `storage_access_token` capabilities — the client-signed, scoped,\n * session-bound proofs that authorize UKYC storage operations. See the\n * architecture doc, section \"Storage Authentication\".\n *\n * The token is Ed25519 over RFC 8785 (JCS) canonical JSON of the payload. Only\n * the client holds the private `signing_key`, so only the client can mint a\n * token; a `read`/`write`-scoped token may then be handed to the Relay to\n * present, but `delete` is never delegated.\n */\n\n/**\n * Storage operations a `storage_access_token` can authorize.\n */\nexport type UkycStorageOperation = 'read' | 'write' | 'delete';\n\n/**\n * Who presents the token to UKYC storage. The Relay may only present\n * `read`/`write` tokens; `delete` is always client-presented.\n */\nexport type UkycTokenPresenter = 'client' | 'idos-relay';\n\n/**\n * The signed `storage_access_token` payload. Field names are snake_case because\n * they are canonicalized and hashed exactly as they appear on the wire.\n */\nexport type UkycStorageAccessTokenPayload = {\n version: number;\n /** Every verifier that may accept the token, e.g. UKYC Storage and idOS Kwil. */\n aud: string[];\n // Wire-shape fields are snake_case; they are canonicalized and signed exactly\n // as they appear on the wire.\n /* eslint-disable @typescript-eslint/naming-convention */\n storage_id: string;\n signing_public_key: string;\n operations: UkycStorageOperation[];\n presenter: UkycTokenPresenter;\n /** UKYC session id. Required (and only present) when presenter is `idos-relay`. */\n session_id?: string;\n issued_at: string;\n expires_at: string;\n /* eslint-enable @typescript-eslint/naming-convention */\n};\n\n/**\n * The on-the-wire envelope: the payload plus its detached Ed25519 signature\n * (base64url) over the JCS canonicalization of the payload.\n */\nexport type UkycStorageAccessToken = {\n payload: UkycStorageAccessTokenPayload;\n signature: string;\n};\n\n/**\n * Inputs for minting a `storage_access_token`.\n */\nexport type SignStorageAccessTokenParams = {\n /** Client material derived from `local_user_secret`. */\n material: UkycClientMaterial;\n /** Operations the token authorizes. `delete` must be the sole operation. */\n operations: UkycStorageOperation[];\n /** Who will present the token. Defaults to `client`. */\n presenter?: UkycTokenPresenter;\n /** UKYC session id. Required when presenter is `idos-relay`. */\n sessionId?: string;\n /** Token issue time. Defaults to now. */\n issuedAt?: Date;\n /** Token expiry. Must be strictly after `issuedAt`. */\n expiresAt: Date;\n};\n\ntype JsonValue =\n | null\n | boolean\n | number\n | string\n | JsonValue[]\n | { [key: string]: JsonValue | undefined };\n\n/**\n * Serializes a JSON value to RFC 8785 (JCS) canonical form.\n *\n * Scope note: this implementation covers the JSON shapes used by UKYC storage\n * payloads — objects, arrays, strings, integers, booleans, and null. Object\n * members are sorted by their UTF-16 code units (matching JS default string\n * ordering, which is what JCS requires) and `undefined` members are dropped.\n * Non-finite and non-integer numbers are rejected, since the payloads never\n * contain them and correct JCS number formatting for the general case is\n * intentionally out of scope here.\n *\n * @param value - The value to canonicalize.\n * @returns The canonical JSON string.\n */\nexport function canonicalizeJson(value: JsonValue): string {\n if (value === null) {\n return 'null';\n }\n\n if (typeof value === 'boolean') {\n return value ? 'true' : 'false';\n }\n\n if (typeof value === 'number') {\n if (!Number.isInteger(value)) {\n throw new Error(\n 'UKYC: cannot canonicalize a non-integer number for JCS.',\n );\n }\n return JSON.stringify(value);\n }\n\n if (typeof value === 'string') {\n return JSON.stringify(value);\n }\n\n if (Array.isArray(value)) {\n return `[${value.map((item) => canonicalizeJson(item)).join(',')}]`;\n }\n\n const entries = Object.keys(value)\n .sort()\n .reduce<string[]>((acc, key) => {\n const child = value[key];\n if (child !== undefined) {\n acc.push(`${JSON.stringify(key)}:${canonicalizeJson(child)}`);\n }\n return acc;\n }, []);\n\n return `{${entries.join(',')}}`;\n}\n\n/**\n * Formats a date as RFC 3339 UTC with whole-second precision (e.g.\n * `2026-07-07T00:00:00Z`). `Date.prototype.toISOString` always emits\n * milliseconds (`...:00.000Z`); the `storage_access_token` wire format omits\n * fractional seconds, so the sub-second component is truncated (not rounded).\n *\n * @param date - The date to format.\n * @returns The RFC 3339 timestamp without fractional seconds.\n */\nfunction toRfc3339Seconds(date: Date): string {\n return `${date.toISOString().slice(0, 19)}Z`;\n}\n\n/**\n * Builds and signs a `storage_access_token`.\n *\n * @param params - See {@link SignStorageAccessTokenParams}.\n * @returns The signed token envelope.\n */\nexport function signStorageAccessToken(\n params: SignStorageAccessTokenParams,\n): UkycStorageAccessToken {\n const {\n material,\n operations,\n presenter = 'client',\n sessionId,\n issuedAt = new Date(),\n expiresAt,\n } = params;\n\n assertValidOperations(operations);\n\n if (expiresAt.getTime() <= issuedAt.getTime()) {\n throw new Error(\n 'UKYC: storage_access_token expires_at must be after issued_at.',\n );\n }\n\n const isDelete = operations.includes('delete');\n\n if (presenter === 'idos-relay' && isDelete) {\n throw new Error(\n 'UKYC: a delete-scoped storage_access_token cannot be delegated to the Relay.',\n );\n }\n\n if (presenter === 'idos-relay' && !sessionId) {\n throw new Error(\n 'UKYC: a Relay-presented storage_access_token requires a session_id.',\n );\n }\n\n const payload: UkycStorageAccessTokenPayload = {\n version: UKYC_STORAGE_ACCESS_TOKEN_VERSION,\n aud: [...UKYC_STORAGE_ACCESS_TOKEN_AUDIENCES],\n storage_id: toBase64Url(material.storageId),\n signing_public_key: toBase64Url(material.signingPublicKey),\n operations,\n presenter,\n issued_at: toRfc3339Seconds(issuedAt),\n expires_at: toRfc3339Seconds(expiresAt),\n };\n\n // Only bind session_id for Relay-presented tokens; omit the key entirely for\n // client-presented tokens so it does not appear in the canonicalized payload.\n if (presenter === 'idos-relay') {\n payload.session_id = sessionId;\n }\n\n const message = stringToBytes(canonicalizeJson(payload));\n const signature = ed25519.sign(message, material.signingKey);\n\n return {\n payload,\n signature: toBase64Url(signature),\n };\n}\n\n/**\n * Serializes a signed token to a compact string suitable for header transport\n * (`Authorization: AccessToken <token>`, see `UKYC_CAPABILITY_AUTH_SCHEME`). The\n * complete envelope is base64url-encoded; the private `signing_key` is never\n * included.\n *\n * @param token - The signed token envelope.\n * @returns The base64url-encoded envelope string.\n */\nexport function encodeStorageAccessTokenForHeader(\n token: UkycStorageAccessToken,\n): string {\n return toBase64Url(stringToBytes(JSON.stringify(token)));\n}\n\n/**\n * Validates that an operations list is one storage understands: a non-empty set\n * of `read`/`write`, or exactly `['delete']`. `delete` is never combined with\n * other operations.\n *\n * @param operations - The requested operations.\n */\nfunction assertValidOperations(operations: UkycStorageOperation[]): void {\n if (operations.length === 0) {\n throw new Error(\n 'UKYC: storage_access_token requires at least one operation.',\n );\n }\n\n const unique = new Set(operations);\n\n if (unique.size !== operations.length) {\n throw new Error('UKYC: storage_access_token operations must be unique.');\n }\n\n if (unique.has('delete') && operations.length > 1) {\n throw new Error(\n 'UKYC: a delete-scoped storage_access_token must contain only \"delete\".',\n );\n }\n}\n"]}