@learncard/lca-api-service 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +26 -2
- package/AGENTS.md +59 -44
- package/CHANGELOG.md +41 -0
- package/compose.env +5 -0
- package/dist/accesslayer/signing-authority/index.d.ts +8 -2
- package/dist/app.d.ts +297 -8
- package/dist/cache/challenges.d.ts +2 -0
- package/dist/cache/recoverySessions.d.ts +42 -0
- package/dist/config/environment.d.ts +152 -2
- package/dist/config/environment.test.d.ts +1 -0
- package/dist/helpers/learnCard.signingAuthority.test.d.ts +1 -0
- package/dist/helpers/rateLimit.helpers.d.ts +4 -0
- package/dist/helpers/seedEncryption.helpers.d.ts +23 -0
- package/dist/helpers/seedEncryption.helpers.test.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/migrations/signingAuthoritySeeds.d.ts +13 -0
- package/dist/migrations/signingAuthoritySeedsCli.test.d.ts +1 -0
- package/dist/models/EscrowHold.d.ts +75 -0
- package/dist/models/SigningAuthority.d.ts +7 -1
- package/dist/models/UserKey.d.ts +316 -3
- package/dist/models/UserKey.escrowPin.test.d.ts +1 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/routes/escrow.d.ts +150 -0
- package/dist/routes/index.d.ts +6 -0
- package/dist/routes/keys.d.ts +181 -7
- package/dist/routes/qr-login.d.ts +1 -1
- package/dist/services/escrow-enclave/index.d.ts +9 -0
- package/dist/services/escrow-enclave/notifications.d.ts +9 -0
- package/dist/services/escrow-enclave/softwareEnclave.d.ts +17 -0
- package/dist/services/escrow-enclave/softwareEnclave.test.d.ts +1 -0
- package/dist/services/escrow-enclave/types.d.ts +67 -0
- package/dist/types/seed-encryption.d.ts +31 -0
- package/dist/types/seed-migration.d.ts +49 -0
- package/lambda.ts +8 -3
- package/package.json +17 -11
- package/scripts/check-seed-encryption-infrastructure.mjs +123 -0
- package/scripts/check-signing-authority-redis.mjs +93 -0
- package/scripts/migrate-signing-authority-seeds-local.sh +51 -0
- package/scripts/migrate-signing-authority-seeds.mjs +105 -0
- package/seedMigrationLambda.test.ts +145 -0
- package/seedMigrationLambda.ts +50 -0
- package/serverless-local.yml +3 -0
- package/serverless.escrow-env.cjs +20 -0
- package/serverless.yml +133 -0
- package/src/accesslayer/signing-authority/create.ts +11 -7
- package/src/app.ts +2 -0
- package/src/cache/challenges.ts +12 -2
- package/src/cache/in-memory-lru.ts +1 -0
- package/src/cache/recoverySessions.ts +252 -0
- package/src/config/environment.test.ts +116 -0
- package/src/config/environment.ts +97 -0
- package/src/docker-entry.ts +4 -0
- package/src/helpers/learnCard.helpers.test.ts +11 -1
- package/src/helpers/learnCard.helpers.ts +57 -45
- package/src/helpers/learnCard.signingAuthority.test.ts +194 -0
- package/src/helpers/rateLimit.helpers.ts +112 -0
- package/src/helpers/seedEncryption.helpers.test.ts +427 -0
- package/src/helpers/seedEncryption.helpers.ts +372 -0
- package/src/helpers/signingAuthority.helpers.ts +4 -1
- package/src/migrations/signingAuthoritySeeds.ts +430 -0
- package/src/migrations/signingAuthoritySeedsCli.test.ts +70 -0
- package/src/models/EscrowHold.ts +225 -0
- package/src/models/SigningAuthority.ts +14 -3
- package/src/models/UserKey.escrowPin.test.ts +49 -0
- package/src/models/UserKey.ts +1024 -40
- package/src/models/index.ts +1 -0
- package/src/models/pruneOrphanedRecoveryMethods.ts +3 -1
- package/src/routes/credentials.ts +8 -0
- package/src/routes/escrow.ts +826 -0
- package/src/routes/firebase.ts +90 -48
- package/src/routes/index.ts +98 -5
- package/src/routes/keys.ts +1017 -174
- package/src/routes/qr-login.ts +51 -53
- package/src/services/delivery/adapters/postmark.adapter.ts +5 -0
- package/src/services/escrow-enclave/README.md +51 -0
- package/src/services/escrow-enclave/index.ts +36 -0
- package/src/services/escrow-enclave/notifications.ts +14 -0
- package/src/services/escrow-enclave/softwareEnclave.test.ts +249 -0
- package/src/services/escrow-enclave/softwareEnclave.ts +168 -0
- package/src/services/escrow-enclave/types.ts +76 -0
- package/src/types/seed-encryption.ts +51 -0
- package/src/types/seed-migration.ts +61 -0
- package/test/credentials.spec.ts +2 -0
- package/test/keys-escrow.spec.ts +1389 -0
- package/test/keys-identity-rebind.spec.ts +456 -0
- package/test/keys-lifecycle-invariants.spec.ts +785 -0
- package/test/keys-recovery-confirmation.spec.ts +586 -0
- package/test/keys-recovery-rate-limit.spec.ts +123 -0
- package/test/keys-secret-leakage.spec.ts +265 -0
- package/test/models/escrow.spec.ts +210 -0
- package/test/signing-authority-http.spec.ts +161 -0
- package/test/signing-authority-seed-migration.spec.ts +414 -0
- package/test/signing-authority.spec.ts +40 -0
- package/vitest.config.ts +1 -1
- package/vitest.integration.config.ts +5 -0
- package/dist/cache/signing-authority.d.ts +0 -6
- package/src/cache/signing-authority.ts +0 -37
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'crypto';
|
|
2
|
+
import {
|
|
3
|
+
decryptEscrowBlob,
|
|
4
|
+
encryptEscrowBlob,
|
|
5
|
+
sealEscrowRelease,
|
|
6
|
+
parseEscrowEnvelope,
|
|
7
|
+
type EscrowBlobPlaintext,
|
|
8
|
+
} from '@learncard/sss-key-manager';
|
|
9
|
+
import {
|
|
10
|
+
EscrowBlobError,
|
|
11
|
+
EscrowPolicyError,
|
|
12
|
+
EscrowPinMismatchError,
|
|
13
|
+
EscrowUnavailableError,
|
|
14
|
+
type EscrowEnclave,
|
|
15
|
+
type EnclaveAttestation,
|
|
16
|
+
type VerifyEscrowBlobInput,
|
|
17
|
+
type VerifyEscrowBlobResult,
|
|
18
|
+
type ReleaseRequest,
|
|
19
|
+
type ReleaseResult,
|
|
20
|
+
type CarryPinVerifierInput,
|
|
21
|
+
} from './types';
|
|
22
|
+
|
|
23
|
+
export interface SoftwareEnclaveConfig {
|
|
24
|
+
privateKeys: Record<string, string>;
|
|
25
|
+
activeKeyId: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** In-process backend for development; host time is not an attested clock. */
|
|
29
|
+
export class SoftwareEnclave implements EscrowEnclave {
|
|
30
|
+
private readonly privateKeys: ReadonlyMap<string, string>;
|
|
31
|
+
private readonly activeKeyId: string;
|
|
32
|
+
|
|
33
|
+
constructor(config: SoftwareEnclaveConfig) {
|
|
34
|
+
this.privateKeys = new Map(Object.entries(config.privateKeys));
|
|
35
|
+
this.activeKeyId = config.activeKeyId;
|
|
36
|
+
if (!this.privateKeys.has(this.activeKeyId)) throw new EscrowUnavailableError();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getAttestation(): Promise<EnclaveAttestation> {
|
|
40
|
+
try {
|
|
41
|
+
const subtle = globalThis.crypto.subtle;
|
|
42
|
+
const key = await subtle.importKey(
|
|
43
|
+
'pkcs8',
|
|
44
|
+
new Uint8Array(Buffer.from(this.privateKeys.get(this.activeKeyId)!, 'base64')),
|
|
45
|
+
{ name: 'ECDH', namedCurve: 'P-256' },
|
|
46
|
+
true,
|
|
47
|
+
['deriveBits']
|
|
48
|
+
);
|
|
49
|
+
// WebCrypto cannot export a private CryptoKey as SPKI. Export its public
|
|
50
|
+
// coordinates as JWK, import a public-only key, then export that as SPKI.
|
|
51
|
+
const jwk = await subtle.exportKey('jwk', key);
|
|
52
|
+
const publicKeyObject = await subtle.importKey(
|
|
53
|
+
'jwk',
|
|
54
|
+
{ kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y, ext: true },
|
|
55
|
+
{ name: 'ECDH', namedCurve: 'P-256' },
|
|
56
|
+
true,
|
|
57
|
+
[]
|
|
58
|
+
);
|
|
59
|
+
const publicKey = Buffer.from(await subtle.exportKey('spki', publicKeyObject)).toString(
|
|
60
|
+
'base64'
|
|
61
|
+
);
|
|
62
|
+
const description = {
|
|
63
|
+
mode: 'software' as const,
|
|
64
|
+
keyId: this.activeKeyId,
|
|
65
|
+
publicKey,
|
|
66
|
+
issuedAt: new Date().toISOString(),
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
...description,
|
|
70
|
+
measurements: {},
|
|
71
|
+
document: Buffer.from(JSON.stringify(description)).toString('base64'),
|
|
72
|
+
};
|
|
73
|
+
} catch {
|
|
74
|
+
throw new EscrowUnavailableError();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async decrypt(envelope: unknown): Promise<EscrowBlobPlaintext> {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = parseEscrowEnvelope(envelope);
|
|
81
|
+
const key = this.privateKeys.get(parsed.keyId);
|
|
82
|
+
if (!key) throw new EscrowBlobError();
|
|
83
|
+
return await decryptEscrowBlob(parsed, key);
|
|
84
|
+
} catch {
|
|
85
|
+
throw new EscrowBlobError();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async verifyEscrowBlob(input: VerifyEscrowBlobInput): Promise<VerifyEscrowBlobResult> {
|
|
90
|
+
const blob = await this.decrypt(input.envelope);
|
|
91
|
+
if (blob.did !== input.expectedDid || blob.shareVersion !== input.expectedShareVersion) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
hasPin: !!blob.pinVerifier,
|
|
95
|
+
reason: 'Escrow recovery is not permitted.',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return { ok: true, hasPin: !!blob.pinVerifier };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Transfer only the verifier; recovery material remains sealed within the enclave. */
|
|
102
|
+
async carryPinVerifier(input: CarryPinVerifierInput) {
|
|
103
|
+
const source = await this.decrypt(input.sourceEnvelope);
|
|
104
|
+
const target = await this.decrypt(input.targetEnvelope);
|
|
105
|
+
if (
|
|
106
|
+
source.did !== input.expectedDid ||
|
|
107
|
+
target.did !== input.expectedDid ||
|
|
108
|
+
source.shareVersion !== input.sourceShareVersion ||
|
|
109
|
+
target.shareVersion !== input.targetShareVersion ||
|
|
110
|
+
target.shareVersion <= source.shareVersion ||
|
|
111
|
+
!source.pinVerifier ||
|
|
112
|
+
target.pinVerifier
|
|
113
|
+
)
|
|
114
|
+
throw new EscrowBlobError();
|
|
115
|
+
const attestation = await this.getAttestation();
|
|
116
|
+
return {
|
|
117
|
+
envelope: await encryptEscrowBlob(
|
|
118
|
+
{ ...target, pinVerifier: source.pinVerifier },
|
|
119
|
+
attestation.publicKey,
|
|
120
|
+
attestation.keyId
|
|
121
|
+
),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async releaseEscrow(input: ReleaseRequest): Promise<ReleaseResult> {
|
|
126
|
+
const { hold } = input;
|
|
127
|
+
if (
|
|
128
|
+
hold.status !== 'pending' ||
|
|
129
|
+
input.clientEphemeralPublicKey !== hold.clientEphemeralPublicKey ||
|
|
130
|
+
!((input.now ?? new Date()).getTime() >= hold.releaseAfter.getTime())
|
|
131
|
+
) {
|
|
132
|
+
throw new EscrowPolicyError();
|
|
133
|
+
}
|
|
134
|
+
const blob = await this.decrypt(input.envelope);
|
|
135
|
+
if (
|
|
136
|
+
blob.did !== input.expectedDid ||
|
|
137
|
+
blob.did !== hold.primaryDid ||
|
|
138
|
+
blob.shareVersion !== hold.shareVersion
|
|
139
|
+
)
|
|
140
|
+
throw new EscrowPolicyError();
|
|
141
|
+
if (hold.releasePolicy === 'pin') {
|
|
142
|
+
if (!blob.pinVerifier || !input.pinProof) throw new EscrowPolicyError();
|
|
143
|
+
const expected = Buffer.from(blob.pinVerifier, 'hex');
|
|
144
|
+
const actual = Buffer.from(input.pinProof, 'hex');
|
|
145
|
+
if (
|
|
146
|
+
!/^[0-9a-f]{64}$/i.test(input.pinProof) ||
|
|
147
|
+
actual.length !== expected.length ||
|
|
148
|
+
!timingSafeEqual(actual, expected)
|
|
149
|
+
)
|
|
150
|
+
throw new EscrowPinMismatchError();
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
return {
|
|
154
|
+
sealed: await sealEscrowRelease(
|
|
155
|
+
{
|
|
156
|
+
recoveryShare: blob.recoveryShare,
|
|
157
|
+
did: blob.did,
|
|
158
|
+
shareVersion: blob.shareVersion,
|
|
159
|
+
holdId: hold._id,
|
|
160
|
+
},
|
|
161
|
+
input.clientEphemeralPublicKey
|
|
162
|
+
),
|
|
163
|
+
};
|
|
164
|
+
} catch {
|
|
165
|
+
throw new EscrowBlobError();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { EscrowEnvelope } from '@learncard/sss-key-manager';
|
|
2
|
+
import type { EscrowHold } from '../../models/EscrowHold';
|
|
3
|
+
|
|
4
|
+
export interface EnclaveAttestation {
|
|
5
|
+
mode: 'software' | 'nitro';
|
|
6
|
+
keyId: string;
|
|
7
|
+
publicKey: string;
|
|
8
|
+
measurements: { imageSha384?: string; pcr0?: string; pcr1?: string; pcr2?: string };
|
|
9
|
+
document: string;
|
|
10
|
+
issuedAt: string;
|
|
11
|
+
}
|
|
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 {
|
|
23
|
+
envelope: EscrowEnvelope;
|
|
24
|
+
expectedDid: string;
|
|
25
|
+
expectedShareVersion: number;
|
|
26
|
+
}
|
|
27
|
+
export type VerifyEscrowBlobResult =
|
|
28
|
+
{ ok: true; hasPin: boolean } | { ok: false; hasPin: boolean; reason: string };
|
|
29
|
+
export interface ReleaseRequest {
|
|
30
|
+
envelope: EscrowEnvelope;
|
|
31
|
+
hold: EscrowHoldForEnclave;
|
|
32
|
+
clientEphemeralPublicKey: string;
|
|
33
|
+
expectedDid: string;
|
|
34
|
+
now?: Date;
|
|
35
|
+
pinProof?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ReleaseResult {
|
|
38
|
+
sealed: EscrowEnvelope;
|
|
39
|
+
}
|
|
40
|
+
export interface CarryPinVerifierInput {
|
|
41
|
+
sourceEnvelope: EscrowEnvelope;
|
|
42
|
+
targetEnvelope: EscrowEnvelope;
|
|
43
|
+
expectedDid: string;
|
|
44
|
+
sourceShareVersion: number;
|
|
45
|
+
targetShareVersion: number;
|
|
46
|
+
}
|
|
47
|
+
export interface EscrowEnclave {
|
|
48
|
+
getAttestation(): Promise<EnclaveAttestation>;
|
|
49
|
+
verifyEscrowBlob(input: VerifyEscrowBlobInput): Promise<VerifyEscrowBlobResult>;
|
|
50
|
+
carryPinVerifier(input: CarryPinVerifierInput): Promise<{ envelope: EscrowEnvelope }>;
|
|
51
|
+
releaseEscrow(input: ReleaseRequest): Promise<ReleaseResult>;
|
|
52
|
+
}
|
|
53
|
+
export class EscrowPolicyError extends Error {
|
|
54
|
+
constructor() {
|
|
55
|
+
super('Escrow recovery is not permitted.');
|
|
56
|
+
this.name = 'EscrowPolicyError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export class EscrowPinMismatchError extends Error {
|
|
60
|
+
constructor() {
|
|
61
|
+
super('Incorrect PIN.');
|
|
62
|
+
this.name = 'EscrowPinMismatchError';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export class EscrowBlobError extends Error {
|
|
66
|
+
constructor() {
|
|
67
|
+
super('Invalid escrow payload.');
|
|
68
|
+
this.name = 'EscrowBlobError';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export class EscrowUnavailableError extends Error {
|
|
72
|
+
constructor() {
|
|
73
|
+
super('Escrow recovery is not available.');
|
|
74
|
+
this.name = 'EscrowUnavailableError';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface SigningAuthoritySeedIdentity {
|
|
2
|
+
_id: string;
|
|
3
|
+
ownerDid: string;
|
|
4
|
+
name: string;
|
|
5
|
+
did?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface EncryptedSigningAuthoritySeed {
|
|
9
|
+
encryptedSeed: string;
|
|
10
|
+
encryptedDek: string;
|
|
11
|
+
keyVersion: 'kms-v1' | 'local-v1';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Untrusted persisted fields are validated before use, including on cache hits. */
|
|
15
|
+
export interface SigningAuthoritySeedRecord {
|
|
16
|
+
_id?: unknown;
|
|
17
|
+
ownerDid: string;
|
|
18
|
+
name: string;
|
|
19
|
+
did?: string;
|
|
20
|
+
seed?: string;
|
|
21
|
+
encryptedSeed?: unknown;
|
|
22
|
+
encryptedDek?: unknown;
|
|
23
|
+
keyVersion?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SeedEncryptionConfig {
|
|
27
|
+
kmsKeyArn?: string;
|
|
28
|
+
localKek?: string;
|
|
29
|
+
allowLocal: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SeedEncryption {
|
|
33
|
+
encrypt: (
|
|
34
|
+
seed: string,
|
|
35
|
+
identity: SigningAuthoritySeedIdentity
|
|
36
|
+
) => Promise<EncryptedSigningAuthoritySeed>;
|
|
37
|
+
decrypt: (
|
|
38
|
+
envelope: EncryptedSigningAuthoritySeed,
|
|
39
|
+
identity: SigningAuthoritySeedIdentity
|
|
40
|
+
) => Promise<string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type SeedEncryptionFailure =
|
|
44
|
+
| 'invalid_record'
|
|
45
|
+
| 'invalid_envelope'
|
|
46
|
+
| 'configuration'
|
|
47
|
+
| 'legacy_disabled'
|
|
48
|
+
| 'authentication_failed'
|
|
49
|
+
| 'kms_access_denied'
|
|
50
|
+
| 'kms_unavailable'
|
|
51
|
+
| 'kms_failed';
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Document, ObjectId } from 'mongodb';
|
|
2
|
+
import type { EncryptedSigningAuthoritySeed } from './seed-encryption';
|
|
3
|
+
|
|
4
|
+
export type SeedMigrationPhase = 'dry-run' | 'prepare' | 'verify' | 'purge';
|
|
5
|
+
|
|
6
|
+
export interface SeedMigrationRequest {
|
|
7
|
+
phase: SeedMigrationPhase;
|
|
8
|
+
batchSize?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SeedMigrationCounts {
|
|
12
|
+
total: number;
|
|
13
|
+
encrypted: number;
|
|
14
|
+
legacyOnly: number;
|
|
15
|
+
malformed: number;
|
|
16
|
+
plaintextRemaining: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SeedMigrationResult {
|
|
20
|
+
phase: SeedMigrationPhase;
|
|
21
|
+
done: boolean;
|
|
22
|
+
processed: number;
|
|
23
|
+
counts: SeedMigrationCounts;
|
|
24
|
+
/** False for an intermediate batch: counts are the last full reconciliation snapshot. */
|
|
25
|
+
countsReconciled: boolean;
|
|
26
|
+
/** A final sweep found work inserted/changed behind the saved cursor. */
|
|
27
|
+
rescanRequired?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Read the historical Mongo shape without coercing BSON IDs or changing metadata. */
|
|
31
|
+
export interface MigrationSigningAuthority extends Document {
|
|
32
|
+
_id: string | ObjectId;
|
|
33
|
+
ownerDid: string;
|
|
34
|
+
name: string;
|
|
35
|
+
did?: string;
|
|
36
|
+
seed?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface SeedMigrationState {
|
|
40
|
+
_id: string;
|
|
41
|
+
leaseOwner?: string;
|
|
42
|
+
leaseExpiresAt?: Date;
|
|
43
|
+
phase?: SeedMigrationPhase;
|
|
44
|
+
epoch?: string;
|
|
45
|
+
verifiedEpoch?: string;
|
|
46
|
+
status?: 'running' | 'complete' | 'failed';
|
|
47
|
+
processed?: number;
|
|
48
|
+
counts?: SeedMigrationCounts;
|
|
49
|
+
updatedAt?: Date;
|
|
50
|
+
/** Last processed BSON ID, scoped to phase/epoch; final reconciliation rechecks earlier IDs. */
|
|
51
|
+
cursor?: string | ObjectId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Ciphertext-only receipt: exact comparisons make changed rows pending during the final sweep. */
|
|
55
|
+
export interface SeedMigrationReceipt extends EncryptedSigningAuthoritySeed {
|
|
56
|
+
_id: string | ObjectId;
|
|
57
|
+
epoch: string;
|
|
58
|
+
ownerDid: string;
|
|
59
|
+
name: string;
|
|
60
|
+
did: string | null;
|
|
61
|
+
}
|
package/test/credentials.spec.ts
CHANGED
|
@@ -95,6 +95,8 @@ describe('Credentials', () => {
|
|
|
95
95
|
})
|
|
96
96
|
);
|
|
97
97
|
expect(proof).toBeDefined();
|
|
98
|
+
const verification = await userA.learnCard.invoke.verifyCredential(signedVc);
|
|
99
|
+
expect(verification.errors).toHaveLength(0);
|
|
98
100
|
});
|
|
99
101
|
|
|
100
102
|
it('rejects an unresolvable encryption recipient as BAD_REQUEST', async () => {
|