@learncard/lca-api-service 1.4.0 → 1.5.1

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 (191) hide show
  1. package/.env.example +73 -0
  2. package/AGENTS.md +106 -10
  3. package/CHANGELOG.md +52 -0
  4. package/bootstrapLambda.test.ts +76 -0
  5. package/bundle.mjs +9 -1
  6. package/config/config.dev.json +23 -0
  7. package/config/config.example.json +22 -0
  8. package/config/config.json +1 -0
  9. package/config/config.production.json +12 -0
  10. package/config/config.scouts.dev.json +11 -0
  11. package/config/config.scouts.production.json +11 -0
  12. package/dist/app.d.ts +67 -3
  13. package/dist/cache/getDel.d.ts +2 -0
  14. package/dist/cache/getDel.test.d.ts +1 -0
  15. package/dist/cache/login-tickets.d.ts +12 -0
  16. package/dist/config/applyDockerStageConfig.d.ts +1 -0
  17. package/dist/config/cacheEnvironment.d.ts +16 -0
  18. package/dist/config/environment.d.ts +101 -0
  19. package/dist/config/escrowBundleMode.test.d.ts +1 -0
  20. package/dist/config/escrowDeployment.test.d.ts +1 -0
  21. package/dist/config/oidcEnvironment.d.ts +74 -0
  22. package/dist/config/serverlessFunctionEnv.test.d.ts +1 -0
  23. package/dist/config/stageConfig.d.ts +62 -0
  24. package/dist/config/stageConfig.test.d.ts +1 -0
  25. package/dist/didkit_wasm_bg.wasm +0 -0
  26. package/dist/docker-entry.d.ts +1 -1
  27. package/dist/helpers/auth.helpers.d.ts +1 -1
  28. package/dist/helpers/escrowCancelLink.d.ts +7 -0
  29. package/dist/helpers/escrowCancelLink.test.d.ts +1 -0
  30. package/dist/helpers/escrowCancelToken.d.ts +4 -0
  31. package/dist/helpers/escrowCancelToken.test.d.ts +1 -0
  32. package/dist/helpers/keycloak.helpers.d.ts +23 -0
  33. package/dist/helpers/login-ticket.helpers.d.ts +31 -0
  34. package/dist/helpers/login-ticket.helpers.test.d.ts +1 -0
  35. package/dist/helpers/oidc.helpers.d.ts +47 -0
  36. package/dist/helpers/rate-limit.helpers.d.ts +6 -0
  37. package/dist/helpers/rate-limit.helpers.test.d.ts +1 -0
  38. package/dist/helpers/social-token.helpers.d.ts +19 -0
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.js +2 -2
  41. package/dist/jobs/escrowBlobRewrap.d.ts +9 -0
  42. package/dist/jobs/escrowHoldReminders.d.ts +12 -0
  43. package/dist/models/AuthSubject.d.ts +20 -0
  44. package/dist/models/EscrowHold.d.ts +79 -1
  45. package/dist/models/Notification.d.ts +5 -0
  46. package/dist/models/UserKey.d.ts +11 -1
  47. package/dist/models/index.d.ts +1 -0
  48. package/dist/oidc.d.ts +5 -0
  49. package/dist/publish-metafile.json +1 -0
  50. package/dist/routes/auth.d.ts +31 -0
  51. package/dist/routes/escrow.d.ts +18 -4
  52. package/dist/routes/keys.d.ts +1 -0
  53. package/dist/routes/notifications.d.ts +5 -0
  54. package/dist/routes/signing-authority.d.ts +14 -0
  55. package/dist/services/escrow-enclave/blobIdentity.d.ts +10 -0
  56. package/dist/services/escrow-enclave/blobIdentity.test.d.ts +1 -0
  57. package/dist/services/escrow-enclave/cancellation.d.ts +3 -0
  58. package/dist/services/escrow-enclave/enclaveTrustAnchors.d.ts +1 -0
  59. package/dist/services/escrow-enclave/index.d.ts +16 -0
  60. package/dist/services/escrow-enclave/notifications.d.ts +19 -4
  61. package/dist/services/escrow-enclave/notifications.test.d.ts +1 -0
  62. package/dist/services/escrow-enclave/production.test.d.ts +1 -0
  63. package/dist/services/escrow-enclave/remoteEnclave.contract.test.d.ts +1 -0
  64. package/dist/services/escrow-enclave/remoteEnclave.d.ts +20 -0
  65. package/dist/services/escrow-enclave/remoteEnclave.test.d.ts +1 -0
  66. package/dist/services/escrow-enclave/remoteEnclave.tls.test.d.ts +1 -0
  67. package/dist/services/escrow-enclave/softwareEnclave.d.ts +12 -2
  68. package/dist/services/escrow-enclave/types.d.ts +52 -8
  69. package/dist/types/notifications.d.ts +5 -0
  70. package/esbuildPlugins.cjs +1 -0
  71. package/lambda.ts +27 -117
  72. package/lambdaApp.ts +146 -0
  73. package/oidcLambda.test.ts +38 -0
  74. package/oidcLambda.ts +18 -0
  75. package/oidcLambdaApp.ts +11 -0
  76. package/package.json +22 -13
  77. package/project.json +8 -1
  78. package/scripts/README.md +88 -0
  79. package/scripts/check-seed-encryption-infrastructure.mjs +18 -2
  80. package/scripts/escrow-blob-mode-report.test.ts +227 -0
  81. package/scripts/escrow-blob-mode-report.ts +465 -0
  82. package/scripts/keycloak-admin.ts +108 -0
  83. package/scripts/keycloak-provider-links.ts +147 -0
  84. package/scripts/print-seed-rollout-flags.ts +21 -0
  85. package/scripts/provision-keycloak-users.ts +419 -0
  86. package/seedMigrationApp.test.ts +145 -0
  87. package/seedMigrationApp.ts +50 -0
  88. package/seedMigrationLambda.test.ts +30 -135
  89. package/seedMigrationLambda.ts +15 -46
  90. package/serverless-local.yml +25 -1
  91. package/serverless.function-env.cjs +58 -0
  92. package/serverless.yml +174 -37
  93. package/src/accesslayer/notifications/index.ts +9 -2
  94. package/src/accesslayer/signing-authority/index.ts +4 -1
  95. package/src/app.ts +2 -0
  96. package/src/cache/getDel.test.ts +34 -0
  97. package/src/cache/getDel.ts +32 -0
  98. package/src/cache/index.ts +1 -1
  99. package/src/cache/login-tickets.ts +26 -0
  100. package/src/config/applyDockerStageConfig.ts +6 -0
  101. package/src/config/cacheEnvironment.ts +34 -0
  102. package/src/config/environment.test.ts +45 -2
  103. package/src/config/environment.ts +90 -23
  104. package/src/config/escrowBundleMode.test.ts +52 -0
  105. package/src/config/escrowDeployment.test.ts +114 -0
  106. package/src/config/oidcEnvironment.ts +59 -0
  107. package/src/config/serverlessFunctionEnv.test.ts +98 -0
  108. package/src/config/stageConfig.test.ts +26 -0
  109. package/src/config/stageConfig.ts +24 -0
  110. package/src/docker-entry.ts +9 -0
  111. package/src/helpers/auth.helpers.ts +68 -30
  112. package/src/helpers/escrowCancelLink.test.ts +60 -0
  113. package/src/helpers/escrowCancelLink.ts +38 -0
  114. package/src/helpers/escrowCancelToken.test.ts +105 -0
  115. package/src/helpers/escrowCancelToken.ts +47 -0
  116. package/src/helpers/keycloak.helpers.ts +120 -0
  117. package/src/helpers/login-ticket.helpers.test.ts +45 -0
  118. package/src/helpers/login-ticket.helpers.ts +91 -0
  119. package/src/helpers/oidc.helpers.ts +354 -0
  120. package/src/helpers/rate-limit.helpers.test.ts +61 -0
  121. package/src/helpers/rate-limit.helpers.ts +73 -0
  122. package/src/helpers/social-token.helpers.ts +144 -0
  123. package/src/index.ts +1 -0
  124. package/src/jobs/escrowBlobRewrap.ts +121 -0
  125. package/src/jobs/escrowHoldReminders.ts +177 -0
  126. package/src/models/AuthSubject.ts +147 -0
  127. package/src/models/EscrowHold.ts +321 -5
  128. package/src/models/UserKey.ts +91 -6
  129. package/src/models/index.ts +1 -0
  130. package/src/oidc.ts +371 -0
  131. package/src/routes/auth.ts +173 -0
  132. package/src/routes/escrow.ts +281 -34
  133. package/src/routes/keys.ts +5 -0
  134. package/src/routes/signing-authority.ts +39 -1
  135. package/src/services/delivery/adapters/postmark.adapter.ts +43 -1
  136. package/src/services/escrow-enclave/README.md +81 -10
  137. package/src/services/escrow-enclave/__fixtures__/tls/pinned-ca.pem +11 -0
  138. package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.key +5 -0
  139. package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.pem +12 -0
  140. package/src/services/escrow-enclave/__fixtures__/tls/rogue-ca.pem +11 -0
  141. package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.key +5 -0
  142. package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.pem +12 -0
  143. package/src/services/escrow-enclave/blobIdentity.test.ts +42 -0
  144. package/src/services/escrow-enclave/blobIdentity.ts +55 -0
  145. package/src/services/escrow-enclave/cancellation.ts +59 -0
  146. package/src/services/escrow-enclave/enclaveTrustAnchors.ts +34 -0
  147. package/src/services/escrow-enclave/index.ts +145 -7
  148. package/src/services/escrow-enclave/notifications.test.ts +377 -0
  149. package/src/services/escrow-enclave/notifications.ts +340 -6
  150. package/src/services/escrow-enclave/production.test.ts +34 -0
  151. package/src/services/escrow-enclave/remoteEnclave.contract.test.ts +344 -0
  152. package/src/services/escrow-enclave/remoteEnclave.test.ts +490 -0
  153. package/src/services/escrow-enclave/remoteEnclave.tls.test.ts +62 -0
  154. package/src/services/escrow-enclave/remoteEnclave.ts +263 -0
  155. package/src/services/escrow-enclave/softwareEnclave.test.ts +231 -30
  156. package/src/services/escrow-enclave/softwareEnclave.ts +97 -11
  157. package/src/services/escrow-enclave/types.ts +63 -17
  158. package/test/auth-tickets.spec.ts +480 -0
  159. package/test/helpers/keycloak-broker.ts +181 -0
  160. package/test/helpers/live-mongo.ts +9 -0
  161. package/test/jobs/escrowBlobRewrap.spec.ts +262 -0
  162. package/test/jobs/escrowHoldReminders.spec.ts +319 -0
  163. package/test/keycloak-admin.spec.ts +58 -0
  164. package/test/keycloak-broker-roundtrip.integration.spec.ts +40 -0
  165. package/test/keycloak-migration.integration.spec.ts +266 -0
  166. package/test/keycloak-provider-links.spec.ts +201 -0
  167. package/test/keycloak-social-broker.integration.spec.ts +228 -0
  168. package/test/keycloak-verify.integration.spec.ts +100 -0
  169. package/test/keycloak-verify.spec.ts +307 -0
  170. package/test/keys-escrow.spec.ts +963 -24
  171. package/test/models/authSubjectIndexes.spec.ts +132 -0
  172. package/test/models/escrow.spec.ts +375 -13
  173. package/test/notifications.spec.ts +4 -0
  174. package/test/oidc-e2e/broker.spec.ts +116 -0
  175. package/test/oidc-e2e/compose.yaml +33 -0
  176. package/test/oidc-e2e/fixtures.ts +245 -0
  177. package/test/oidc-e2e/lock.test.mjs +73 -0
  178. package/test/oidc-e2e/playwright.config.ts +21 -0
  179. package/test/oidc-e2e/run.mjs +312 -0
  180. package/test/oidc-e2e/tsconfig.json +10 -0
  181. package/test/oidc-e2e/types.ts +21 -0
  182. package/test/oidc.integration.spec.ts +54 -0
  183. package/test/oidc.spec.ts +697 -0
  184. package/test/oidcLambdaApp.boot.spec.ts +105 -0
  185. package/test/provision-keycloak-users.spec.ts +299 -0
  186. package/test/signing-authority.spec.ts +50 -0
  187. package/tsconfig.build.json +1 -1
  188. package/tsconfig.json +1 -0
  189. package/vitest.config.ts +24 -1
  190. package/vitest.integration.config.ts +20 -3
  191. package/serverless.escrow-env.cjs +0 -20
@@ -1,4 +1,6 @@
1
1
  import { timingSafeEqual } from 'crypto';
2
+ import { escrowBlobIdentity } from './blobIdentity';
3
+ import type { EnclaveCreateHoldInput, EscrowHoldRecord, CancelHoldRequest } from './types';
2
4
  import {
3
5
  decryptEscrowBlob,
4
6
  encryptEscrowBlob,
@@ -18,25 +20,72 @@ import {
18
20
  type ReleaseRequest,
19
21
  type ReleaseResult,
20
22
  type CarryPinVerifierInput,
23
+ type RewrapEscrowBlobInput,
21
24
  } from './types';
22
25
 
23
26
  export interface SoftwareEnclaveConfig {
24
27
  privateKeys: Record<string, string>;
25
28
  activeKeyId: string;
29
+ /** Supplied by the factory to match its configured host-trusted waiting period. */
30
+ holdDurationMs?: number;
26
31
  }
27
32
 
28
33
  /** In-process backend for development; host time is not an attested clock. */
29
34
  export class SoftwareEnclave implements EscrowEnclave {
30
35
  private readonly privateKeys: ReadonlyMap<string, string>;
31
36
  private readonly activeKeyId: string;
32
-
37
+ private readonly holdDurationMs: number;
33
38
  constructor(config: SoftwareEnclaveConfig) {
34
39
  this.privateKeys = new Map(Object.entries(config.privateKeys));
35
40
  this.activeKeyId = config.activeKeyId;
36
41
  if (!this.privateKeys.has(this.activeKeyId)) throw new EscrowUnavailableError();
42
+ // Match the factory configuration without a circular import; default is seven days.
43
+ this.holdDurationMs = config.holdDurationMs ?? 7 * 24 * 60 * 60 * 1000;
37
44
  }
38
-
39
- async getAttestation(): Promise<EnclaveAttestation> {
45
+ async createHold(input: EnclaveCreateHoldInput): Promise<{ holdRecord: EscrowHoldRecord }> {
46
+ const blob = await this.decrypt(input.envelope);
47
+ if (
48
+ blob.did !== input.expectedDid ||
49
+ blob.shareVersion !== input.expectedShareVersion ||
50
+ !Number.isInteger(input.enrollmentEpoch) ||
51
+ input.enrollmentEpoch <= 0 ||
52
+ (input.releasePolicy === 'pin' && !blob.pinVerifier)
53
+ )
54
+ throw new EscrowPolicyError();
55
+ const blobHash = escrowBlobIdentity(input.envelope);
56
+ const now = Date.now();
57
+ return {
58
+ holdRecord: {
59
+ hold: {
60
+ holdId: input.holdId,
61
+ did: blob.did,
62
+ shareVersion: input.expectedShareVersion,
63
+ blobHash,
64
+ enrollmentEpoch: input.enrollmentEpoch,
65
+ releasePolicy: input.releasePolicy,
66
+ clientEphemeralPublicKey: input.clientEphemeralPublicKey,
67
+ createdLo: now,
68
+ createdHi: now,
69
+ policyVersion: 1,
70
+ // Non-cryptographic placeholder: software mode trusts the host entirely.
71
+ signature: 'software-mode-unsigned',
72
+ },
73
+ holdDurationMs: input.releasePolicy === 'pin' ? 0 : this.holdDurationMs,
74
+ ledgerSeq: 0,
75
+ },
76
+ };
77
+ }
78
+ async cancelHold(input: CancelHoldRequest): Promise<void> {
79
+ const blob = await this.decrypt(input.envelope);
80
+ if (
81
+ blob.did !== input.expectedDid ||
82
+ blob.did !== input.hold.hold.did ||
83
+ input.clientEphemeralPublicKey !== input.hold.hold.clientEphemeralPublicKey
84
+ )
85
+ throw new EscrowPolicyError();
86
+ // Development only: Mongo owns cancellation; there is no software enclave ledger.
87
+ }
88
+ async getAttestation(_nonce?: Uint8Array): Promise<EnclaveAttestation> {
40
89
  try {
41
90
  const subtle = globalThis.crypto.subtle;
42
91
  const key = await subtle.importKey(
@@ -62,6 +111,12 @@ export class SoftwareEnclave implements EscrowEnclave {
62
111
  const description = {
63
112
  mode: 'software' as const,
64
113
  keyId: this.activeKeyId,
114
+ // P9.1 software-enclave equivalent: every OTHER configured key is
115
+ // already decrypt-eligible in `decrypt()` below (keyed lookup by
116
+ // envelope keyId, not restricted to `activeKeyId`) — this just
117
+ // advertises that existing behaviour the same way the Nitro
118
+ // backend advertises its `ESCROW_PREVIOUS_KEY_IDS`.
119
+ previousKeyIds: [...this.privateKeys.keys()].filter(id => id !== this.activeKeyId),
65
120
  publicKey,
66
121
  issuedAt: new Date().toISOString(),
67
122
  };
@@ -98,7 +153,17 @@ export class SoftwareEnclave implements EscrowEnclave {
98
153
  return { ok: true, hasPin: !!blob.pinVerifier };
99
154
  }
100
155
 
101
- /** Transfer only the verifier; recovery material remains sealed within the enclave. */
156
+ /**
157
+ * Transfer only the verifier; recovery material remains sealed within the enclave.
158
+ *
159
+ * `input.sourceEnrollmentEpoch` is intentionally unused: software mode has no
160
+ * enclave ledger to reset in the first place, so there is no epoch-scoped PIN
161
+ * attempt budget to carry. The PIN attempt budget here is `escrowPin.failedAttempts`
162
+ * / `verifiedFailedAttempts` in MongoDB (see routes/escrow.ts), which the enroll
163
+ * route already copies forward unchanged on every carry, independent of
164
+ * `enrollmentEpoch` — so software mode never had the epoch-reset bug this field
165
+ * exists to fix in remote/nitro mode.
166
+ */
102
167
  async carryPinVerifier(input: CarryPinVerifierInput) {
103
168
  const source = await this.decrypt(input.sourceEnvelope);
104
169
  const target = await this.decrypt(input.targetEnvelope);
@@ -122,19 +187,40 @@ export class SoftwareEnclave implements EscrowEnclave {
122
187
  };
123
188
  }
124
189
 
190
+ /**
191
+ * Software-mode equivalent of the Nitro enclave's `rewrap_escrow_blob`
192
+ * (P9.3): moves a blob sealed under any OTHER configured key onto the
193
+ * active key, unchanged otherwise. Mirrors its refusal shape — an
194
+ * envelope already under `activeKeyId` has nothing to migrate, and
195
+ * `decrypt()` already throws `EscrowBlobError` for a keyId this backend
196
+ * was never configured with, so both collapse into the same generic
197
+ * error. `sourceEnrollmentEpoch` is unused for the same reason
198
+ * `carryPinVerifier` ignores it above: no enclave ledger here, so no
199
+ * epoch-scoped PIN attempt budget to carry.
200
+ */
201
+ async rewrapEscrowBlob(input: RewrapEscrowBlobInput) {
202
+ const parsed = parseEscrowEnvelope(input.envelope);
203
+ if (parsed.keyId === this.activeKeyId) throw new EscrowBlobError();
204
+ const blob = await this.decrypt(input.envelope);
205
+ if (blob.did !== input.expectedDid || blob.shareVersion !== input.expectedShareVersion)
206
+ throw new EscrowBlobError();
207
+ const attestation = await this.getAttestation();
208
+ return {
209
+ envelope: await encryptEscrowBlob(blob, attestation.publicKey, attestation.keyId),
210
+ };
211
+ }
212
+
125
213
  async releaseEscrow(input: ReleaseRequest): Promise<ReleaseResult> {
126
- const { hold } = input;
214
+ const { hold } = input.hold;
127
215
  if (
128
- hold.status !== 'pending' ||
129
216
  input.clientEphemeralPublicKey !== hold.clientEphemeralPublicKey ||
130
- !((input.now ?? new Date()).getTime() >= hold.releaseAfter.getTime())
131
- ) {
217
+ !((input.now ?? new Date()).getTime() >= hold.createdHi + input.hold.holdDurationMs)
218
+ )
132
219
  throw new EscrowPolicyError();
133
- }
134
220
  const blob = await this.decrypt(input.envelope);
135
221
  if (
136
222
  blob.did !== input.expectedDid ||
137
- blob.did !== hold.primaryDid ||
223
+ blob.did !== hold.did ||
138
224
  blob.shareVersion !== hold.shareVersion
139
225
  )
140
226
  throw new EscrowPolicyError();
@@ -156,7 +242,7 @@ export class SoftwareEnclave implements EscrowEnclave {
156
242
  recoveryShare: blob.recoveryShare,
157
243
  did: blob.did,
158
244
  shareVersion: blob.shareVersion,
159
- holdId: hold._id,
245
+ holdId: hold.holdId,
160
246
  },
161
247
  input.clientEphemeralPublicKey
162
248
  ),
@@ -1,36 +1,60 @@
1
1
  import type { EscrowEnvelope } from '@learncard/sss-key-manager';
2
- import type { EscrowHold } from '../../models/EscrowHold';
2
+ // Hold records are independent of host Mongo status and deadlines.
3
3
 
4
4
  export interface EnclaveAttestation {
5
5
  mode: 'software' | 'nitro';
6
6
  keyId: string;
7
+ /** P9.1: retired keyIds the enclave still accepts for decrypt only (never
8
+ * for sealing output). Empty/absent means no previous key is configured. */
9
+ previousKeyIds?: string[];
7
10
  publicKey: string;
8
11
  measurements: { imageSha384?: string; pcr0?: string; pcr1?: string; pcr2?: string };
9
12
  document: string;
10
13
  issuedAt: string;
11
14
  }
12
- export type EscrowHoldForEnclave = Pick<
13
- EscrowHold,
14
- | '_id'
15
- | 'status'
16
- | 'releaseAfter'
17
- | 'releasePolicy'
18
- | 'primaryDid'
19
- | 'shareVersion'
20
- | 'clientEphemeralPublicKey'
21
- >;
22
- export interface VerifyEscrowBlobInput {
15
+ export interface EscrowHoldRecord {
16
+ [key: string]: unknown;
17
+ hold: {
18
+ [key: string]: unknown;
19
+ holdId: string;
20
+ did: string;
21
+ shareVersion: number;
22
+ blobHash: string;
23
+ enrollmentEpoch: number;
24
+ releasePolicy: 'hold' | 'pin';
25
+ clientEphemeralPublicKey: string;
26
+ createdLo: number;
27
+ createdHi: number;
28
+ policyVersion: number;
29
+ signature: string;
30
+ };
31
+ holdDurationMs: number;
32
+ ledgerSeq: number;
33
+ }
34
+ export interface EnclaveCreateHoldInput {
23
35
  envelope: EscrowEnvelope;
36
+ holdId: string;
24
37
  expectedDid: string;
25
38
  expectedShareVersion: number;
39
+ enrollmentEpoch: number;
40
+ releasePolicy: 'hold' | 'pin';
41
+ clientEphemeralPublicKey: string;
26
42
  }
27
- export type VerifyEscrowBlobResult =
28
- { ok: true; hasPin: boolean } | { ok: false; hasPin: boolean; reason: string };
29
- export interface ReleaseRequest {
43
+ export interface CancelHoldRequest {
30
44
  envelope: EscrowEnvelope;
31
- hold: EscrowHoldForEnclave;
45
+ hold: EscrowHoldRecord;
32
46
  clientEphemeralPublicKey: string;
33
47
  expectedDid: string;
48
+ }
49
+ export interface VerifyEscrowBlobInput {
50
+ envelope: EscrowEnvelope;
51
+ expectedDid: string;
52
+ expectedShareVersion: number;
53
+ }
54
+ export type VerifyEscrowBlobResult =
55
+ { ok: true; hasPin: boolean } | { ok: false; hasPin: boolean; reason: string };
56
+ export interface ReleaseRequest extends CancelHoldRequest {
57
+ /** Development-only host clock; never forwarded by the remote backend. */
34
58
  now?: Date;
35
59
  pinProof?: string;
36
60
  }
@@ -43,12 +67,34 @@ export interface CarryPinVerifierInput {
43
67
  expectedDid: string;
44
68
  sourceShareVersion: number;
45
69
  targetShareVersion: number;
70
+ /** Signed source-record binding; blob hash, not epoch, selects the chain. */
71
+ sourceEnrollmentEpoch: number;
72
+ /** Signed binding for the output blob's first Carried record. */
73
+ targetEnrollmentEpoch: number;
74
+ }
75
+ export interface RewrapEscrowBlobInput {
76
+ envelope: EscrowEnvelope;
77
+ expectedDid: string;
78
+ expectedShareVersion: number;
79
+ /** Source and destination record binding. Rewrap preserves this epoch. */
80
+ sourceEnrollmentEpoch: number;
46
81
  }
47
82
  export interface EscrowEnclave {
48
- getAttestation(): Promise<EnclaveAttestation>;
83
+ /** `nonce` binds a nitro attestation to one client-generated challenge (64 hex chars,
84
+ * decoded to bytes); the software backend has no freshness story and ignores it. */
85
+ getAttestation(nonce?: Uint8Array): Promise<EnclaveAttestation>;
49
86
  verifyEscrowBlob(input: VerifyEscrowBlobInput): Promise<VerifyEscrowBlobResult>;
50
87
  carryPinVerifier(input: CarryPinVerifierInput): Promise<{ envelope: EscrowEnvelope }>;
88
+ /** P9.3: migrates a copy sealed under a recognised PREVIOUS key onto the
89
+ * CURRENT key (same did/shareVersion/recoveryShare/pinVerifier), so a
90
+ * previous key can eventually be retired. Refuses (generic
91
+ * `EscrowBlobError`) unless the envelope's own keyId is a recognised
92
+ * previous key — never the current key (nothing to migrate) or an
93
+ * unknown one. */
94
+ rewrapEscrowBlob(input: RewrapEscrowBlobInput): Promise<{ envelope: EscrowEnvelope }>;
51
95
  releaseEscrow(input: ReleaseRequest): Promise<ReleaseResult>;
96
+ createHold(input: EnclaveCreateHoldInput): Promise<{ holdRecord: EscrowHoldRecord }>;
97
+ cancelHold(input: CancelHoldRequest): Promise<void>;
52
98
  }
53
99
  export class EscrowPolicyError extends Error {
54
100
  constructor() {