@learncard/network-brain-service 3.17.0 → 3.19.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 +4 -0
- package/AGENTS.md +177 -159
- package/CHANGELOG.md +148 -0
- package/README.md +26 -8
- package/compose.inbox.yml +7 -0
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/auth-grant/read.d.ts +1 -0
- package/dist/accesslayer/auth-grant/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +3 -2
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +5 -3
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential/create.d.ts +2 -1
- package/dist/accesslayer/credential/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/create.test.d.ts +2 -0
- package/dist/accesslayer/credential/create.test.d.ts.map +1 -0
- package/dist/accesslayer/credential/relationships/read.d.ts +1 -0
- package/dist/accesslayer/credential/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/credential-activity/create.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/email-delivery.d.ts +18 -0
- package/dist/accesslayer/credential-refresh/email-delivery.d.ts.map +1 -0
- package/dist/accesslayer/credential-refresh/index.d.ts +1 -0
- package/dist/accesslayer/credential-refresh/index.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/read.d.ts.map +1 -1
- package/dist/accesslayer/credential-refresh/update.d.ts.map +1 -1
- package/dist/accesslayer/inbox-batch/store.d.ts +33 -0
- package/dist/accesslayer/inbox-batch/store.d.ts.map +1 -0
- package/dist/accesslayer/inbox-credential/create.d.ts +5 -0
- package/dist/accesslayer/inbox-credential/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/refresh.d.ts +71 -0
- package/dist/accesslayer/inbox-credential/refresh.d.ts.map +1 -0
- package/dist/accesslayer/inbox-credential/update.d.ts +1 -1
- package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
- package/dist/cache/atomic-counter.test-helpers.d.ts +3 -0
- package/dist/cache/atomic-counter.test-helpers.d.ts.map +1 -0
- package/dist/cache/atomic-counter.test.d.ts +2 -0
- package/dist/cache/atomic-counter.test.d.ts.map +1 -0
- package/dist/cache/index.d.ts +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/config/environment.d.ts +46 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/credential-refresh.d.ts.map +1 -1
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/helpers/activity.helpers.d.ts +4 -0
- package/dist/helpers/activity.helpers.d.ts.map +1 -1
- package/dist/helpers/boost-storage.helpers.test.d.ts +2 -0
- package/dist/helpers/boost-storage.helpers.test.d.ts.map +1 -0
- package/dist/helpers/boost.helpers.d.ts +3 -7
- package/dist/helpers/boost.helpers.d.ts.map +1 -1
- package/dist/helpers/contact-method.helpers.d.ts.map +1 -1
- package/dist/helpers/contact-method.helpers.test.d.ts +2 -0
- package/dist/helpers/contact-method.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email-content.helpers.d.ts +44 -0
- package/dist/helpers/credential-refresh-email-content.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-refresh-email-content.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts +17 -0
- package/dist/helpers/credential-refresh-email.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-proof.helpers.d.ts +4 -0
- package/dist/helpers/credential-refresh-proof.helpers.d.ts.map +1 -0
- package/dist/helpers/credential-refresh-proof.helpers.test.d.ts +2 -0
- package/dist/helpers/credential-refresh-proof.helpers.test.d.ts.map +1 -0
- package/dist/helpers/credential-refresh.helpers.d.ts +45 -2
- package/dist/helpers/credential-refresh.helpers.d.ts.map +1 -1
- package/dist/helpers/credential.helpers.d.ts +4 -2
- package/dist/helpers/credential.helpers.d.ts.map +1 -1
- package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox-batch-http.helpers.d.ts +15 -0
- package/dist/helpers/inbox-batch-http.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch-http.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-batch-http.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-batch-status.helpers.d.ts +3 -0
- package/dist/helpers/inbox-batch-status.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch.helpers.d.ts +10 -0
- package/dist/helpers/inbox-batch.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-batch.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-batch.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-issuance-error.helpers.d.ts +13 -0
- package/dist/helpers/inbox-issuance-error.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-queue.helpers.d.ts +11 -0
- package/dist/helpers/inbox-queue.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox-queue.helpers.test.d.ts +2 -0
- package/dist/helpers/inbox-queue.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox-refresh.helpers.d.ts +186 -0
- package/dist/helpers/inbox-refresh.helpers.d.ts.map +1 -0
- package/dist/helpers/inbox.helpers.d.ts +9 -1
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/issuedCredentialStatus.helpers.d.ts +5 -0
- package/dist/helpers/issuedCredentialStatus.helpers.d.ts.map +1 -0
- package/dist/helpers/notifications.helpers.d.ts.map +1 -1
- package/dist/helpers/rateLimit.helpers.d.ts.map +1 -1
- package/dist/helpers/refresh-send-intent.helpers.d.ts +51 -0
- package/dist/helpers/refresh-send-intent.helpers.d.ts.map +1 -0
- package/dist/helpers/shim.d.ts +1 -1
- package/dist/helpers/shim.d.ts.map +1 -1
- package/dist/helpers/signingAuthority.helpers.d.ts +3 -2
- package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
- package/dist/helpers/signingAuthority.helpers.test.d.ts +2 -0
- package/dist/helpers/signingAuthority.helpers.test.d.ts.map +1 -0
- package/dist/helpers/status-list.helpers.d.ts.map +1 -1
- package/dist/helpers/status-list.helpers.test.d.ts +2 -0
- package/dist/helpers/status-list.helpers.test.d.ts.map +1 -0
- package/dist/inbox-queue-local.d.ts +2 -0
- package/dist/inbox-queue-local.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/AuthGrant.d.ts.map +1 -1
- package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
- package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
- package/dist/models/Credential.d.ts +1 -0
- package/dist/models/Credential.d.ts.map +1 -1
- package/dist/models/CredentialActivity.d.ts +1 -0
- package/dist/models/CredentialActivity.d.ts.map +1 -1
- package/dist/models/CredentialRefresh.d.ts +5 -2
- package/dist/models/CredentialRefresh.d.ts.map +1 -1
- package/dist/models/InboxCredential.d.ts +1 -0
- package/dist/models/InboxCredential.d.ts.map +1 -1
- package/dist/models/credential-refresh-constraints.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/routes/act-as.test.d.ts +2 -0
- package/dist/routes/act-as.test.d.ts.map +1 -0
- package/dist/routes/activity.d.ts +3 -0
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/auth-grants.d.ts +10 -0
- package/dist/routes/auth-grants.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +506 -12
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +48 -2
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credential-refreshes.d.ts +4 -4
- package/dist/routes/credential-refreshes.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +8 -2
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/did-metadata.d.ts +8 -8
- package/dist/routes/federation.d.ts +2 -2
- package/dist/routes/inbox.d.ts +973 -31
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.d.ts +47 -127
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/presentations.d.ts +6 -6
- package/dist/routes/storage.d.ts +63 -63
- package/dist/routes/test.d.ts +6 -0
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts +14 -14
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/services/delivery/adapters/postmark.adapter.test.d.ts +2 -0
- package/dist/services/delivery/adapters/postmark.adapter.test.d.ts.map +1 -0
- package/dist/services/registry/registry.factory.d.ts.map +1 -1
- package/dist/types/activity.d.ts +4 -0
- package/dist/types/activity.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +22 -8
- package/dist/types/consentflowcontract.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +10 -1
- package/dist/types/credential-refresh.d.ts.map +1 -1
- package/dist/types/credential.d.ts +7 -0
- package/dist/types/credential.d.ts.map +1 -1
- package/dist/types/did-metadata.d.ts +2 -2
- package/dist/types/inbox-batch.d.ts +38 -0
- package/dist/types/inbox-batch.d.ts.map +1 -0
- package/inbox-e2e-setup.ts +53 -1
- package/inbox-elasticmq.conf +10 -0
- package/lambda.ts +61 -16
- package/package.json +18 -16
- package/scripts/setup-inbox-queues.ts +60 -0
- package/serverless-local.yml +2 -0
- package/serverless.yml +75 -0
- package/src/accesslayer/app-store-listing/relationships/read.ts +4 -3
- package/src/accesslayer/auth-grant/read.ts +2 -1
- package/src/accesslayer/consentflowcontract/relationships/create.ts +15 -5
- package/src/accesslayer/consentflowcontract/relationships/read.ts +161 -63
- package/src/accesslayer/consentflowcontract/relationships/update.ts +43 -10
- package/src/accesslayer/credential/create.test.ts +141 -0
- package/src/accesslayer/credential/create.ts +27 -3
- package/src/accesslayer/credential-activity/create.ts +1 -0
- package/src/accesslayer/credential-refresh/email-delivery.ts +116 -0
- package/src/accesslayer/credential-refresh/index.ts +1 -0
- package/src/accesslayer/credential-refresh/read.ts +8 -2
- package/src/accesslayer/credential-refresh/update.ts +4 -0
- package/src/accesslayer/inbox-batch/store.ts +469 -0
- package/src/accesslayer/inbox-credential/create.ts +51 -19
- package/src/accesslayer/inbox-credential/refresh.ts +221 -0
- package/src/accesslayer/inbox-credential/update.ts +1 -1
- package/src/cache/atomic-counter.test-helpers.ts +27 -0
- package/src/cache/atomic-counter.test.ts +6 -0
- package/src/cache/index.ts +25 -13
- package/src/config/environment.ts +26 -0
- package/src/credential-refresh.ts +5 -1
- package/src/docker-entry.ts +8 -0
- package/src/helpers/activity.helpers.ts +4 -0
- package/src/helpers/app-counter.helpers.ts +1 -1
- package/src/helpers/boost-storage.helpers.test.ts +242 -0
- package/src/helpers/boost.helpers.ts +58 -437
- package/src/helpers/contact-method.helpers.test.ts +43 -0
- package/src/helpers/contact-method.helpers.ts +3 -4
- package/src/helpers/credential-refresh-email-content.helpers.test.ts +222 -0
- package/src/helpers/credential-refresh-email-content.helpers.ts +155 -0
- package/src/helpers/credential-refresh-email.helpers.ts +201 -0
- package/src/helpers/credential-refresh-proof.helpers.test.ts +103 -0
- package/src/helpers/credential-refresh-proof.helpers.ts +43 -0
- package/src/helpers/credential-refresh.helpers.ts +385 -107
- package/src/helpers/credential.helpers.ts +6 -2
- package/src/helpers/finalize-inbox.helpers.ts +39 -22
- package/src/helpers/inbox-batch-http.helpers.test.ts +188 -0
- package/src/helpers/inbox-batch-http.helpers.ts +150 -0
- package/src/helpers/inbox-batch-status.helpers.ts +11 -0
- package/src/helpers/inbox-batch.helpers.test.ts +668 -0
- package/src/helpers/inbox-batch.helpers.ts +374 -0
- package/src/helpers/inbox-issuance-error.helpers.ts +25 -0
- package/src/helpers/inbox-queue.helpers.test.ts +253 -0
- package/src/helpers/inbox-queue.helpers.ts +372 -0
- package/src/helpers/inbox-refresh.helpers.ts +432 -0
- package/src/helpers/inbox.helpers.ts +301 -50
- package/src/helpers/issuedCredentialStatus.helpers.ts +11 -0
- package/src/helpers/notifications.helpers.ts +4 -2
- package/src/helpers/rateLimit.helpers.ts +2 -5
- package/src/helpers/refresh-send-intent.helpers.ts +273 -0
- package/src/helpers/shim.ts +32 -31
- package/src/helpers/signingAuthority.helpers.test.ts +198 -0
- package/src/helpers/signingAuthority.helpers.ts +41 -18
- package/src/helpers/status-list.helpers.test.ts +77 -0
- package/src/helpers/status-list.helpers.ts +31 -8
- package/src/inbox-queue-local.ts +28 -0
- package/src/models/AuthGrant.ts +1 -0
- package/src/models/ConsentFlowTerms.ts +4 -0
- package/src/models/ConsentFlowTransaction.ts +4 -0
- package/src/models/Credential.ts +1 -0
- package/src/models/CredentialActivity.ts +3 -5
- package/src/models/CredentialRefresh.ts +4 -2
- package/src/models/InboxCredential.ts +2 -0
- package/src/models/credential-refresh-constraints.test.ts +8 -3
- package/src/models/credential-refresh-constraints.ts +5 -0
- package/src/openapi.ts +8 -0
- package/src/routes/act-as.test.ts +239 -0
- package/src/routes/app-store.ts +69 -52
- package/src/routes/auth-grants.ts +13 -11
- package/src/routes/boosts.ts +943 -120
- package/src/routes/contracts.ts +55 -7
- package/src/routes/credential-refreshes.test.ts +8 -2
- package/src/routes/credential-refreshes.ts +4 -1
- package/src/routes/credentials.ts +2 -1
- package/src/routes/inbox.ts +95 -49
- package/src/routes/index.ts +164 -88
- package/src/routes/skills.ts +1 -1
- package/src/routes/workflows.ts +80 -31
- package/src/services/delivery/adapters/postmark.adapter.test.ts +54 -0
- package/src/services/delivery/adapters/postmark.adapter.ts +12 -0
- package/src/services/registry/registry.factory.ts +2 -1
- package/src/types/activity.ts +2 -0
- package/src/types/consentflowcontract.ts +21 -3
- package/src/types/credential-refresh.ts +21 -2
- package/src/types/credential.ts +13 -1
- package/src/types/inbox-batch.ts +46 -0
- package/test/app-store-listing.spec.ts +66 -30
- package/test/atomic-counter.e2e.ts +12 -0
- package/test/auth-grant.spec.ts +21 -21
- package/test/bitstring-status-list.spec.ts +12 -9
- package/test/consentflow.spec.ts +453 -26
- package/test/credential-refresh-email.spec.ts +663 -0
- package/test/credential-refresh-publication.spec.ts +13 -5
- package/test/guardian-gated-routes.spec.ts +337 -180
- package/test/helpers/inbox-e2e-environment.ts +6 -0
- package/test/inbox-batch.spec.ts +1338 -0
- package/test/inbox-refresh.spec.ts +534 -0
- package/test/inbox-security.e2e.ts +48 -3
- package/test/openapi.spec.ts +41 -1
- package/test/refresh-send.spec.ts +1799 -0
- package/test/registry.spec.ts +56 -11
- package/test/revoke-suspend-per-instance.spec.ts +9 -1
- package/vitest.config.ts +13 -0
- package/vitest.inbox-e2e.config.ts +5 -1
- package/vitest.integration.config.ts +2 -0
|
@@ -3,9 +3,20 @@ import {
|
|
|
3
3
|
ConsentFlowTermsValidator,
|
|
4
4
|
ConsentFlowTermsStatusValidator,
|
|
5
5
|
ConsentFlowTransactionActionValidator,
|
|
6
|
+
ConsentFlowGuardianApprovalValidator,
|
|
6
7
|
} from '@learncard/types';
|
|
7
8
|
import { z } from 'zod';
|
|
8
9
|
|
|
10
|
+
type FlatGuardianApproval = {
|
|
11
|
+
[
|
|
12
|
+
Key in keyof z.infer<
|
|
13
|
+
typeof ConsentFlowGuardianApprovalValidator
|
|
14
|
+
> as `guardianApproval.${Key}`
|
|
15
|
+
]?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type FlatConsentTerms = Partial<Record<`terms.${string}`, string | boolean | string[]>>;
|
|
19
|
+
|
|
9
20
|
export const DbContractValidator = z.object({
|
|
10
21
|
id: z.string(),
|
|
11
22
|
name: z.string(),
|
|
@@ -29,18 +40,23 @@ export const DbTransactionValidator = z.object({
|
|
|
29
40
|
id: z.string(),
|
|
30
41
|
action: ConsentFlowTransactionActionValidator,
|
|
31
42
|
terms: ConsentFlowTermsValidator,
|
|
43
|
+
guardianApproval: ConsentFlowGuardianApprovalValidator.optional(),
|
|
32
44
|
date: z.string(),
|
|
33
45
|
expiresAt: z.string().optional(),
|
|
34
46
|
oneTime: z.boolean().optional(),
|
|
35
47
|
});
|
|
36
48
|
|
|
37
49
|
export type DbTransactionType = z.infer<typeof DbTransactionValidator>;
|
|
38
|
-
export type FlatDbTransactionType = DbTransactionType
|
|
50
|
+
export type FlatDbTransactionType = Omit<DbTransactionType, 'guardianApproval' | 'terms'> &
|
|
51
|
+
FlatConsentTerms &
|
|
52
|
+
FlatGuardianApproval;
|
|
39
53
|
|
|
40
54
|
export const DbTermsValidator = z.object({
|
|
41
55
|
id: z.string(),
|
|
42
56
|
status: ConsentFlowTermsStatusValidator,
|
|
43
|
-
|
|
57
|
+
// Neo4j has no properties to store for entirely empty permission objects.
|
|
58
|
+
terms: ConsentFlowTermsValidator.prefault({}),
|
|
59
|
+
guardianApproval: ConsentFlowGuardianApprovalValidator.optional(),
|
|
44
60
|
createdAt: z.string().optional(),
|
|
45
61
|
updatedAt: z.string().optional(),
|
|
46
62
|
expiresAt: z.string().optional(),
|
|
@@ -48,4 +64,6 @@ export const DbTermsValidator = z.object({
|
|
|
48
64
|
});
|
|
49
65
|
|
|
50
66
|
export type DbTermsType = z.infer<typeof DbTermsValidator>;
|
|
51
|
-
export type FlatDbTermsType = DbTermsType
|
|
67
|
+
export type FlatDbTermsType = Omit<DbTermsType, 'guardianApproval' | 'terms'> &
|
|
68
|
+
FlatConsentTerms &
|
|
69
|
+
FlatGuardianApproval;
|
|
@@ -12,17 +12,26 @@ import {
|
|
|
12
12
|
* immutable Credential nodes as holder-encrypted JWE JSON — never plaintext VC JSON.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
export const CredentialRefreshStateValidator = z.enum([
|
|
15
|
+
export const CredentialRefreshStateValidator = z.enum([
|
|
16
|
+
'pending_holder',
|
|
17
|
+
'awaiting_claim',
|
|
18
|
+
'active',
|
|
19
|
+
'revoked',
|
|
20
|
+
]);
|
|
16
21
|
export type CredentialRefreshState = z.infer<typeof CredentialRefreshStateValidator>;
|
|
17
22
|
|
|
18
23
|
/** The managed refresh aggregate node (metadata only — no credential bodies) */
|
|
19
24
|
export const CredentialRefreshRecordValidator = z.object({
|
|
25
|
+
/** Pending Universal Inbox delivery associated with this aggregate. */
|
|
26
|
+
inboxCredentialId: z.string().optional(),
|
|
27
|
+
/** Public status mechanism coordinates retained for the issuer allocation receipt. */
|
|
28
|
+
inboxCredentialStatus: z.string().optional(),
|
|
20
29
|
/** Cryptographically random, unguessable public route identifier */
|
|
21
30
|
refreshId: z.string().min(1),
|
|
22
31
|
issuerProfileId: z.string().min(1),
|
|
23
32
|
issuerDid: z.string().min(1),
|
|
24
33
|
holderProfileId: z.string().min(1).optional(),
|
|
25
|
-
holderDid: z.string().min(1),
|
|
34
|
+
holderDid: z.string().min(1).optional(),
|
|
26
35
|
/** Stable nonempty VC identifier shared by every version */
|
|
27
36
|
credentialId: z.string().min(1),
|
|
28
37
|
state: CredentialRefreshStateValidator,
|
|
@@ -45,6 +54,12 @@ export const CredentialRefreshRecordValidator = z.object({
|
|
|
45
54
|
idempotencyKey: z.string().optional(),
|
|
46
55
|
/** Issuer-authored, privacy-safe update summary */
|
|
47
56
|
updateSummary: z.string().optional(),
|
|
57
|
+
/**
|
|
58
|
+
* Bounded, normalized credential display title used only for the email update
|
|
59
|
+
* notice. Never rendered in in-app/push payloads. Sourced from the unsigned
|
|
60
|
+
* publish input or the original boost template; never from decrypted content.
|
|
61
|
+
*/
|
|
62
|
+
credentialDisplayName: z.string().optional(),
|
|
48
63
|
lastPublishedAt: z.string().optional(),
|
|
49
64
|
/** Notification delivery window key used to collapse repeat notifications */
|
|
50
65
|
notificationWindowKey: z.string().optional(),
|
|
@@ -93,6 +108,8 @@ export const CredentialRefreshVersionNodeValidator = z.object({
|
|
|
93
108
|
etag: z.string().optional(),
|
|
94
109
|
signingMode: CredentialRefreshSigningModeValidator,
|
|
95
110
|
updateSummary: z.string().optional(),
|
|
111
|
+
/** Bounded credential display title for the email update notice only. */
|
|
112
|
+
credentialDisplayName: z.string().optional(),
|
|
96
113
|
/** Caller-provided key for replaying this exact historical publication */
|
|
97
114
|
idempotencyKey: z.string().optional(),
|
|
98
115
|
/** Aggregate-scoped unique key used to enforce historical idempotency */
|
|
@@ -132,6 +149,8 @@ export const AdvanceCredentialRefreshHeadParamsValidator = z.object({
|
|
|
132
149
|
materialDigest: z.string().optional(),
|
|
133
150
|
signingMode: CredentialRefreshSigningModeValidator,
|
|
134
151
|
updateSummary: z.string().optional(),
|
|
152
|
+
/** Bounded credential display title persisted for the email update notice only. */
|
|
153
|
+
credentialDisplayName: z.string().optional(),
|
|
135
154
|
idempotencyKey: z.string().optional(),
|
|
136
155
|
/** Aggregate state observed by the caller; prevents claim/revocation races. */
|
|
137
156
|
expectedState: CredentialRefreshStateValidator.optional(),
|
package/src/types/credential.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import type { BitstringStatusListEntry, JWE, VC } from '@learncard/types';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
/** Internal issuance result; serialize and pass both fields together until storage. */
|
|
5
|
+
export interface IssuedCredential {
|
|
6
|
+
readonly kind: 'issued-credential';
|
|
7
|
+
readonly credential: VC | JWE;
|
|
8
|
+
readonly statusEntries: BitstringStatusListEntry[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const CredentialValidator = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
credential: z.string(),
|
|
14
|
+
statusEntries: z.string().optional(),
|
|
15
|
+
});
|
|
4
16
|
export type CredentialType = z.infer<typeof CredentialValidator>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { InboxBatchStatus, IssueInboxCredentialBatch } from '@learncard/types';
|
|
2
|
+
import type { Context } from '@routes';
|
|
3
|
+
|
|
4
|
+
export type BatchItem = {
|
|
5
|
+
id: string;
|
|
6
|
+
batchId: string;
|
|
7
|
+
index: number;
|
|
8
|
+
state: InboxBatchStatus['items'][number]['state'];
|
|
9
|
+
phase?: 'PREPARING' | 'ISSUING';
|
|
10
|
+
owner?: string;
|
|
11
|
+
leaseUntil?: number;
|
|
12
|
+
attempts: number;
|
|
13
|
+
result?: string;
|
|
14
|
+
replayKey: string;
|
|
15
|
+
duplicate: boolean;
|
|
16
|
+
/** Encrypted client correlation fields retained after the batch payload is removed. */
|
|
17
|
+
correlation?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type InboxDispatchLease = { id: string; dispatchAt: number };
|
|
21
|
+
|
|
22
|
+
export type BatchJob = {
|
|
23
|
+
id: string;
|
|
24
|
+
issuer: string;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
payload?: string;
|
|
27
|
+
completedAt?: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type BatchJobPayload = {
|
|
31
|
+
batch: IssueInboxCredentialBatch;
|
|
32
|
+
context: Pick<Context, 'domain' | 'tenant'>;
|
|
33
|
+
/** Submitting caller's permissions, retained for deferred refresh authorization. */
|
|
34
|
+
scope?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type BatchReplayStore = {
|
|
38
|
+
get: (key: string) => Promise<string | null | undefined>;
|
|
39
|
+
setIfAbsent: (key: string, value: string, ttl: number) => Promise<'OK' | null | undefined>;
|
|
40
|
+
compareAndSet: (
|
|
41
|
+
key: string,
|
|
42
|
+
expected: string,
|
|
43
|
+
value: string | null,
|
|
44
|
+
ttl: number
|
|
45
|
+
) => Promise<boolean | undefined>;
|
|
46
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, beforeAll, beforeEach, afterAll, expect } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import { getClient, getUser } from './helpers/getClient';
|
|
4
|
+
import { minimalContract, minimalTerms } from './helpers/contract';
|
|
4
5
|
|
|
5
6
|
import { AppStoreListing, Integration, Profile } from '@models';
|
|
6
7
|
|
|
@@ -40,7 +41,7 @@ let userB: Awaited<ReturnType<typeof getUser>>;
|
|
|
40
41
|
let userC: Awaited<ReturnType<typeof getUser>>;
|
|
41
42
|
|
|
42
43
|
// For access layer tests - can set all fields including protected ones
|
|
43
|
-
const makeListingInput = (overrides?: Record<string,
|
|
44
|
+
const makeListingInput = (overrides?: Record<string, unknown>) => ({
|
|
44
45
|
display_name: 'Test App',
|
|
45
46
|
tagline: 'A test application',
|
|
46
47
|
full_description: 'This is a comprehensive test application for the app store',
|
|
@@ -54,7 +55,7 @@ const makeListingInput = (overrides?: Record<string, any>) => ({
|
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
// For router tests - excludes protected fields (app_listing_status, promotion_level)
|
|
57
|
-
const makeRouterListingInput = (overrides?: Record<string,
|
|
58
|
+
const makeRouterListingInput = (overrides?: Record<string, unknown>) => {
|
|
58
59
|
const { app_listing_status, promotion_level, ...rest } = makeListingInput(overrides);
|
|
59
60
|
return rest;
|
|
60
61
|
};
|
|
@@ -157,33 +158,6 @@ describe('AppStoreListing', () => {
|
|
|
157
158
|
expect(listing.contact_email).toBe('developer@example.com');
|
|
158
159
|
});
|
|
159
160
|
|
|
160
|
-
it('creates and updates contact_email field', async () => {
|
|
161
|
-
// Create without contact_email
|
|
162
|
-
const listing = await createAppStoreListing(makeListingInput());
|
|
163
|
-
expect(listing.contact_email).toBeUndefined();
|
|
164
|
-
|
|
165
|
-
// Update to add contact_email
|
|
166
|
-
await updateAppStoreListing(listing, {
|
|
167
|
-
contact_email: 'support@example.com',
|
|
168
|
-
});
|
|
169
|
-
const afterAdd = await readAppStoreListingById(listing.listing_id);
|
|
170
|
-
expect(afterAdd?.contact_email).toBe('support@example.com');
|
|
171
|
-
|
|
172
|
-
// Update to change contact_email
|
|
173
|
-
await updateAppStoreListing(afterAdd!, {
|
|
174
|
-
contact_email: 'newcontact@example.com',
|
|
175
|
-
});
|
|
176
|
-
const afterChange = await readAppStoreListingById(listing.listing_id);
|
|
177
|
-
expect(afterChange?.contact_email).toBe('newcontact@example.com');
|
|
178
|
-
|
|
179
|
-
// Update to remove contact_email (cast to any to test null handling)
|
|
180
|
-
await updateAppStoreListing(afterChange!, {
|
|
181
|
-
contact_email: null as any,
|
|
182
|
-
});
|
|
183
|
-
const afterRemove = await readAppStoreListingById(listing.listing_id);
|
|
184
|
-
expect(afterRemove?.contact_email).toBeUndefined();
|
|
185
|
-
});
|
|
186
|
-
|
|
187
161
|
it('creates listing with custom listing_id', async () => {
|
|
188
162
|
const customId = 'custom-listing-123';
|
|
189
163
|
const listing = await createAppStoreListing(
|
|
@@ -671,7 +645,7 @@ describe('AppStoreListing', () => {
|
|
|
671
645
|
const seedListingViaRouter = async (
|
|
672
646
|
user: Awaited<ReturnType<typeof getUser>>,
|
|
673
647
|
integrationId: string,
|
|
674
|
-
overrides?: Record<string,
|
|
648
|
+
overrides?: Record<string, unknown>
|
|
675
649
|
) => {
|
|
676
650
|
// Router creates listings as DRAFT - protected fields are stripped
|
|
677
651
|
const listingId = await user.clients.fullAuth.appStore.createListing({
|
|
@@ -692,6 +666,68 @@ describe('AppStoreListing', () => {
|
|
|
692
666
|
}
|
|
693
667
|
};
|
|
694
668
|
|
|
669
|
+
describe('request-learner-context authorization', () => {
|
|
670
|
+
it('uses current app consent and stops returning data after withdrawal', async () => {
|
|
671
|
+
const integrationId = await seedIntegrationViaRouter(userA);
|
|
672
|
+
const contractUri =
|
|
673
|
+
await userA.clients.fullAuth.contracts.createConsentFlowContract({
|
|
674
|
+
contract: minimalContract,
|
|
675
|
+
name: 'App context consent',
|
|
676
|
+
});
|
|
677
|
+
const listingId = await seedListingViaRouter(userA, integrationId, {
|
|
678
|
+
launch_config_json: JSON.stringify({
|
|
679
|
+
iframeUrl: 'https://app.example.com',
|
|
680
|
+
contractUri,
|
|
681
|
+
}),
|
|
682
|
+
});
|
|
683
|
+
await installAppForProfile('userb', listingId);
|
|
684
|
+
const request = {
|
|
685
|
+
listingId,
|
|
686
|
+
event: { type: 'request-learner-context' as const, includePersonalData: true },
|
|
687
|
+
};
|
|
688
|
+
await expect(
|
|
689
|
+
userB.clients.fullAuth.appStore.appEvent(request)
|
|
690
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
691
|
+
const { termsUri } = await userB.clients.fullAuth.contracts.consentToContract({
|
|
692
|
+
contractUri,
|
|
693
|
+
terms: minimalTerms,
|
|
694
|
+
});
|
|
695
|
+
const context = await userB.clients.fullAuth.appStore.appEvent(request);
|
|
696
|
+
expect(context.personalData).toEqual({ name: minimalTerms.read.personal.name });
|
|
697
|
+
expect(context.credentialUris).toEqual([]);
|
|
698
|
+
expect(context.did).toMatch(/:users:userb$/);
|
|
699
|
+
await userB.clients.fullAuth.contracts.withdrawConsent({ uri: termsUri });
|
|
700
|
+
await expect(
|
|
701
|
+
userB.clients.fullAuth.appStore.appEvent(request)
|
|
702
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('cannot borrow another integration owner’s consent contract', async () => {
|
|
706
|
+
const integrationId = await seedIntegrationViaRouter(userA);
|
|
707
|
+
const contractUri =
|
|
708
|
+
await userB.clients.fullAuth.contracts.createConsentFlowContract({
|
|
709
|
+
contract: minimalContract,
|
|
710
|
+
name: 'Unrelated owner contract',
|
|
711
|
+
});
|
|
712
|
+
await userA.clients.fullAuth.contracts.consentToContract({
|
|
713
|
+
contractUri,
|
|
714
|
+
terms: minimalTerms,
|
|
715
|
+
});
|
|
716
|
+
const listingId = await seedListingViaRouter(userA, integrationId, {
|
|
717
|
+
launch_config_json: JSON.stringify({
|
|
718
|
+
iframeUrl: 'https://app.example.com',
|
|
719
|
+
contractUri,
|
|
720
|
+
}),
|
|
721
|
+
});
|
|
722
|
+
await expect(
|
|
723
|
+
userA.clients.fullAuth.appStore.appEvent({
|
|
724
|
+
listingId,
|
|
725
|
+
event: { type: 'request-learner-context', includePersonalData: true },
|
|
726
|
+
})
|
|
727
|
+
).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
695
731
|
describe('createListing', () => {
|
|
696
732
|
it('requires app-store:write scope and an existing profile', async () => {
|
|
697
733
|
const integrationId = await seedIntegrationViaRouter(userA);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { afterAll, expect, it } from 'vitest';
|
|
2
|
+
import cache from '@cache';
|
|
3
|
+
import { testAtomicCacheCounter } from '../src/cache/atomic-counter.test-helpers';
|
|
4
|
+
|
|
5
|
+
// Fail if the isolated Redis container is missing; never silently test the mock in CI.
|
|
6
|
+
it('uses real Redis for the atomic counter contract', () => {
|
|
7
|
+
expect(cache.redis).toBeDefined();
|
|
8
|
+
});
|
|
9
|
+
testAtomicCacheCounter(cache);
|
|
10
|
+
afterAll(async () => {
|
|
11
|
+
await cache.redis?.quit();
|
|
12
|
+
});
|
package/test/auth-grant.spec.ts
CHANGED
|
@@ -200,7 +200,7 @@ describe('Auth Grants', () => {
|
|
|
200
200
|
|
|
201
201
|
const authGrants = await userA.clients.fullAuth.authGrants.getAuthGrants();
|
|
202
202
|
expect(authGrants).toHaveLength(1);
|
|
203
|
-
expect(
|
|
203
|
+
expect(authGrants[0]?.id).toEqual(authGrantId);
|
|
204
204
|
|
|
205
205
|
await userA.clients.fullAuth.authGrants.addAuthGrant({
|
|
206
206
|
name: 'test2',
|
|
@@ -209,8 +209,8 @@ describe('Auth Grants', () => {
|
|
|
209
209
|
const authGrants2 = await userA.clients.fullAuth.authGrants.getAuthGrants();
|
|
210
210
|
|
|
211
211
|
expect(authGrants2).toHaveLength(2);
|
|
212
|
-
expect(authGrants2[0]
|
|
213
|
-
expect(authGrants2[1]
|
|
212
|
+
expect(authGrants2[0]).toBeDefined();
|
|
213
|
+
expect(authGrants2[1]).toBeDefined();
|
|
214
214
|
});
|
|
215
215
|
|
|
216
216
|
it('should accept a limit', async () => {
|
|
@@ -247,7 +247,7 @@ describe('Auth Grants', () => {
|
|
|
247
247
|
});
|
|
248
248
|
|
|
249
249
|
expect(authGrants).toHaveLength(1);
|
|
250
|
-
expect(
|
|
250
|
+
expect(authGrants[0]?.id).toEqual(authGrantId);
|
|
251
251
|
});
|
|
252
252
|
});
|
|
253
253
|
|
|
@@ -278,17 +278,18 @@ describe('Auth Grants', () => {
|
|
|
278
278
|
id: authGrantId,
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
-
expect(
|
|
281
|
+
expect(updatedAuthGrant?.name).toEqual('test2');
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
-
it('should
|
|
284
|
+
it('should reject updates to the scopes, actAs, challenge, id, createdAt, expiresAt, status of an auth grant', async () => {
|
|
285
285
|
await userA.clients.fullAuth.profile.createProfile({ profileId: 'usera' });
|
|
286
286
|
const authGrantId = await userA.clients.fullAuth.authGrants.addAuthGrant({
|
|
287
287
|
name: 'test',
|
|
288
288
|
});
|
|
289
289
|
|
|
290
|
-
const invalidUpdates:
|
|
290
|
+
const invalidUpdates: Record<string, string> = {
|
|
291
291
|
scope: '*:*',
|
|
292
|
+
actAs: '*',
|
|
292
293
|
challenge: 'test',
|
|
293
294
|
id: 'test',
|
|
294
295
|
createdAt: 'test',
|
|
@@ -296,27 +297,26 @@ describe('Auth Grants', () => {
|
|
|
296
297
|
status: 'revoked',
|
|
297
298
|
};
|
|
298
299
|
|
|
299
|
-
for (const key
|
|
300
|
+
for (const [key, value] of Object.entries(invalidUpdates)) {
|
|
300
301
|
await expect(
|
|
301
302
|
userA.clients.fullAuth.authGrants.updateAuthGrant({
|
|
302
303
|
id: authGrantId,
|
|
303
|
-
updates: {
|
|
304
|
-
[key]: invalidUpdates[key],
|
|
305
|
-
},
|
|
304
|
+
updates: { [key]: value } as never,
|
|
306
305
|
})
|
|
307
|
-
).
|
|
306
|
+
).rejects.toMatchObject({ code: 'BAD_REQUEST' });
|
|
308
307
|
}
|
|
309
308
|
|
|
310
309
|
const updatedAuthGrant = await userA.clients.fullAuth.authGrants.getAuthGrant({
|
|
311
310
|
id: authGrantId,
|
|
312
311
|
});
|
|
313
312
|
|
|
314
|
-
expect(
|
|
315
|
-
expect(
|
|
316
|
-
expect(
|
|
317
|
-
expect(
|
|
318
|
-
expect(
|
|
319
|
-
expect(
|
|
313
|
+
expect(updatedAuthGrant).toMatchObject({ scope: AUTH_GRANT_READ_ONLY_SCOPE });
|
|
314
|
+
expect(updatedAuthGrant?.challenge).not.toEqual('test');
|
|
315
|
+
expect(updatedAuthGrant?.id).not.toEqual('test');
|
|
316
|
+
expect(updatedAuthGrant?.createdAt).not.toEqual('test');
|
|
317
|
+
expect(updatedAuthGrant?.expiresAt).not.toEqual('test');
|
|
318
|
+
expect(updatedAuthGrant?.status).not.toEqual('revoked');
|
|
319
|
+
expect(updatedAuthGrant?.actAs).toBeUndefined();
|
|
320
320
|
});
|
|
321
321
|
|
|
322
322
|
// it('should not allow you to update an auth grant for a scoped user profile', async () => {
|
|
@@ -366,7 +366,7 @@ describe('Auth Grants', () => {
|
|
|
366
366
|
id: authGrantId,
|
|
367
367
|
});
|
|
368
368
|
|
|
369
|
-
expect(
|
|
369
|
+
expect(updatedAuthGrant?.name).toEqual('test2');
|
|
370
370
|
});
|
|
371
371
|
|
|
372
372
|
it("should not allow you to update someone else's auth grant", async () => {
|
|
@@ -511,7 +511,7 @@ describe('Auth Grants', () => {
|
|
|
511
511
|
id: authGrantId,
|
|
512
512
|
});
|
|
513
513
|
|
|
514
|
-
expect(
|
|
514
|
+
expect(authGrant?.status).toEqual(AuthGrantStatusValidator.enum.revoked);
|
|
515
515
|
});
|
|
516
516
|
|
|
517
517
|
// it('should not allow you to revoke an auth grant for a scoped user profile', async () => {
|
|
@@ -552,7 +552,7 @@ describe('Auth Grants', () => {
|
|
|
552
552
|
id: authGrantId,
|
|
553
553
|
});
|
|
554
554
|
|
|
555
|
-
expect(
|
|
555
|
+
expect(authGrant?.status).toEqual(AuthGrantStatusValidator.enum.revoked);
|
|
556
556
|
});
|
|
557
557
|
|
|
558
558
|
it("should not allow you to revoke someone else's auth grant", async () => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
1
3
|
import { Profile, Credential, Boost, StatusList } from '@models';
|
|
2
4
|
import { app as statusListsApp } from '../src/status-lists';
|
|
3
5
|
import { decodeBitstring } from '../src/helpers/status-list.helpers';
|
|
@@ -42,7 +44,7 @@ const isStatusBitSet = async (entry: any): Promise<boolean> => {
|
|
|
42
44
|
const index = Number(entry.statusListIndex);
|
|
43
45
|
const byte = bitstring[Math.floor(index / 8)] ?? 0;
|
|
44
46
|
|
|
45
|
-
return (byte & (1 << index % 8)) !== 0;
|
|
47
|
+
return (byte & (1 << (index % 8))) !== 0;
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
const statusBoostTemplate = {
|
|
@@ -78,18 +80,18 @@ const sendBoostWithStatus = async (
|
|
|
78
80
|
const boostUri = await userA.clients.fullAuth.boost.createBoost({
|
|
79
81
|
credential: template,
|
|
80
82
|
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
// Status coordinates must be included in the issuer's signature. The network
|
|
84
|
+
// no longer adds a certified wrapper after receiving the signed credential.
|
|
85
|
+
const statusEntries = await userA.clients.fullAuth.boost.allocateCredentialStatus({
|
|
86
|
+
statusPurposes:
|
|
87
|
+
statusPurpose === 'suspension' ? ['suspension'] : ['revocation', 'suspension'],
|
|
88
|
+
});
|
|
87
89
|
|
|
88
90
|
const signedCredential = await userA.learnCard.invoke.issueCredential({
|
|
89
91
|
...template,
|
|
90
92
|
issuer: userA.learnCard.id.did(),
|
|
91
93
|
validFrom: new Date().toISOString(),
|
|
92
|
-
|
|
94
|
+
credentialStatus: statusEntries,
|
|
93
95
|
credentialSubject: {
|
|
94
96
|
id: userB.learnCard.id.did(),
|
|
95
97
|
},
|
|
@@ -105,6 +107,7 @@ const sendBoostWithStatus = async (
|
|
|
105
107
|
await userB.clients.fullAuth.credential.acceptCredential({ uri: credentialUri });
|
|
106
108
|
|
|
107
109
|
const credential = await userB.clients.fullAuth.storage.resolve({ uri: credentialUri });
|
|
110
|
+
expect(credential).toEqual(signedCredential);
|
|
108
111
|
|
|
109
112
|
return { credentialUri, credential, boostUri };
|
|
110
113
|
};
|
|
@@ -140,7 +143,7 @@ describe('Bitstring Status List issuance', () => {
|
|
|
140
143
|
expect(await isStatusBitSet(getEntryForPurpose(credential, 'revocation'))).toBe(false);
|
|
141
144
|
});
|
|
142
145
|
|
|
143
|
-
it('
|
|
146
|
+
it('preserves both issuer-signed revocation and suspension entries', async () => {
|
|
144
147
|
const { credential } = await sendBoostWithStatus();
|
|
145
148
|
expect(getEntryForPurpose(credential, 'revocation').type).toBe('BitstringStatusListEntry');
|
|
146
149
|
expect(getEntryForPurpose(credential, 'suspension').type).toBe('BitstringStatusListEntry');
|