@learncard/network-brain-service 3.19.0 → 3.19.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.
- package/.env.example +34 -2
- package/CHANGELOG.md +38 -0
- package/README.md +42 -0
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/presentation/read.d.ts.map +1 -1
- package/dist/accesslayer/presentation/relationships/create.d.ts +2 -2
- package/dist/accesslayer/presentation/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/presentation/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/share-link/abandon.d.ts +3 -0
- package/dist/accesslayer/share-link/abandon.d.ts.map +1 -0
- package/dist/accesslayer/share-link/cleanup.d.ts +6 -0
- package/dist/accesslayer/share-link/cleanup.d.ts.map +1 -0
- package/dist/accesslayer/share-link/errors.d.ts +8 -0
- package/dist/accesslayer/share-link/errors.d.ts.map +1 -0
- package/dist/accesslayer/share-link/finalize.d.ts +3 -0
- package/dist/accesslayer/share-link/finalize.d.ts.map +1 -0
- package/dist/accesslayer/share-link/helpers.d.ts +56 -0
- package/dist/accesslayer/share-link/helpers.d.ts.map +1 -0
- package/dist/accesslayer/share-link/index.d.ts +12 -0
- package/dist/accesslayer/share-link/index.d.ts.map +1 -0
- package/dist/accesslayer/share-link/list.d.ts +5 -0
- package/dist/accesslayer/share-link/list.d.ts.map +1 -0
- package/dist/accesslayer/share-link/read.d.ts +5 -0
- package/dist/accesslayer/share-link/read.d.ts.map +1 -0
- package/dist/accesslayer/share-link/receipt.d.ts +11 -0
- package/dist/accesslayer/share-link/receipt.d.ts.map +1 -0
- package/dist/accesslayer/share-link/recovery.d.ts +6 -0
- package/dist/accesslayer/share-link/recovery.d.ts.map +1 -0
- package/dist/accesslayer/share-link/reserve.d.ts +4 -0
- package/dist/accesslayer/share-link/reserve.d.ts.map +1 -0
- package/dist/accesslayer/share-link/revoke.d.ts +3 -0
- package/dist/accesslayer/share-link/revoke.d.ts.map +1 -0
- package/dist/accesslayer/share-link/transaction-execution.d.ts +18 -0
- package/dist/accesslayer/share-link/transaction-execution.d.ts.map +1 -0
- package/dist/accesslayer/share-link/transaction.d.ts +15 -0
- package/dist/accesslayer/share-link/transaction.d.ts.map +1 -0
- package/dist/accesslayer/share-link/types.d.ts +370 -0
- package/dist/accesslayer/share-link/types.d.ts.map +1 -0
- package/dist/app.d.ts +4 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/config/environment.d.ts +9 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/constants/auth-grant.d.ts +2 -0
- package/dist/constants/auth-grant.d.ts.map +1 -1
- package/dist/helpers/boost.helpers.d.ts.map +1 -1
- package/dist/helpers/credentialSubject.helpers.d.ts +3 -0
- package/dist/helpers/credentialSubject.helpers.d.ts.map +1 -0
- package/dist/helpers/credentialSubject.helpers.test.d.ts +2 -0
- package/dist/helpers/credentialSubject.helpers.test.d.ts.map +1 -0
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/notificationMessages.d.ts +1 -1
- package/dist/helpers/notificationMessages.d.ts.map +1 -1
- package/dist/helpers/presentation-metadata.d.ts +4 -0
- package/dist/helpers/presentation-metadata.d.ts.map +1 -0
- package/dist/helpers/presentation.helpers.d.ts +1 -1
- package/dist/helpers/presentation.helpers.d.ts.map +1 -1
- package/dist/helpers/profile.helpers.d.ts +1 -0
- package/dist/helpers/profile.helpers.d.ts.map +1 -1
- package/dist/helpers/share-content-client/adapters.d.ts +10 -0
- package/dist/helpers/share-content-client/adapters.d.ts.map +1 -0
- package/dist/helpers/share-content-client/canonical.d.ts +7 -0
- package/dist/helpers/share-content-client/canonical.d.ts.map +1 -0
- package/dist/helpers/share-content-client/client.d.ts +5 -0
- package/dist/helpers/share-content-client/client.d.ts.map +1 -0
- package/dist/helpers/share-content-client/config.d.ts +11 -0
- package/dist/helpers/share-content-client/config.d.ts.map +1 -0
- package/dist/helpers/share-content-client/index.d.ts +6 -0
- package/dist/helpers/share-content-client/index.d.ts.map +1 -0
- package/dist/helpers/share-content-client/responses.d.ts +16 -0
- package/dist/helpers/share-content-client/responses.d.ts.map +1 -0
- package/dist/helpers/share-content-client/types.d.ts +145 -0
- package/dist/helpers/share-content-client/types.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/budget-helpers.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/budget-helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts +3 -0
- package/dist/helpers/share-link-coordinator/cleanup-runner.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/coordinator.d.ts +4 -0
- package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/index.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/index.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/recovery-runner.d.ts +9 -0
- package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/runtime.d.ts +11 -0
- package/dist/helpers/share-link-coordinator/runtime.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/stat.d.ts +4 -0
- package/dist/helpers/share-link-coordinator/stat.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/types.d.ts +160 -0
- package/dist/helpers/share-link-coordinator/types.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/index.d.ts +5 -0
- package/dist/helpers/share-link-lifecycle/index.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts +4 -0
- package/dist/helpers/share-link-lifecycle/lease.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts +21 -0
- package/dist/helpers/share-link-lifecycle/recovery.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts +9 -0
- package/dist/helpers/share-link-lifecycle/request-hash.helpers.d.ts.map +1 -0
- package/dist/helpers/share-link-lifecycle/types.d.ts +14 -0
- package/dist/helpers/share-link-lifecycle/types.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/budget.d.ts +12 -0
- package/dist/helpers/share-link-maintenance/budget.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/config.d.ts +48 -0
- package/dist/helpers/share-link-maintenance/config.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/index.d.ts +8 -0
- package/dist/helpers/share-link-maintenance/index.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts +19 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts +36 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/scheduler.d.ts +18 -0
- package/dist/helpers/share-link-maintenance/scheduler.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/telemetry.d.ts +5 -0
- package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -0
- package/dist/helpers/share-link-maintenance/types.d.ts +53 -0
- package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/config.d.ts +16 -0
- package/dist/helpers/share-link-owner/config.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/diagnostics.d.ts +26 -0
- package/dist/helpers/share-link-owner/diagnostics.d.ts.map +1 -0
- package/dist/helpers/share-link-owner/lazy-initializer.d.ts +2 -0
- package/dist/helpers/share-link-owner/lazy-initializer.d.ts.map +1 -0
- package/dist/helpers/share-link-owner-projection.d.ts +4 -0
- package/dist/helpers/share-link-owner-projection.d.ts.map +1 -0
- package/dist/helpers/share-link-passcode-abuse.d.ts +2 -0
- package/dist/helpers/share-link-passcode-abuse.d.ts.map +1 -0
- package/dist/helpers/share-link-passcode.d.ts +6 -0
- package/dist/helpers/share-link-passcode.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/index.d.ts +4 -0
- package/dist/helpers/share-link-policy/index.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/production.d.ts +13 -0
- package/dist/helpers/share-link-policy/production.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/resolver.d.ts +6 -0
- package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -0
- package/dist/helpers/share-link-policy/types.d.ts +16 -0
- package/dist/helpers/share-link-policy/types.d.ts.map +1 -0
- package/dist/helpers/share-link-public/config.d.ts +13 -0
- package/dist/helpers/share-link-public/config.d.ts.map +1 -0
- package/dist/helpers/share-link-view-notification.d.ts +2 -0
- package/dist/helpers/share-link-view-notification.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/Presentation.d.ts +2 -0
- package/dist/models/Presentation.d.ts.map +1 -1
- package/dist/models/Profile.d.ts +2 -0
- package/dist/models/Profile.d.ts.map +1 -1
- package/dist/models/ShareLink.d.ts +44 -0
- package/dist/models/ShareLink.d.ts.map +1 -0
- package/dist/models/ShareViewReceipt.d.ts +22 -0
- package/dist/models/ShareViewReceipt.d.ts.map +1 -0
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/share-link-constraints.d.ts +3 -0
- package/dist/models/share-link-constraints.d.ts.map +1 -0
- package/dist/public-share-link-http.d.ts +4 -0
- package/dist/public-share-link-http.d.ts.map +1 -0
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +2 -2
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +1 -1
- package/dist/routes/index.d.ts +153 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/presentations.d.ts +1 -0
- package/dist/routes/presentations.d.ts.map +1 -1
- package/dist/routes/profile-manager.d.ts.map +1 -1
- package/dist/routes/profiles.d.ts.map +1 -1
- package/dist/routes/public-share-links.d.ts +230 -0
- package/dist/routes/public-share-links.d.ts.map +1 -0
- package/dist/routes/share-links.d.ts +844 -0
- package/dist/routes/share-links.d.ts.map +1 -0
- package/dist/services/delivery/adapters/postmark.adapter.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +1 -1
- package/lambda.ts +4 -1
- package/package.json +22 -16
- package/scripts/seed-demo-persona.ts +425 -0
- package/serverless.yml +36 -0
- package/shareLinkMaintenanceLambda.ts +49 -0
- package/src/accesslayer/consentflowcontract/relationships/create.ts +9 -16
- package/src/accesslayer/consentflowcontract/relationships/update.ts +15 -28
- package/src/accesslayer/presentation/read.ts +57 -25
- package/src/accesslayer/presentation/relationships/create.ts +54 -11
- package/src/accesslayer/presentation/relationships/read.ts +21 -6
- package/src/accesslayer/share-link/abandon.ts +100 -0
- package/src/accesslayer/share-link/cleanup.ts +271 -0
- package/src/accesslayer/share-link/errors.ts +32 -0
- package/src/accesslayer/share-link/finalize.ts +265 -0
- package/src/accesslayer/share-link/helpers.ts +419 -0
- package/src/accesslayer/share-link/index.ts +11 -0
- package/src/accesslayer/share-link/list.ts +162 -0
- package/src/accesslayer/share-link/read.ts +107 -0
- package/src/accesslayer/share-link/receipt.ts +548 -0
- package/src/accesslayer/share-link/recovery.ts +545 -0
- package/src/accesslayer/share-link/reserve.ts +684 -0
- package/src/accesslayer/share-link/revoke.ts +197 -0
- package/src/accesslayer/share-link/transaction-execution.ts +72 -0
- package/src/accesslayer/share-link/transaction.ts +126 -0
- package/src/accesslayer/share-link/types.ts +601 -0
- package/src/app.ts +6 -0
- package/src/config/environment.ts +14 -0
- package/src/constants/auth-grant.ts +5 -0
- package/src/docker-entry.ts +57 -0
- package/src/helpers/boost.helpers.ts +4 -31
- package/src/helpers/credentialSubject.helpers.test.ts +114 -0
- package/src/helpers/credentialSubject.helpers.ts +40 -0
- package/src/helpers/inbox.helpers.ts +7 -4
- package/src/helpers/notificationMessages.ts +18 -1
- package/src/helpers/presentation-metadata.ts +41 -0
- package/src/helpers/presentation.helpers.ts +27 -20
- package/src/helpers/profile.helpers.ts +7 -0
- package/src/helpers/share-content-client/adapters.ts +55 -0
- package/src/helpers/share-content-client/canonical.ts +25 -0
- package/src/helpers/share-content-client/client.ts +440 -0
- package/src/helpers/share-content-client/config.ts +189 -0
- package/src/helpers/share-content-client/index.ts +27 -0
- package/src/helpers/share-content-client/responses.ts +273 -0
- package/src/helpers/share-content-client/types.ts +253 -0
- package/src/helpers/share-link-coordinator/budget-helpers.ts +53 -0
- package/src/helpers/share-link-coordinator/cleanup-runner.ts +286 -0
- package/src/helpers/share-link-coordinator/coordinator.ts +714 -0
- package/src/helpers/share-link-coordinator/index.ts +8 -0
- package/src/helpers/share-link-coordinator/recovery-runner.ts +428 -0
- package/src/helpers/share-link-coordinator/runtime.ts +88 -0
- package/src/helpers/share-link-coordinator/stat.ts +30 -0
- package/src/helpers/share-link-coordinator/types.ts +353 -0
- package/src/helpers/share-link-lifecycle/index.ts +4 -0
- package/src/helpers/share-link-lifecycle/lease.helpers.ts +35 -0
- package/src/helpers/share-link-lifecycle/recovery.helpers.ts +173 -0
- package/src/helpers/share-link-lifecycle/request-hash.helpers.ts +130 -0
- package/src/helpers/share-link-lifecycle/types.ts +58 -0
- package/src/helpers/share-link-maintenance/budget.ts +63 -0
- package/src/helpers/share-link-maintenance/config.ts +312 -0
- package/src/helpers/share-link-maintenance/index.ts +24 -0
- package/src/helpers/share-link-maintenance/runner.ts +223 -0
- package/src/helpers/share-link-maintenance/runtime.ts +334 -0
- package/src/helpers/share-link-maintenance/scheduler.ts +126 -0
- package/src/helpers/share-link-maintenance/telemetry.ts +70 -0
- package/src/helpers/share-link-maintenance/types.ts +89 -0
- package/src/helpers/share-link-owner/config.ts +48 -0
- package/src/helpers/share-link-owner/diagnostics.ts +89 -0
- package/src/helpers/share-link-owner/lazy-initializer.ts +29 -0
- package/src/helpers/share-link-owner-projection.ts +52 -0
- package/src/helpers/share-link-passcode-abuse.ts +36 -0
- package/src/helpers/share-link-passcode.ts +47 -0
- package/src/helpers/share-link-policy/index.ts +7 -0
- package/src/helpers/share-link-policy/production.ts +88 -0
- package/src/helpers/share-link-policy/resolver.ts +108 -0
- package/src/helpers/share-link-policy/types.ts +48 -0
- package/src/helpers/share-link-public/config.ts +41 -0
- package/src/helpers/share-link-view-notification.ts +18 -0
- package/src/models/Presentation.ts +2 -2
- package/src/models/Profile.ts +3 -2
- package/src/models/ShareLink.ts +130 -0
- package/src/models/ShareViewReceipt.ts +69 -0
- package/src/models/index.ts +2 -0
- package/src/models/share-link-constraints.ts +93 -0
- package/src/public-share-link-http.ts +49 -0
- package/src/routes/app-store.ts +2 -11
- package/src/routes/boosts.ts +2 -11
- package/src/routes/contracts.ts +2 -11
- package/src/routes/index.ts +139 -44
- package/src/routes/presentations.ts +19 -3
- package/src/routes/profile-manager.ts +10 -2
- package/src/routes/profiles.ts +17 -6
- package/src/routes/public-share-links.ts +814 -0
- package/src/routes/share-links.ts +640 -0
- package/src/services/delivery/adapters/postmark.adapter.ts +4 -1
- package/test/dids.spec.ts +10 -1
- package/test/guardian-gated-routes.spec.ts +13 -0
- package/test/helpers/share-link-fixtures.ts +94 -0
- package/test/managed-recipient-inbox.spec.ts +130 -0
- package/test/notificationMessages.spec.ts +13 -0
- package/test/openapi.spec.ts +20 -0
- package/test/presentations.spec.ts +54 -6
- package/test/profiles.spec.ts +67 -1
- package/test/share-content-client.contract.spec.ts +365 -0
- package/test/share-content-client.unit.spec.ts +462 -0
- package/test/share-link-abuse.unit.spec.ts +65 -0
- package/test/share-link-coordinator.neo4j.spec.ts +684 -0
- package/test/share-link-coordinator.unit.spec.ts +990 -0
- package/test/share-link-dependency-diagnostics.unit.spec.ts +127 -0
- package/test/share-link-lifecycle.neo4j.spec.ts +1268 -0
- package/test/share-link-lifecycle.unit.spec.ts +205 -0
- package/test/share-link-list-cursor.unit.spec.ts +86 -0
- package/test/share-link-list.neo4j.spec.ts +324 -0
- package/test/share-link-maintenance.neo4j.spec.ts +355 -0
- package/test/share-link-maintenance.unit.spec.ts +1113 -0
- package/test/share-link-owner-policy.unit.spec.ts +210 -0
- package/test/share-link-owner-route.unit.spec.ts +752 -0
- package/test/share-link-owner-runtime.unit.spec.ts +102 -0
- package/test/share-link-passcode.unit.spec.ts +29 -0
- package/test/share-link-policy.unit.spec.ts +135 -0
- package/test/share-link-presentation-metadata.unit.spec.ts +49 -0
- package/test/share-link-production-policy.unit.spec.ts +77 -0
- package/test/share-link-public-config.unit.spec.ts +64 -0
- package/test/share-link-public-http-adapter.unit.spec.ts +315 -0
- package/test/share-link-public-route.unit.spec.ts +800 -0
- package/test/share-link-receipt.neo4j.spec.ts +943 -0
- package/test/share-link-recovery.neo4j.spec.ts +935 -0
- package/test/share-link-recovery.unit.spec.ts +726 -0
- package/vitest.config.ts +1 -0
- package/vitest.share-links-neo4j.config.ts +19 -0
- package/vitest.share-links-unit.config.ts +28 -0
package/src/docker-entry.ts
CHANGED
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
|
|
16
16
|
import { neogma } from '@instance';
|
|
17
17
|
import { appRouter, type AppRouter, createContext } from './app';
|
|
18
|
+
import { publicShareLinkCacheControlHeaders } from './routes';
|
|
19
|
+
import { registerPublicShareLinkNoStore } from './public-share-link-http';
|
|
18
20
|
import { openApiDocument } from './openapi';
|
|
19
21
|
import { inboxBatchResponseMeta } from './helpers/inbox-batch-http.helpers';
|
|
20
22
|
import { didFastifyPlugin } from './dids';
|
|
@@ -23,10 +25,26 @@ import { statusListsFastifyPlugin } from './status-lists';
|
|
|
23
25
|
import { credentialRefreshFastifyPlugin } from './credential-refresh';
|
|
24
26
|
import { deliverQueuedNotification } from '@helpers/notificationQueue.helpers';
|
|
25
27
|
import { startSkillEmbeddingBackfill } from '@helpers/skill-embedding.helpers';
|
|
28
|
+
import {
|
|
29
|
+
createConsoleMaintenanceLogger,
|
|
30
|
+
createShareLinkMaintenanceRuntime,
|
|
31
|
+
createShareLinkMaintenanceScheduler,
|
|
32
|
+
type ShareLinkMaintenanceScheduler,
|
|
33
|
+
} from '@helpers/share-link-maintenance';
|
|
26
34
|
import { maybeAutoSeedSkillFrameworks } from './seed/seedSkillFrameworks';
|
|
27
35
|
|
|
28
36
|
const server = Fastify({ routerOptions: { maxParamLength: 5000 } });
|
|
29
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The scheduled maintenance runtime is created only after the server is ready.
|
|
40
|
+
* When its config is disabled/invalid it is inert, so no interval and no
|
|
41
|
+
* graph/remote/signing dependency is initialized at all.
|
|
42
|
+
*/
|
|
43
|
+
let shareLinkMaintenanceScheduler: ShareLinkMaintenanceScheduler | null = null;
|
|
44
|
+
|
|
45
|
+
server.addHook('onClose', async () => {
|
|
46
|
+
await shareLinkMaintenanceScheduler?.stop();
|
|
47
|
+
});
|
|
30
48
|
// Register before either OpenAPI or tRPC registers its wildcard route. The hook raises the
|
|
31
49
|
// parser ceiling for those shared routes, then narrows it back to 4 MiB only for batch issuance.
|
|
32
50
|
// Registering this later would leave the already-created adapter routes at Fastify's 1 MiB limit.
|
|
@@ -78,6 +96,9 @@ server.register(fastifyTRPCPlugin, {
|
|
|
78
96
|
allowMethodOverride: true,
|
|
79
97
|
router: appRouter,
|
|
80
98
|
createContext,
|
|
99
|
+
responseMeta: ({ paths }) => ({
|
|
100
|
+
headers: publicShareLinkCacheControlHeaders(paths),
|
|
101
|
+
}),
|
|
81
102
|
onError({ path, error }) {
|
|
82
103
|
// report to error monitoring
|
|
83
104
|
console.error(`Error in tRPC handler on path '${path}':`, error);
|
|
@@ -96,6 +117,13 @@ server.register(fastifyTRPCOpenApiPlugin, {
|
|
|
96
117
|
},
|
|
97
118
|
} satisfies CreateOpenApiFastifyPluginOptions<AppRouter>);
|
|
98
119
|
|
|
120
|
+
/**
|
|
121
|
+
* LC-2187: the Fastify OpenAPI adapter has no `responseMeta` hook, so the
|
|
122
|
+
* no-store contract for the anonymous public share-link routes is applied at
|
|
123
|
+
* the adapter boundary itself. It never depends on a cache layer above.
|
|
124
|
+
*/
|
|
125
|
+
registerPublicShareLinkNoStore(server);
|
|
126
|
+
|
|
99
127
|
server.get('/docs/openapi.json', () => openApiDocument);
|
|
100
128
|
|
|
101
129
|
const SCALAR_HTML = `<!DOCTYPE html>
|
|
@@ -138,12 +166,41 @@ server.register(credentialRefreshFastifyPlugin);
|
|
|
138
166
|
} catch (error) {
|
|
139
167
|
console.error('Skill embedding backfill failed', error);
|
|
140
168
|
}
|
|
169
|
+
|
|
170
|
+
// Explicit startup after readiness. Disabled/invalid config stays inert.
|
|
171
|
+
const maintenanceRuntime = createShareLinkMaintenanceRuntime();
|
|
172
|
+
|
|
173
|
+
if (maintenanceRuntime.resolution.status === 'enabled') {
|
|
174
|
+
shareLinkMaintenanceScheduler = createShareLinkMaintenanceScheduler({
|
|
175
|
+
runOnce: () => maintenanceRuntime.runOnce(),
|
|
176
|
+
intervalMs: maintenanceRuntime.resolution.config.intervalMs,
|
|
177
|
+
logger: createConsoleMaintenanceLogger(),
|
|
178
|
+
});
|
|
179
|
+
shareLinkMaintenanceScheduler.start();
|
|
180
|
+
}
|
|
141
181
|
} catch (err) {
|
|
142
182
|
console.error(err);
|
|
143
183
|
process.exit(1);
|
|
144
184
|
}
|
|
145
185
|
})();
|
|
146
186
|
|
|
187
|
+
const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
|
|
188
|
+
console.log(`Received ${signal}; closing server and draining maintenance`);
|
|
189
|
+
try {
|
|
190
|
+
await server.close();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error('Server shutdown failed', error);
|
|
193
|
+
}
|
|
194
|
+
process.exit(0);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
process.once('SIGTERM', () => {
|
|
198
|
+
void shutdown('SIGTERM');
|
|
199
|
+
});
|
|
200
|
+
process.once('SIGINT', () => {
|
|
201
|
+
void shutdown('SIGINT');
|
|
202
|
+
});
|
|
203
|
+
|
|
147
204
|
const pollUrl = environment.NOTIFICATIONS_QUEUE_POLL_URL;
|
|
148
205
|
|
|
149
206
|
if (pollUrl) {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
parseRenderedTemplate,
|
|
23
23
|
shouldAutoAppendTemplateEvidence,
|
|
24
24
|
} from '@helpers/template.helpers';
|
|
25
|
+
import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
|
|
25
26
|
|
|
26
27
|
import { getBoostOwner } from '@accesslayer/boost/relationships/read';
|
|
27
28
|
import { BoostInstance } from '@models';
|
|
@@ -95,15 +96,7 @@ export const convertCredentialToBoostTemplateJSON = (
|
|
|
95
96
|
delete template.proof;
|
|
96
97
|
template.issuer = defaultIssuerDid ?? 'did:example:123';
|
|
97
98
|
|
|
98
|
-
|
|
99
|
-
template.credentialSubject = template.credentialSubject.map(subject => {
|
|
100
|
-
subject.id = 'did:example:123';
|
|
101
|
-
|
|
102
|
-
return subject;
|
|
103
|
-
});
|
|
104
|
-
} else {
|
|
105
|
-
template.credentialSubject.id = 'did:example:123';
|
|
106
|
-
}
|
|
99
|
+
setCredentialSubjectIds(template, 'did:example:123');
|
|
107
100
|
return JSON.stringify(template);
|
|
108
101
|
};
|
|
109
102
|
|
|
@@ -262,14 +255,7 @@ export const issueClaimLinkBoost = async (
|
|
|
262
255
|
|
|
263
256
|
boostCredential.issuer = signingAuthorityForUser.relationship.did;
|
|
264
257
|
|
|
265
|
-
|
|
266
|
-
boostCredential.credentialSubject = boostCredential.credentialSubject.map(subject => ({
|
|
267
|
-
...subject,
|
|
268
|
-
id: getDidWeb(domain, to.profileId),
|
|
269
|
-
}));
|
|
270
|
-
} else {
|
|
271
|
-
boostCredential.credentialSubject.id = getDidWeb(domain, to.profileId);
|
|
272
|
-
}
|
|
258
|
+
setCredentialSubjectIds(boostCredential, getDidWeb(domain, to.profileId));
|
|
273
259
|
|
|
274
260
|
// Embed the boostURI into the boost credential for verification purposes.
|
|
275
261
|
if (boostCredential.type.includes('BoostCredential')) {
|
|
@@ -362,20 +348,7 @@ export const prepareCredentialFromBoost = async (
|
|
|
362
348
|
credential.issuer = issuerDid;
|
|
363
349
|
}
|
|
364
350
|
|
|
365
|
-
|
|
366
|
-
if (recipientDid) {
|
|
367
|
-
if (Array.isArray(credential.credentialSubject)) {
|
|
368
|
-
credential.credentialSubject = credential.credentialSubject.map(subject => ({
|
|
369
|
-
...subject,
|
|
370
|
-
id: recipientDid,
|
|
371
|
-
}));
|
|
372
|
-
} else {
|
|
373
|
-
credential.credentialSubject = {
|
|
374
|
-
...(credential.credentialSubject || {}),
|
|
375
|
-
id: recipientDid,
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
}
|
|
351
|
+
if (recipientDid) setCredentialSubjectIds(credential, recipientDid);
|
|
379
352
|
|
|
380
353
|
// Embed boostId for BoostCredential types
|
|
381
354
|
if (credential?.type?.includes('BoostCredential')) {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { UnsignedVC } from '@learncard/types';
|
|
3
|
+
|
|
4
|
+
import { setCredentialSubjectIds } from './credentialSubject.helpers';
|
|
5
|
+
|
|
6
|
+
const collectSubjectIds = (credential: Record<string, unknown>): string[] => {
|
|
7
|
+
const subject = credential.credentialSubject;
|
|
8
|
+
const subjects = Array.isArray(subject) ? subject : [subject];
|
|
9
|
+
|
|
10
|
+
return subjects.flatMap(currentSubject => {
|
|
11
|
+
if (!currentSubject || typeof currentSubject !== 'object') return [];
|
|
12
|
+
|
|
13
|
+
const subjectRecord = currentSubject as Record<string, unknown>;
|
|
14
|
+
const embedded = subjectRecord.verifiableCredential;
|
|
15
|
+
const embeddedCredentials = Array.isArray(embedded) ? embedded : embedded ? [embedded] : [];
|
|
16
|
+
|
|
17
|
+
return [
|
|
18
|
+
subjectRecord.id as string,
|
|
19
|
+
...embeddedCredentials.flatMap(nested =>
|
|
20
|
+
collectSubjectIds(nested as Record<string, unknown>)
|
|
21
|
+
),
|
|
22
|
+
];
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('setCredentialSubjectIds', () => {
|
|
27
|
+
it('patches every CLR learner subject without rewriting achievement identities', () => {
|
|
28
|
+
const credential = {
|
|
29
|
+
credentialSubject: {
|
|
30
|
+
id: 'did:example:old-root',
|
|
31
|
+
achievement: {
|
|
32
|
+
id: 'https://example.edu/transcript',
|
|
33
|
+
creator: { id: 'https://example.edu' },
|
|
34
|
+
},
|
|
35
|
+
verifiableCredential: [
|
|
36
|
+
{
|
|
37
|
+
credentialSubject: {
|
|
38
|
+
id: 'did:example:old-course',
|
|
39
|
+
achievement: {
|
|
40
|
+
id: 'https://example.edu/courses/imd-120',
|
|
41
|
+
creator: { id: 'https://example.edu/departments/design' },
|
|
42
|
+
},
|
|
43
|
+
verifiableCredential: {
|
|
44
|
+
credentialSubject: { id: 'did:example:old-assessment' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
} as unknown as UnsignedVC;
|
|
51
|
+
|
|
52
|
+
setCredentialSubjectIds(credential, 'did:example:learner');
|
|
53
|
+
|
|
54
|
+
expect(collectSubjectIds(credential as unknown as Record<string, unknown>)).toEqual([
|
|
55
|
+
'did:example:learner',
|
|
56
|
+
'did:example:learner',
|
|
57
|
+
'did:example:learner',
|
|
58
|
+
]);
|
|
59
|
+
expect(credential.credentialSubject).toMatchObject({
|
|
60
|
+
achievement: {
|
|
61
|
+
id: 'https://example.edu/transcript',
|
|
62
|
+
creator: { id: 'https://example.edu' },
|
|
63
|
+
},
|
|
64
|
+
verifiableCredential: [
|
|
65
|
+
{
|
|
66
|
+
credentialSubject: {
|
|
67
|
+
achievement: {
|
|
68
|
+
id: 'https://example.edu/courses/imd-120',
|
|
69
|
+
creator: { id: 'https://example.edu/departments/design' },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('preserves signed embedded credentials while patching unsigned subjects', () => {
|
|
78
|
+
const credential = {
|
|
79
|
+
credentialSubject: {
|
|
80
|
+
id: 'did:example:old-root',
|
|
81
|
+
verifiableCredential: [
|
|
82
|
+
{
|
|
83
|
+
credentialSubject: { id: 'did:example:signed-subject' },
|
|
84
|
+
proof: { type: 'DataIntegrityProof', proofValue: 'signed-value' },
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
credentialSubject: { id: 'did:example:unsigned-subject' },
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
} as unknown as UnsignedVC;
|
|
92
|
+
|
|
93
|
+
setCredentialSubjectIds(credential, 'did:example:learner');
|
|
94
|
+
|
|
95
|
+
const subject = credential.credentialSubject as Record<string, unknown>;
|
|
96
|
+
const embedded = subject.verifiableCredential as Array<Record<string, unknown>>;
|
|
97
|
+
|
|
98
|
+
expect(subject.id).toBe('did:example:learner');
|
|
99
|
+
expect(embedded[0]?.credentialSubject).toEqual({
|
|
100
|
+
id: 'did:example:signed-subject',
|
|
101
|
+
});
|
|
102
|
+
expect(embedded[1]?.credentialSubject).toEqual({
|
|
103
|
+
id: 'did:example:learner',
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('creates an outer credential subject when a legacy template omits it', () => {
|
|
108
|
+
const credential = {} as UnsignedVC;
|
|
109
|
+
|
|
110
|
+
setCredentialSubjectIds(credential, 'did:example:learner');
|
|
111
|
+
|
|
112
|
+
expect(credential.credentialSubject).toEqual({ id: 'did:example:learner' });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { UnsignedVC, VC } from '@learncard/types';
|
|
2
|
+
|
|
3
|
+
const setSubjectIds = (subject: unknown, subjectDid: string): void => {
|
|
4
|
+
if (Array.isArray(subject)) {
|
|
5
|
+
subject.forEach(nestedSubject => setSubjectIds(nestedSubject, subjectDid));
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (!subject || typeof subject !== 'object') return;
|
|
10
|
+
|
|
11
|
+
const mutableSubject = subject as Record<string, unknown>;
|
|
12
|
+
mutableSubject.id = subjectDid;
|
|
13
|
+
|
|
14
|
+
const embeddedCredentials = mutableSubject.verifiableCredential;
|
|
15
|
+
const credentials = Array.isArray(embeddedCredentials)
|
|
16
|
+
? embeddedCredentials
|
|
17
|
+
: [embeddedCredentials];
|
|
18
|
+
|
|
19
|
+
credentials.forEach(credential => {
|
|
20
|
+
if (!credential || typeof credential !== 'object' || Array.isArray(credential)) return;
|
|
21
|
+
|
|
22
|
+
const credentialRecord = credential as Record<string, unknown>;
|
|
23
|
+
if (credentialRecord.proof !== undefined) return;
|
|
24
|
+
|
|
25
|
+
setSubjectIds(credentialRecord.credentialSubject, subjectDid);
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sets the recipient DID on the outer credential and unsigned credentials nested in a CLR.
|
|
31
|
+
* Signed embedded credentials are immutable because changing their subject invalidates the proof.
|
|
32
|
+
*/
|
|
33
|
+
export const setCredentialSubjectIds = (credential: UnsignedVC | VC, subjectDid: string): void => {
|
|
34
|
+
if (!credential.credentialSubject) {
|
|
35
|
+
credential.credentialSubject = { id: subjectDid };
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
setSubjectIds(credential.credentialSubject, subjectDid);
|
|
40
|
+
};
|
|
@@ -325,9 +325,11 @@ export const issueToInbox = async (
|
|
|
325
325
|
|
|
326
326
|
// Check if the recipient is a managed child (has a guardian via MANAGES relationship).
|
|
327
327
|
// Managed children need guardian approval for all inbox credentials, even without explicit guardianEmail.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
328
|
+
// Service profiles (orgs) are managed for admin purposes, not guardianship.
|
|
329
|
+
const recipientManagers =
|
|
330
|
+
existingProfile && !existingProfile.isServiceProfile
|
|
331
|
+
? await getProfilesThatManageAProfile(existingProfile.profileId)
|
|
332
|
+
: [];
|
|
331
333
|
const recipientIsManaged = recipientManagers.length > 0;
|
|
332
334
|
|
|
333
335
|
if (existingProfile && !guardianEmail && !recipientIsManaged && !configuration.refresh) {
|
|
@@ -877,8 +879,9 @@ export const issueToInbox = async (
|
|
|
877
879
|
|
|
878
880
|
await deliveryService.send({
|
|
879
881
|
contactMethod: recipient,
|
|
880
|
-
templateId: delivery?.template?.id
|
|
882
|
+
templateId: delivery?.template?.id || 'universal-inbox-claim',
|
|
881
883
|
templateModel: {
|
|
884
|
+
claimUrl: emailClaimUrl,
|
|
882
885
|
emailClaimUrl,
|
|
883
886
|
claimToken: emailClaimToken,
|
|
884
887
|
...injectedTemplateFields,
|
|
@@ -57,7 +57,8 @@ export type NotificationMessageKey =
|
|
|
57
57
|
| 'credentialRestoredUnnamed'
|
|
58
58
|
// --- Managed credential refresh (LC-2136) — generic copy only: never name the
|
|
59
59
|
// credential, the holder's claims, or implementation terms (refresh/sync/etc.) ---
|
|
60
|
-
| 'credentialRefreshed'
|
|
60
|
+
| 'credentialRefreshed'
|
|
61
|
+
| 'shareViewed';
|
|
61
62
|
|
|
62
63
|
type MessageTemplate = { title: string; body: string };
|
|
63
64
|
type LocaleCatalog = Record<NotificationMessageKey, MessageTemplate>;
|
|
@@ -216,6 +217,10 @@ const en: LocaleCatalog = {
|
|
|
216
217
|
title: 'Credential updated',
|
|
217
218
|
body: '{from} updated one of your credentials.',
|
|
218
219
|
},
|
|
220
|
+
shareViewed: {
|
|
221
|
+
title: 'Share viewed',
|
|
222
|
+
body: 'Your share “{title} ({count})” was viewed.',
|
|
223
|
+
},
|
|
219
224
|
};
|
|
220
225
|
|
|
221
226
|
const es: LocaleCatalog = {
|
|
@@ -367,6 +372,10 @@ const es: LocaleCatalog = {
|
|
|
367
372
|
title: 'Credencial actualizada',
|
|
368
373
|
body: '{from} actualizó una de tus credenciales.',
|
|
369
374
|
},
|
|
375
|
+
shareViewed: {
|
|
376
|
+
title: 'Enlace visto',
|
|
377
|
+
body: 'Se vio tu enlace «{title} ({count})».',
|
|
378
|
+
},
|
|
370
379
|
};
|
|
371
380
|
|
|
372
381
|
const fr: LocaleCatalog = {
|
|
@@ -518,6 +527,10 @@ const fr: LocaleCatalog = {
|
|
|
518
527
|
title: 'Titre mis à jour',
|
|
519
528
|
body: "{from} a mis à jour l'un de vos titres.",
|
|
520
529
|
},
|
|
530
|
+
shareViewed: {
|
|
531
|
+
title: 'Partage consulté',
|
|
532
|
+
body: 'Votre partage « {title} ({count}) » a été consulté.',
|
|
533
|
+
},
|
|
521
534
|
};
|
|
522
535
|
|
|
523
536
|
const ar: LocaleCatalog = {
|
|
@@ -669,6 +682,10 @@ const ar: LocaleCatalog = {
|
|
|
669
682
|
title: 'تم تحديث الشهادة',
|
|
670
683
|
body: 'قام {from} بتحديث إحدى شهاداتك.',
|
|
671
684
|
},
|
|
685
|
+
shareViewed: {
|
|
686
|
+
title: 'تمت مشاهدة المشاركة',
|
|
687
|
+
body: 'تمت مشاهدة مشاركتك «{title} ({count})».',
|
|
688
|
+
},
|
|
672
689
|
};
|
|
673
690
|
|
|
674
691
|
const CATALOGS: Record<NotificationLocale, LocaleCatalog> = { en, es, fr, ar };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { TRPCError } from '@trpc/server';
|
|
2
|
+
import { ShareLinkIdValidator } from '@learncard/types';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
|
|
5
|
+
// Relationship metadata is not signed VP content. Bound generic metadata and
|
|
6
|
+
// reserve the share-link shape for the recipient's own send/accept workflow.
|
|
7
|
+
const ShareLinkPresentationMetadataValidator = z
|
|
8
|
+
.object({
|
|
9
|
+
type: z.literal('learncard.share-link.v1'),
|
|
10
|
+
shareId: ShareLinkIdValidator,
|
|
11
|
+
title: z.string().min(1).max(120),
|
|
12
|
+
note: z.string().max(500).optional(),
|
|
13
|
+
sharer: z
|
|
14
|
+
.object({
|
|
15
|
+
profileId: z.string().min(1).max(128),
|
|
16
|
+
displayName: z.string().min(1).max(120),
|
|
17
|
+
})
|
|
18
|
+
.strict(),
|
|
19
|
+
})
|
|
20
|
+
.strict();
|
|
21
|
+
|
|
22
|
+
export const PresentationMetadataValidator = z
|
|
23
|
+
.record(z.string().min(1).max(64), z.unknown())
|
|
24
|
+
.refine(value => JSON.stringify(value).length <= 4096, 'Metadata is too large')
|
|
25
|
+
.superRefine((value, ctx) => {
|
|
26
|
+
if (value.type !== 'learncard.share-link.v1') return;
|
|
27
|
+
if (!ShareLinkPresentationMetadataValidator.safeParse(value).success)
|
|
28
|
+
ctx.addIssue({ code: 'custom', message: 'Invalid share-link metadata' });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const assertPresentationMetadataRecipient = (
|
|
32
|
+
metadata: Record<string, unknown> | undefined,
|
|
33
|
+
senderProfileId: string,
|
|
34
|
+
recipientProfileId: string
|
|
35
|
+
): void => {
|
|
36
|
+
if (metadata?.type === 'learncard.share-link.v1' && senderProfileId !== recipientProfileId)
|
|
37
|
+
throw new TRPCError({
|
|
38
|
+
code: 'BAD_REQUEST',
|
|
39
|
+
message: 'Share-link metadata requires a self-save',
|
|
40
|
+
});
|
|
41
|
+
};
|
|
@@ -23,23 +23,29 @@ export const sendPresentation = async (
|
|
|
23
23
|
from: ProfileType,
|
|
24
24
|
to: ProfileType,
|
|
25
25
|
presentation: VP | JWE,
|
|
26
|
-
domain: string
|
|
26
|
+
domain: string,
|
|
27
|
+
metadata?: Record<string, unknown>
|
|
27
28
|
): Promise<string> => {
|
|
28
29
|
const presentationInstance = await storePresentation(presentation);
|
|
29
30
|
|
|
30
|
-
await createSentPresentationRelationship(from, to, presentationInstance);
|
|
31
|
+
await createSentPresentationRelationship(from, to, presentationInstance, metadata);
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
const uri = getPresentationUri(presentationInstance.id, domain);
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
// Saving a shared collection reuses the presentation send/accept pipeline by
|
|
36
|
+
// sending it to the current profile. Keep that storage behavior without
|
|
37
|
+
// creating a meaningless "you sent this to yourself" recipient alert.
|
|
38
|
+
if (from.profileId !== to.profileId) {
|
|
39
|
+
await addNotificationToQueue({
|
|
40
|
+
type: LCNNotificationTypeEnumValidator.enum.PRESENTATION_RECEIVED,
|
|
41
|
+
to,
|
|
42
|
+
from,
|
|
43
|
+
message: getNotificationMessage('presentationReceived', resolveRecipientLocale(to), {
|
|
44
|
+
from: from.displayName,
|
|
45
|
+
}),
|
|
46
|
+
data: { vpUris: [uri] },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
43
49
|
|
|
44
50
|
return uri;
|
|
45
51
|
};
|
|
@@ -63,16 +69,17 @@ export const acceptPresentation = async (profile: ProfileType, uri: string): Pro
|
|
|
63
69
|
});
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
//
|
|
72
|
+
// Acceptance is idempotent so a client can safely recover from a response
|
|
73
|
+
// interruption without producing a second received relationship.
|
|
67
74
|
const alreadyReceived = await getPresentationReceivedByProfile(id, profile);
|
|
68
|
-
if (alreadyReceived) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
75
|
+
if (!alreadyReceived) {
|
|
76
|
+
await createReceivedPresentationRelationship(
|
|
77
|
+
profile,
|
|
78
|
+
pendingVp.source,
|
|
79
|
+
pendingVp.target,
|
|
80
|
+
pendingVp.relationship.metadata
|
|
81
|
+
);
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
await createReceivedPresentationRelationship(profile, pendingVp.source, pendingVp.target);
|
|
76
|
-
|
|
77
84
|
return true;
|
|
78
85
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LCNProfileValidator } from '@learncard/types';
|
|
2
|
+
|
|
1
3
|
import { getProfileByProfileId } from '@accesslayer/profile/read';
|
|
2
4
|
import { createProfile } from '@accesslayer/profile/create';
|
|
3
5
|
import { getProfileIdFromDid } from './did.helpers';
|
|
@@ -6,6 +8,11 @@ import { ProfileType } from 'types/profile';
|
|
|
6
8
|
export const transformProfileId = (rawInput: string): string =>
|
|
7
9
|
rawInput.toLowerCase().replaceAll(':', '%3A');
|
|
8
10
|
|
|
11
|
+
export const PublicProfileIdValidator = LCNProfileValidator.shape.profileId.refine(
|
|
12
|
+
profileId => !transformProfileId(profileId).startsWith('sample-'),
|
|
13
|
+
{ message: 'Profile IDs beginning with "sample-" are reserved.' }
|
|
14
|
+
);
|
|
15
|
+
|
|
9
16
|
/**
|
|
10
17
|
* Gets or creates a federated profile for cross-instance credential sending.
|
|
11
18
|
* Federated profiles use email-like format: profileId@service-domain to avoid collisions.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { getDidWebLearnCard, getServerDidWebDID } from '@helpers/learnCard.helpers';
|
|
2
|
+
|
|
3
|
+
import type { ShareContentTokenSigner } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runtime signing adapter for the LC-2187 share-content service token.
|
|
7
|
+
*
|
|
8
|
+
* It reuses the existing did:web LearnCard signing primitives — no new
|
|
9
|
+
* cryptographic algorithm or key material is introduced. The fixed C1 verifier
|
|
10
|
+
* expects a holder-signed VP JWT whose `nonce` is the JSON-serialized claims
|
|
11
|
+
* object, signed with `EdDSA` by an allowlisted did:web verification method. The
|
|
12
|
+
* `kid` did:web fragment is produced by DIDKit from the signing key; the client
|
|
13
|
+
* independently re-checks that it binds the configured signer DID before any
|
|
14
|
+
* request is sent.
|
|
15
|
+
*
|
|
16
|
+
* Exported separately from the client barrel so unit tests never load the
|
|
17
|
+
* LearnCard/environment graph.
|
|
18
|
+
*/
|
|
19
|
+
export const createDidWebLearnCardTokenSigner = (dependencies?: {
|
|
20
|
+
getLearnCard?: () => Promise<{
|
|
21
|
+
invoke: { issuePresentation: (...args: unknown[]) => unknown };
|
|
22
|
+
}>;
|
|
23
|
+
getSignerDid?: () => string;
|
|
24
|
+
}): ShareContentTokenSigner => {
|
|
25
|
+
const resolveLearnCard = dependencies?.getLearnCard ?? getDidWebLearnCard;
|
|
26
|
+
const resolveSignerDid = dependencies?.getSignerDid ?? getServerDidWebDID;
|
|
27
|
+
|
|
28
|
+
return async claims => {
|
|
29
|
+
const learnCard = await resolveLearnCard();
|
|
30
|
+
const signerDid = resolveSignerDid();
|
|
31
|
+
|
|
32
|
+
if (claims.iss !== signerDid) {
|
|
33
|
+
throw new Error('share-content signer DID does not match the token issuer binding');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const presentation = {
|
|
37
|
+
'@context': ['https://www.w3.org/2018/credentials/v1'],
|
|
38
|
+
type: ['VerifiablePresentation'],
|
|
39
|
+
holder: signerDid,
|
|
40
|
+
verifiableCredential: [],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const issued = await learnCard.invoke.issuePresentation(presentation, {
|
|
44
|
+
proofPurpose: 'authentication',
|
|
45
|
+
proofFormat: 'jwt',
|
|
46
|
+
challenge: JSON.stringify(claims),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (typeof issued !== 'string' || issued.length === 0) {
|
|
50
|
+
throw new Error('share-content signer did not return a JWT presentation');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return issued;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { canonicalizeShareContentRequestBody } from '@learncard/helpers';
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
canonicalizeShareContentRequestBody,
|
|
6
|
+
DEFAULT_MAX_CANONICAL_BYTES,
|
|
7
|
+
DEFAULT_MAX_CANONICAL_DEPTH,
|
|
8
|
+
ShareContentCanonicalizationError,
|
|
9
|
+
} from '@learncard/helpers';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* SHA-256 (lowercase hex) over the canonical request body. This is exactly the
|
|
13
|
+
* value signed into `claims.requestHash` and recomputed by C1.
|
|
14
|
+
*/
|
|
15
|
+
export const computeShareContentRequestBodyHash = (
|
|
16
|
+
body: unknown,
|
|
17
|
+
options: { maxBytes?: number; maxDepth?: number } = {}
|
|
18
|
+
): string =>
|
|
19
|
+
createHash('sha256')
|
|
20
|
+
.update(canonicalizeShareContentRequestBody(body, options), 'utf8')
|
|
21
|
+
.digest('hex');
|
|
22
|
+
|
|
23
|
+
/** Strict lowercase hex SHA-256. */
|
|
24
|
+
export const isSha256Hex = (value: unknown): value is string =>
|
|
25
|
+
typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
|