@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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { ModelFactory, NeogmaInstance } from 'neogma';
|
|
2
|
+
|
|
3
|
+
import { neogma } from '@instance';
|
|
4
|
+
|
|
5
|
+
/** Lifecycle status of a share. Only `active` is ever served to a recipient. */
|
|
6
|
+
export type ShareLinkStatus = 'pending' | 'active' | 'stopped';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Whether the visible content pointer for a share is committed.
|
|
10
|
+
*
|
|
11
|
+
* `staging` means a reservation exists but the object is not visible yet;
|
|
12
|
+
* `content_missing` means Brain believes content should be visible but LearnCloud
|
|
13
|
+
* does not have it, so the share fails closed and is never served.
|
|
14
|
+
*/
|
|
15
|
+
export type ShareContentState = 'staging' | 'finalized' | 'content_missing';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Persisted ShareLink metadata (LC-2187).
|
|
19
|
+
*
|
|
20
|
+
* This node is the only Brain-side source of truth for the *visible* content
|
|
21
|
+
* pointer. It stores an opaque immutable `activeObjectRef` plus content/recovery
|
|
22
|
+
* hashes and byte counts — never ciphertext, a raw private source URI or a key.
|
|
23
|
+
* The concrete encrypted envelope and owner recovery JWE live together in the
|
|
24
|
+
* dedicated LearnCloud collection keyed by the immutable object reference.
|
|
25
|
+
*
|
|
26
|
+
* `version` is the committed record revision used for optimistic concurrency; a
|
|
27
|
+
* reservation (`staging`) does not change it. `contentVersion` is the visible
|
|
28
|
+
* content revision and changes only when a replacement is finalized.
|
|
29
|
+
*/
|
|
30
|
+
export type ShareLinkRecord = {
|
|
31
|
+
id: string;
|
|
32
|
+
namespace: string;
|
|
33
|
+
ownerProfileId: string;
|
|
34
|
+
version: number;
|
|
35
|
+
contentVersion: number;
|
|
36
|
+
/**
|
|
37
|
+
* Monotonic fence. Bumped on every reservation and on revoke; a worker may
|
|
38
|
+
* only finalize the reservation whose generation still equals this value.
|
|
39
|
+
*/
|
|
40
|
+
generation: number;
|
|
41
|
+
status: ShareLinkStatus;
|
|
42
|
+
contentState: ShareContentState;
|
|
43
|
+
activeObjectRef: string | null;
|
|
44
|
+
activeObjectOperationId: string | null;
|
|
45
|
+
activeContentHash: string | null;
|
|
46
|
+
activeContentBytes: number | null;
|
|
47
|
+
activeRecoveryHash: string | null;
|
|
48
|
+
/** Operation binding: the operation id that produced the visible revision. */
|
|
49
|
+
lastOperationId: string | null;
|
|
50
|
+
/** Operation binding: the create request that first bound this share id. */
|
|
51
|
+
createdByClientRequestId: string | null;
|
|
52
|
+
title: string;
|
|
53
|
+
note: string | null;
|
|
54
|
+
selectedCount: number;
|
|
55
|
+
expiresAt: string | null;
|
|
56
|
+
stoppedAt: string | null;
|
|
57
|
+
viewCount: number;
|
|
58
|
+
lastViewedAt: string | null;
|
|
59
|
+
/** Argon2id PHC string. Plaintext passcodes are never persisted. */
|
|
60
|
+
passcodeHash?: string | null;
|
|
61
|
+
notifyOnView?: boolean;
|
|
62
|
+
minorPolicyIsMinor: boolean | null;
|
|
63
|
+
minorPolicyResolved: boolean;
|
|
64
|
+
minorPolicyDefaultExpiryDays: number;
|
|
65
|
+
minorPolicyViewCountingEnabled: boolean;
|
|
66
|
+
createdAt: string;
|
|
67
|
+
updatedAt: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Neo4j omits null properties; the repository maps missing values back to null. */
|
|
71
|
+
type ShareLinkProperties = { lockTick?: number } & {
|
|
72
|
+
[Key in keyof ShareLinkRecord]:
|
|
73
|
+
| Exclude<ShareLinkRecord[Key], null>
|
|
74
|
+
| (null extends ShareLinkRecord[Key] ? undefined : never);
|
|
75
|
+
};
|
|
76
|
+
export type ShareLinkInstance = NeogmaInstance<ShareLinkProperties, Record<string, never>>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* `:ShareLink` model registration. The repository writes this label with raw
|
|
80
|
+
* parameterized Cypher so it can hold the share write lock across read + write
|
|
81
|
+
* within one transaction; the model exists for schema/typed instance support and
|
|
82
|
+
* constraint registration.
|
|
83
|
+
*/
|
|
84
|
+
export const ShareLink = ModelFactory<ShareLinkProperties, Record<string, never>>(
|
|
85
|
+
{
|
|
86
|
+
label: 'ShareLink',
|
|
87
|
+
schema: {
|
|
88
|
+
id: { type: 'string', required: true },
|
|
89
|
+
namespace: { type: 'string', required: true },
|
|
90
|
+
ownerProfileId: { type: 'string', required: true },
|
|
91
|
+
version: { type: 'number', required: true },
|
|
92
|
+
contentVersion: { type: 'number', required: true },
|
|
93
|
+
generation: { type: 'number', required: true },
|
|
94
|
+
status: { type: 'string', required: true, enum: ['pending', 'active', 'stopped'] },
|
|
95
|
+
contentState: {
|
|
96
|
+
type: 'string',
|
|
97
|
+
required: true,
|
|
98
|
+
enum: ['staging', 'finalized', 'content_missing'],
|
|
99
|
+
},
|
|
100
|
+
activeObjectRef: { type: 'string', required: false },
|
|
101
|
+
activeObjectOperationId: { type: 'string', required: false },
|
|
102
|
+
activeContentHash: { type: 'string', required: false },
|
|
103
|
+
activeContentBytes: { type: 'number', required: false },
|
|
104
|
+
activeRecoveryHash: { type: 'string', required: false },
|
|
105
|
+
lastOperationId: { type: 'string', required: false },
|
|
106
|
+
createdByClientRequestId: { type: 'string', required: false },
|
|
107
|
+
title: { type: 'string', required: true },
|
|
108
|
+
note: { type: 'string', required: false },
|
|
109
|
+
selectedCount: { type: 'number', required: true },
|
|
110
|
+
expiresAt: { type: 'string', required: false },
|
|
111
|
+
stoppedAt: { type: 'string', required: false },
|
|
112
|
+
viewCount: { type: 'number', required: true },
|
|
113
|
+
lastViewedAt: { type: 'string', required: false },
|
|
114
|
+
passcodeHash: { type: 'string', required: false },
|
|
115
|
+
// LC-2187 records created before opt-in alerts may omit this field.
|
|
116
|
+
notifyOnView: { type: 'boolean', required: false },
|
|
117
|
+
minorPolicyIsMinor: { type: 'boolean', required: false },
|
|
118
|
+
minorPolicyResolved: { type: 'boolean', required: true },
|
|
119
|
+
minorPolicyDefaultExpiryDays: { type: 'number', required: true },
|
|
120
|
+
minorPolicyViewCountingEnabled: { type: 'boolean', required: true },
|
|
121
|
+
createdAt: { type: 'string', required: true },
|
|
122
|
+
updatedAt: { type: 'string', required: true },
|
|
123
|
+
lockTick: { type: 'number', required: false },
|
|
124
|
+
},
|
|
125
|
+
primaryKeyField: 'id',
|
|
126
|
+
},
|
|
127
|
+
neogma
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
export default ShareLink;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { ModelFactory, NeogmaInstance } from 'neogma';
|
|
2
|
+
|
|
3
|
+
import { neogma } from '@instance';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Persisted, short-lived view receipt (LC-2187 ADR-10).
|
|
7
|
+
*
|
|
8
|
+
* Only the hash of the opaque CSPRNG token is stored — never the token, a viewer
|
|
9
|
+
* identity, an IP, a fingerprint, a credential, a key or any request body. The
|
|
10
|
+
* node is bound to one immutable committed tuple
|
|
11
|
+
* `(namespace, ownerProfileId, shareId, contentVersion, objectRef, operationId)`
|
|
12
|
+
* so a receipt for a superseded revision can never count toward the current one.
|
|
13
|
+
*
|
|
14
|
+
* `consumedAt` is the single-use marker; `lockTick` exists only so a transaction
|
|
15
|
+
* can take a write lock on the receipt AFTER it holds the share write lock.
|
|
16
|
+
* `expiresAt` bounds retention and is indexed for one-shot pruning.
|
|
17
|
+
*/
|
|
18
|
+
export type ShareViewReceiptRecord = {
|
|
19
|
+
receiptHash: string;
|
|
20
|
+
namespace: string;
|
|
21
|
+
ownerProfileId: string;
|
|
22
|
+
shareId: string;
|
|
23
|
+
contentVersion: number;
|
|
24
|
+
objectRef: string;
|
|
25
|
+
operationId: string;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
expiresAt: string;
|
|
28
|
+
consumedAt: string | null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type ShareViewReceiptProperties = { lockTick?: number } & {
|
|
32
|
+
[Key in keyof ShareViewReceiptRecord]:
|
|
33
|
+
| Exclude<ShareViewReceiptRecord[Key], null>
|
|
34
|
+
| (null extends ShareViewReceiptRecord[Key] ? undefined : never);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ShareViewReceiptInstance = NeogmaInstance<
|
|
38
|
+
ShareViewReceiptProperties,
|
|
39
|
+
Record<string, never>
|
|
40
|
+
>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `:ShareViewReceipt` model registration. The repository writes this label with
|
|
44
|
+
* raw parameterized Cypher so consume + increment happen in one transaction with
|
|
45
|
+
* explicit share-then-receipt lock ordering; the model exists for schema and
|
|
46
|
+
* constraint registration.
|
|
47
|
+
*/
|
|
48
|
+
export const ShareViewReceipt = ModelFactory<ShareViewReceiptProperties, Record<string, never>>(
|
|
49
|
+
{
|
|
50
|
+
label: 'ShareViewReceipt',
|
|
51
|
+
schema: {
|
|
52
|
+
receiptHash: { type: 'string', required: true },
|
|
53
|
+
namespace: { type: 'string', required: true },
|
|
54
|
+
ownerProfileId: { type: 'string', required: true },
|
|
55
|
+
shareId: { type: 'string', required: true },
|
|
56
|
+
contentVersion: { type: 'number', required: true },
|
|
57
|
+
objectRef: { type: 'string', required: true },
|
|
58
|
+
operationId: { type: 'string', required: true },
|
|
59
|
+
createdAt: { type: 'string', required: true },
|
|
60
|
+
expiresAt: { type: 'string', required: true },
|
|
61
|
+
consumedAt: { type: 'string', required: false },
|
|
62
|
+
lockTick: { type: 'number', required: false },
|
|
63
|
+
},
|
|
64
|
+
primaryKeyField: 'receiptHash',
|
|
65
|
+
},
|
|
66
|
+
neogma
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export default ShareViewReceipt;
|
package/src/models/index.ts
CHANGED
|
@@ -69,6 +69,7 @@ Presentation.addRelationships({
|
|
|
69
69
|
properties: {
|
|
70
70
|
from: { property: 'from', schema: { type: 'string', required: true } },
|
|
71
71
|
date: { property: 'date', schema: { type: 'string', required: true } },
|
|
72
|
+
metadata: { property: 'metadata', schema: { type: 'object', required: false } },
|
|
72
73
|
},
|
|
73
74
|
},
|
|
74
75
|
});
|
|
@@ -256,4 +257,5 @@ export * from './Integration';
|
|
|
256
257
|
export * from './AppStoreListing';
|
|
257
258
|
export * from './CredentialActivity';
|
|
258
259
|
export * from './CredentialRefresh';
|
|
260
|
+
export * from './ShareLink';
|
|
259
261
|
export * from './StatusList';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { neogma } from '@instance';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Uniqueness rules required for the LC-2187 share lifecycle.
|
|
5
|
+
*
|
|
6
|
+
* A uniqueness constraint alone does not serialize two transactions consuming one
|
|
7
|
+
* reservation; the repository additionally holds a `:ShareLink` write lock before
|
|
8
|
+
* reading status/version/reservation state. These rules are the hard backstop that
|
|
9
|
+
* makes the resulting race fail closed instead of forking state.
|
|
10
|
+
*/
|
|
11
|
+
const SHARE_LINK_CONSTRAINT_QUERIES = [
|
|
12
|
+
// A share id is globally unique across namespaces.
|
|
13
|
+
'CREATE CONSTRAINT share_link_id_unique IF NOT EXISTS FOR (s:ShareLink) REQUIRE (s.id) IS UNIQUE',
|
|
14
|
+
// One idempotency record per (namespace, owner, operation kind, client request).
|
|
15
|
+
'CREATE CONSTRAINT share_link_operation_key_unique IF NOT EXISTS FOR (o:ShareLinkOperation) REQUIRE (o.namespace, o.ownerProfileId, o.opKind, o.clientRequestId) IS UNIQUE',
|
|
16
|
+
// At most one in-flight reservation per share.
|
|
17
|
+
'CREATE CONSTRAINT share_link_reservation_share_unique IF NOT EXISTS FOR (r:ShareLinkReservation) REQUIRE (r.shareId) IS UNIQUE',
|
|
18
|
+
// Cleanup is bound to exactly one immutable object reference.
|
|
19
|
+
'CREATE CONSTRAINT share_content_cleanup_object_unique IF NOT EXISTS FOR (c:ShareContentCleanupJob) REQUIRE (c.objectRef) IS UNIQUE',
|
|
20
|
+
// One persisted receipt node per opaque token hash (the token itself is never stored).
|
|
21
|
+
'CREATE CONSTRAINT share_view_receipt_hash_unique IF NOT EXISTS FOR (v:ShareViewReceipt) REQUIRE (v.receiptHash) IS UNIQUE',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Non-unique indexes that keep the lifecycle queries bounded. Index creation is
|
|
26
|
+
* idempotent and shares the same readiness gate as the constraints.
|
|
27
|
+
*/
|
|
28
|
+
const SHARE_LINK_INDEX_QUERIES = [
|
|
29
|
+
'CREATE INDEX share_link_ns_owner_idx IF NOT EXISTS FOR (s:ShareLink) ON (s.namespace, s.ownerProfileId)',
|
|
30
|
+
// Bounded owner-list keyset pagination: filters on the leading
|
|
31
|
+
// (namespace, ownerProfileId) prefix and orders by the immutable
|
|
32
|
+
// (createdAt, id) key without a full scan/sort.
|
|
33
|
+
'CREATE INDEX share_link_ns_owner_created_idx IF NOT EXISTS FOR (s:ShareLink) ON (s.namespace, s.ownerProfileId, s.createdAt, s.id)',
|
|
34
|
+
'CREATE INDEX share_link_status_idx IF NOT EXISTS FOR (s:ShareLink) ON (s.status)',
|
|
35
|
+
'CREATE INDEX share_link_operation_operation_id_idx IF NOT EXISTS FOR (o:ShareLinkOperation) ON (o.operationId)',
|
|
36
|
+
'CREATE INDEX share_link_operation_share_id_idx IF NOT EXISTS FOR (o:ShareLinkOperation) ON (o.shareId)',
|
|
37
|
+
'CREATE INDEX share_link_reservation_lease_idx IF NOT EXISTS FOR (r:ShareLinkReservation) ON (r.leaseExpiresAt)',
|
|
38
|
+
// Bounded recovery discovery enumerates expired reservations for one namespace.
|
|
39
|
+
'CREATE INDEX share_link_reservation_ns_lease_idx IF NOT EXISTS FOR (r:ShareLinkReservation) ON (r.namespace, r.leaseExpiresAt)',
|
|
40
|
+
'CREATE INDEX share_content_cleanup_due_idx IF NOT EXISTS FOR (c:ShareContentCleanupJob) ON (c.status, c.nextAttemptAt)',
|
|
41
|
+
// One-shot receipt pruning is bounded by expiry/consumed state.
|
|
42
|
+
'CREATE INDEX share_view_receipt_expiry_idx IF NOT EXISTS FOR (v:ShareViewReceipt) ON (v.expiresAt)',
|
|
43
|
+
'CREATE INDEX share_view_receipt_share_idx IF NOT EXISTS FOR (v:ShareViewReceipt) ON (v.namespace, v.shareId)',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
type Neo4jSchemaError = {
|
|
47
|
+
code?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const isEquivalentSchemaRuleRace = (error: unknown): boolean =>
|
|
51
|
+
(error as Neo4jSchemaError | undefined)?.code ===
|
|
52
|
+
'Neo.ClientError.Schema.EquivalentSchemaRuleAlreadyExists';
|
|
53
|
+
|
|
54
|
+
const createShareLinkSchema = async (): Promise<void> => {
|
|
55
|
+
for (const query of [...SHARE_LINK_CONSTRAINT_QUERIES, ...SHARE_LINK_INDEX_QUERIES]) {
|
|
56
|
+
try {
|
|
57
|
+
await neogma.queryRunner.run(query);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Concurrent service starts can both observe a missing rule before one
|
|
60
|
+
// wins creation. The losing CREATE is equivalent to the desired ready state.
|
|
61
|
+
if (!isEquivalentSchemaRuleRace(error)) throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let constraintReadiness: Promise<void> | undefined;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Ensures the uniqueness rules and indexes required for the share lifecycle.
|
|
70
|
+
*
|
|
71
|
+
* Every public repository method awaits this before its transaction, so a caller
|
|
72
|
+
* can never observe the "constraints not initialized" window. Concurrent callers
|
|
73
|
+
* share one attempt; a failed attempt is cleared so a later request can retry
|
|
74
|
+
* instead of leaving the repository permanently unavailable.
|
|
75
|
+
*/
|
|
76
|
+
export const ensureShareLinkConstraints = (): Promise<void> => {
|
|
77
|
+
if (!constraintReadiness) {
|
|
78
|
+
const pendingReadiness = createShareLinkSchema();
|
|
79
|
+
constraintReadiness = pendingReadiness;
|
|
80
|
+
|
|
81
|
+
void pendingReadiness.catch(() => {
|
|
82
|
+
if (constraintReadiness === pendingReadiness) constraintReadiness = undefined;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return constraintReadiness;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Exposed for focused tests that assert the exact schema statements. */
|
|
90
|
+
export const SHARE_LINK_SCHEMA_QUERIES = [
|
|
91
|
+
...SHARE_LINK_CONSTRAINT_QUERIES,
|
|
92
|
+
...SHARE_LINK_INDEX_QUERIES,
|
|
93
|
+
] as const;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* LC-2187 public share-link HTTP adapter seam.
|
|
5
|
+
*
|
|
6
|
+
* The Fastify OpenAPI adapter has no `responseMeta` hook, so the no-store
|
|
7
|
+
* contract for the anonymous public share-link routes is applied at the adapter
|
|
8
|
+
* boundary itself. This module is deliberately free of the server bootstrap
|
|
9
|
+
* (which listens on import) so a real Fastify instance can be exercised in
|
|
10
|
+
* tests without starting the application.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Prefixes that identify a mounted public share-link OpenAPI request.
|
|
15
|
+
*
|
|
16
|
+
* The trpc-to-openapi Fastify adapter rewrites `request.raw.url` by stripping
|
|
17
|
+
* its `/api` base path before the handler runs, so the `onSend` hook sees
|
|
18
|
+
* `/public/share-links/...` even though the client requested
|
|
19
|
+
* `/api/public/share-links/...`. Both forms are matched (with a path-boundary
|
|
20
|
+
* check) so a sibling route such as `/public/share-links-other` is not treated
|
|
21
|
+
* as public.
|
|
22
|
+
*/
|
|
23
|
+
const PUBLIC_SHARE_LINK_PREFIXES = ['/api/public/share-links', '/public/share-links'] as const;
|
|
24
|
+
|
|
25
|
+
/** True for the mounted public share-link OpenAPI route prefix only. */
|
|
26
|
+
export const isPublicShareLinkOpenApiRequestUrl = (url: string | undefined): boolean => {
|
|
27
|
+
if (!url) return false;
|
|
28
|
+
|
|
29
|
+
const path = url.split('?')[0] ?? '';
|
|
30
|
+
|
|
31
|
+
return PUBLIC_SHARE_LINK_PREFIXES.some(
|
|
32
|
+
prefix => path === prefix || path.startsWith(`${prefix}/`)
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Installs the adapter-boundary `Cache-Control: private, no-store` on every
|
|
38
|
+
* public share-link OpenAPI response, independent of any upstream cache layer.
|
|
39
|
+
* It is safe to register on any Fastify instance (tRPC-only test apps included).
|
|
40
|
+
*/
|
|
41
|
+
export const registerPublicShareLinkNoStore = (app: FastifyInstance): void => {
|
|
42
|
+
app.addHook('onSend', async (request, reply, payload) => {
|
|
43
|
+
if (isPublicShareLinkOpenApiRequestUrl(request.raw.url)) {
|
|
44
|
+
reply.header('Cache-Control', 'private, no-store');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return payload;
|
|
48
|
+
});
|
|
49
|
+
};
|
package/src/routes/app-store.ts
CHANGED
|
@@ -104,6 +104,7 @@ import {
|
|
|
104
104
|
isDraftBoost,
|
|
105
105
|
appendTemplateEvidenceToCredential,
|
|
106
106
|
} from '@helpers/boost.helpers';
|
|
107
|
+
import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
|
|
107
108
|
import { createBoostForListing } from '@accesslayer/boost/create';
|
|
108
109
|
import { setBoostAsParent } from '@accesslayer/boost/relationships/create';
|
|
109
110
|
import { issueCredentialWithSigningAuthority } from '@helpers/signingAuthority.helpers';
|
|
@@ -692,17 +693,7 @@ export const handleSendCredentialEvent = async (
|
|
|
692
693
|
|
|
693
694
|
const targetDid = getDidWeb(ctx.domain, target.profileId);
|
|
694
695
|
|
|
695
|
-
|
|
696
|
-
unsignedVc.credentialSubject = unsignedVc.credentialSubject.map(subject => ({
|
|
697
|
-
...subject,
|
|
698
|
-
id: targetDid,
|
|
699
|
-
}));
|
|
700
|
-
} else {
|
|
701
|
-
unsignedVc.credentialSubject = {
|
|
702
|
-
...unsignedVc.credentialSubject,
|
|
703
|
-
id: targetDid,
|
|
704
|
-
};
|
|
705
|
-
}
|
|
696
|
+
setCredentialSubjectIds(unsignedVc, targetDid);
|
|
706
697
|
|
|
707
698
|
if (unsignedVc?.type?.includes('BoostCredential')) {
|
|
708
699
|
unsignedVc.boostId = boostUri;
|
package/src/routes/boosts.ts
CHANGED
|
@@ -115,6 +115,7 @@ import {
|
|
|
115
115
|
prepareCredentialFromBoost,
|
|
116
116
|
appendTemplateEvidenceToCredential,
|
|
117
117
|
} from '@helpers/boost.helpers';
|
|
118
|
+
import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
|
|
118
119
|
import {
|
|
119
120
|
BoostValidator,
|
|
120
121
|
BoostGenerateClaimLinkInput,
|
|
@@ -4549,17 +4550,7 @@ export const boostsRouter = t.router({
|
|
|
4549
4550
|
}
|
|
4550
4551
|
unsignedVc.issuer = { id: getDidWeb(ctx.domain, profile.profileId) };
|
|
4551
4552
|
|
|
4552
|
-
|
|
4553
|
-
unsignedVc.credentialSubject = unsignedVc.credentialSubject.map(subject => ({
|
|
4554
|
-
...subject,
|
|
4555
|
-
id: getDidWeb(ctx.domain, targetProfile.profileId),
|
|
4556
|
-
}));
|
|
4557
|
-
} else {
|
|
4558
|
-
unsignedVc.credentialSubject = {
|
|
4559
|
-
...unsignedVc.credentialSubject,
|
|
4560
|
-
id: getDidWeb(ctx.domain, targetProfile.profileId),
|
|
4561
|
-
};
|
|
4562
|
-
}
|
|
4553
|
+
setCredentialSubjectIds(unsignedVc, getDidWeb(ctx.domain, targetProfile.profileId));
|
|
4563
4554
|
if (unsignedVc?.type?.includes('BoostCredential')) unsignedVc.boostId = boostUri;
|
|
4564
4555
|
// Inject OBv3 skill alignments based on boost's framework/skills
|
|
4565
4556
|
await injectObv3AlignmentsIntoCredentialForBoost(unsignedVc, boost, ctx.domain);
|
package/src/routes/contracts.ts
CHANGED
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
} from '@accesslayer/consentflowcontract/relationships/create';
|
|
78
78
|
import { getProfileByDid, getProfileByProfileId } from '@accesslayer/profile/read';
|
|
79
79
|
import { sendBoost, isDraftBoost } from '@helpers/boost.helpers';
|
|
80
|
+
import { setCredentialSubjectIds } from '@helpers/credentialSubject.helpers';
|
|
80
81
|
import { isRelationshipBlocked } from '@helpers/connection.helpers';
|
|
81
82
|
import { getBoostByUri } from '@accesslayer/boost/read';
|
|
82
83
|
import { canProfileIssueBoost } from '@accesslayer/boost/relationships/read';
|
|
@@ -818,17 +819,7 @@ export const contractsRouter = t.router({
|
|
|
818
819
|
unsignedVc.issuanceDate = new Date().toISOString();
|
|
819
820
|
}
|
|
820
821
|
unsignedVc.issuer = { id: getDidWeb(ctx.domain, profile.profileId) };
|
|
821
|
-
|
|
822
|
-
unsignedVc.credentialSubject = unsignedVc.credentialSubject.map(subject => ({
|
|
823
|
-
...subject,
|
|
824
|
-
id: getDidWeb(ctx.domain, otherProfile.profileId),
|
|
825
|
-
}));
|
|
826
|
-
} else {
|
|
827
|
-
unsignedVc.credentialSubject = {
|
|
828
|
-
...unsignedVc.credentialSubject,
|
|
829
|
-
id: getDidWeb(ctx.domain, otherProfile.profileId),
|
|
830
|
-
};
|
|
831
|
-
}
|
|
822
|
+
setCredentialSubjectIds(unsignedVc, getDidWeb(ctx.domain, otherProfile.profileId));
|
|
832
823
|
if (unsignedVc?.type?.includes('BoostCredential')) unsignedVc.boostId = boostUri;
|
|
833
824
|
// Inject OBv3 skill alignments based on boost's framework/skills
|
|
834
825
|
await injectObv3AlignmentsIntoCredentialForBoost(unsignedVc, boost, ctx.domain);
|
package/src/routes/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import jwtDecode from 'jwt-decode';
|
|
|
11
11
|
import * as Sentry from '@sentry/serverless';
|
|
12
12
|
import { ACT_AS_HEADER, AUTH_GRANT_AUDIENCE_DOMAIN_PREFIX } from '@learncard/types';
|
|
13
13
|
import { ContactMethodType } from '@learncard/types';
|
|
14
|
+
import { MAX_SHARE_LINK_REQUEST_BYTES, utf8ByteLength } from '@learncard/types';
|
|
14
15
|
|
|
15
16
|
import { RegExpTransformer } from '@learncard/helpers';
|
|
16
17
|
import { resolveTenantFromRequest, type ResolvedTenant } from '@learncard/email-templates';
|
|
@@ -217,14 +218,93 @@ export const createContext = async (
|
|
|
217
218
|
return { domain, tenant, _guardianApprovalToken, sourceIp, actAs };
|
|
218
219
|
};
|
|
219
220
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
Sentry.configureScope(scope => {
|
|
224
|
-
scope.setTransactionName(`trpc-${path}`);
|
|
225
|
-
});
|
|
226
|
-
return next({ ctx });
|
|
221
|
+
const sentryTransactionNameMiddleware = t.middleware(({ ctx, next, path }) => {
|
|
222
|
+
Sentry.configureScope(scope => {
|
|
223
|
+
scope.setTransactionName(`trpc-${path}`);
|
|
227
224
|
});
|
|
225
|
+
return next({ ctx });
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Sentry's tRPC middleware type predates this repo's generic context. Cast
|
|
229
|
+
// through the tRPC builder's expected parameter type instead of `any`.
|
|
230
|
+
type SentryTrpcMiddleware = Parameters<typeof t.middleware>[0];
|
|
231
|
+
|
|
232
|
+
const sentryInputCaptureMiddleware = Sentry.Handlers.trpcMiddleware({
|
|
233
|
+
attachRpcInput: true,
|
|
234
|
+
}) as unknown as SentryTrpcMiddleware;
|
|
235
|
+
|
|
236
|
+
const sentryNoInputCaptureMiddleware = Sentry.Handlers.trpcMiddleware({
|
|
237
|
+
attachRpcInput: false,
|
|
238
|
+
}) as unknown as SentryTrpcMiddleware;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Complete-request byte bound for owner share-link routes, enforced on the RAW
|
|
242
|
+
* input before Zod parsing/stripping. Individual field validators cannot bound a
|
|
243
|
+
* request whose unknown or duplicate fields are stripped first, so this measures
|
|
244
|
+
* the whole serialized body (UTF-8) and fails closed over the shared 1 MiB cap.
|
|
245
|
+
*
|
|
246
|
+
* It is composed into the base so it runs before authentication and before any
|
|
247
|
+
* input parser, and can never be skipped by a malformed or unauthenticated call.
|
|
248
|
+
*/
|
|
249
|
+
const enforceShareLinkRequestByteBound = t.middleware(async ({ ctx, next, getRawInput }) => {
|
|
250
|
+
const raw = await getRawInput();
|
|
251
|
+
|
|
252
|
+
let bytes: number;
|
|
253
|
+
if (raw === undefined || raw === null) {
|
|
254
|
+
bytes = 0;
|
|
255
|
+
} else if (typeof raw === 'string') {
|
|
256
|
+
bytes = utf8ByteLength(raw);
|
|
257
|
+
} else {
|
|
258
|
+
let serialized: string;
|
|
259
|
+
try {
|
|
260
|
+
serialized = JSON.stringify(raw);
|
|
261
|
+
} catch {
|
|
262
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'invalid share-link request' });
|
|
263
|
+
}
|
|
264
|
+
bytes = utf8ByteLength(serialized);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (bytes > MAX_SHARE_LINK_REQUEST_BYTES) {
|
|
268
|
+
throw new TRPCError({
|
|
269
|
+
code: 'PAYLOAD_TOO_LARGE',
|
|
270
|
+
message: 'share-link request exceeds the 1 MiB limit',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return next({ ctx });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
export const openRoute = t.procedure
|
|
278
|
+
.use(t.middleware(sentryInputCaptureMiddleware))
|
|
279
|
+
.use(sentryTransactionNameMiddleware);
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* LC-2187 public share-link paths must never be cached by an intermediary or a
|
|
283
|
+
* browser. tRPC has no per-procedure header API, so the actual HTTP adapters
|
|
284
|
+
* (Fastify and Lambda, both tRPC and OpenAPI) apply `Cache-Control` from the
|
|
285
|
+
* resolved procedure path through this single helper.
|
|
286
|
+
*/
|
|
287
|
+
export const PUBLIC_SHARE_LINK_ROUTE_PREFIX = 'publicShareLinks.';
|
|
288
|
+
|
|
289
|
+
export const publicShareLinkCacheControlHeaders = (
|
|
290
|
+
paths: readonly string[] | undefined
|
|
291
|
+
): Record<string, string> =>
|
|
292
|
+
(paths ?? []).some(path => path.startsWith(PUBLIC_SHARE_LINK_ROUTE_PREFIX))
|
|
293
|
+
? { 'Cache-Control': 'private, no-store' }
|
|
294
|
+
: {};
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Route base for procedures whose input carries owner-private material (titles,
|
|
298
|
+
* notes, ciphertext envelopes, recovery JWEs) and whose error paths must not
|
|
299
|
+
* attach raw request bodies to Sentry. It runs the same Sentry error/transaction
|
|
300
|
+
* handling but with input attachment disabled, so no branch — malformed,
|
|
301
|
+
* unauthenticated or failing — can capture the payload, and it enforces the
|
|
302
|
+
* complete-request byte bound before input parsing.
|
|
303
|
+
*/
|
|
304
|
+
export const openRouteWithoutInputCapture = t.procedure
|
|
305
|
+
.use(t.middleware(sentryNoInputCaptureMiddleware))
|
|
306
|
+
.use(sentryTransactionNameMiddleware)
|
|
307
|
+
.use(enforceShareLinkRequestByteBound);
|
|
228
308
|
|
|
229
309
|
export const resolveProfileFromContextDid = async (
|
|
230
310
|
didFromContext: string | undefined,
|
|
@@ -316,27 +396,33 @@ const resolveActAs = async (
|
|
|
316
396
|
return { ...user, profile: target, did: target.did, onBehalfOf: profile.profileId };
|
|
317
397
|
};
|
|
318
398
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
399
|
+
const withDid = (base: typeof openRoute) =>
|
|
400
|
+
base.use(async ({ ctx, next }) => {
|
|
401
|
+
if (!ctx.user?.did) {
|
|
402
|
+
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
403
|
+
}
|
|
323
404
|
|
|
324
|
-
|
|
405
|
+
const profile = await resolveProfileFromContextDid(ctx.user.did, ctx.domain);
|
|
325
406
|
|
|
326
|
-
|
|
407
|
+
if (profile) Sentry.setUser({ id: profile.profileId, username: profile.displayName });
|
|
327
408
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
409
|
+
const user = ctx.user.isChallengeValid
|
|
410
|
+
? await resolveActAs({ ...ctx.user, profile }, ctx.actAs, ctx.domain)
|
|
411
|
+
: { ...ctx.user, profile };
|
|
331
412
|
|
|
332
|
-
|
|
333
|
-
});
|
|
413
|
+
return next({ ctx: { ...ctx, user } });
|
|
414
|
+
});
|
|
334
415
|
|
|
335
|
-
export const
|
|
336
|
-
if (!ctx.user?.isChallengeValid) throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
416
|
+
export const didRoute = withDid(openRoute);
|
|
337
417
|
|
|
338
|
-
|
|
339
|
-
})
|
|
418
|
+
const withChallenge = (base: typeof didRoute) =>
|
|
419
|
+
base.use(({ ctx, next }) => {
|
|
420
|
+
if (!ctx.user?.isChallengeValid) throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
421
|
+
|
|
422
|
+
return next({ ctx: { ...ctx, user: ctx.user } });
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
export const didAndChallengeRoute = withChallenge(didRoute);
|
|
340
426
|
|
|
341
427
|
export const scopedRoute = didAndChallengeRoute.use(({ ctx, next, meta }) => {
|
|
342
428
|
if (!meta?.requiredScope) {
|
|
@@ -370,33 +456,41 @@ export const openProfileRoute = didRoute.use(async ({ ctx, next }) => {
|
|
|
370
456
|
return next({ ctx: { ...ctx, user: { ...ctx.user, profile } } });
|
|
371
457
|
});
|
|
372
458
|
|
|
373
|
-
|
|
374
|
-
|
|
459
|
+
const withProfile = (base: typeof didAndChallengeRoute) =>
|
|
460
|
+
base.use(async ({ ctx, next, meta }) => {
|
|
461
|
+
const { profile } = ctx.user;
|
|
375
462
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
463
|
+
if (!profile) {
|
|
464
|
+
throw new TRPCError({
|
|
465
|
+
code: 'NOT_FOUND',
|
|
466
|
+
message: 'Profile not found. Please make a profile!',
|
|
467
|
+
});
|
|
468
|
+
}
|
|
382
469
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
470
|
+
if (!meta?.requiredScope) {
|
|
471
|
+
return next({ ctx: { ...ctx, user: { ...ctx.user, profile } } });
|
|
472
|
+
}
|
|
386
473
|
|
|
387
|
-
|
|
474
|
+
const userScope = ctx.user?.scope || AUTH_GRANT_NO_ACCESS_SCOPE;
|
|
388
475
|
|
|
389
|
-
|
|
476
|
+
const hasRequiredScope = userHasRequiredScopes(userScope, meta.requiredScope);
|
|
390
477
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
478
|
+
if (!hasRequiredScope) {
|
|
479
|
+
throw new TRPCError({
|
|
480
|
+
code: 'UNAUTHORIZED',
|
|
481
|
+
message: `This operation requires ${meta.requiredScope} scope`,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
397
484
|
|
|
398
|
-
|
|
399
|
-
});
|
|
485
|
+
return next({ ctx: { ...ctx, user: { ...ctx.user, profile } } });
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
export const profileRoute = withProfile(didAndChallengeRoute);
|
|
489
|
+
|
|
490
|
+
/** Same auth chain, composed on a base that never attaches private RPC input to Sentry. */
|
|
491
|
+
export const didRouteWithoutInputCapture = withDid(openRouteWithoutInputCapture);
|
|
492
|
+
export const didAndChallengeRouteWithoutInputCapture = withChallenge(didRouteWithoutInputCapture);
|
|
493
|
+
export const profileRouteWithoutInputCapture = withProfile(didAndChallengeRouteWithoutInputCapture);
|
|
400
494
|
|
|
401
495
|
export const openProfileManagerRoute = openRoute.use(async ({ ctx, next }) => {
|
|
402
496
|
if (!ctx.user?.did) throw new TRPCError({ code: 'UNAUTHORIZED' });
|
|
@@ -460,7 +554,8 @@ const GUARDIAN_APPROVAL_CLOCK_SKEW_SECONDS = 60;
|
|
|
460
554
|
export const guardianGatedRoute = profileRoute.use(async ({ ctx, next }) => {
|
|
461
555
|
const { profile } = ctx.user;
|
|
462
556
|
const guardianApprovalToken = ctx._guardianApprovalToken;
|
|
463
|
-
|
|
557
|
+
// Service profiles (orgs) are managed for admin purposes, not guardianship.
|
|
558
|
+
const isChildAccount = !profile.isServiceProfile && (await isProfileManaged(profile.profileId));
|
|
464
559
|
let guardianIdentity: { profileId: string; did: string } | undefined;
|
|
465
560
|
|
|
466
561
|
if (isChildAccount && guardianApprovalToken) {
|