@learncard/network-brain-service 3.19.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +44 -111
- package/AGENTS.md +52 -0
- package/CHANGELOG.md +109 -0
- package/README.md +9 -2
- package/bootstrapLambda.test.ts +72 -0
- package/config/config.dev.json +29 -0
- package/config/config.example.json +29 -0
- package/config/config.json +1 -0
- package/config/config.production.json +29 -0
- package/config/config.scouts.dev.json +24 -0
- package/config/config.scouts.production.json +24 -0
- package/contractEventsLambda.ts +18 -0
- package/contractEventsLambdaApp.ts +19 -0
- package/didWebLambda.ts +13 -18
- package/didWebLambdaApp.ts +22 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts +10 -0
- package/dist/accesslayer/app-manifest-version/create.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts +11 -0
- package/dist/accesslayer/app-manifest-version/read.d.ts.map +1 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts +7 -0
- package/dist/accesslayer/app-manifest-version/update.d.ts.map +1 -0
- package/dist/accesslayer/app-store-listing/read.d.ts +2 -1
- package/dist/accesslayer/app-store-listing/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts +2 -0
- package/dist/accesslayer/app-store-listing/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/app-store-listing/update.d.ts +1 -0
- package/dist/accesslayer/app-store-listing/update.d.ts.map +1 -1
- package/dist/accesslayer/boost/relationships/create.d.ts +2 -1
- package/dist/accesslayer/boost/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts +9 -1
- package/dist/accesslayer/consentflowcontract/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/read.d.ts +34 -3
- package/dist/accesslayer/consentflowcontract/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts +4 -1
- package/dist/accesslayer/consentflowcontract/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/delete.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts +3 -2
- package/dist/accesslayer/consentflowcontract/relationships/read.d.ts.map +1 -1
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/recipients.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts +12 -0
- package/dist/accesslayer/consentflowcontract/relationships/smartResume.d.ts.map +1 -0
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts +8 -5
- package/dist/accesslayer/consentflowcontract/relationships/update.d.ts.map +1 -1
- package/dist/accesslayer/credential/create.d.ts +2 -1
- package/dist/accesslayer/credential/create.d.ts.map +1 -1
- package/dist/accesslayer/credential/relationships/create.d.ts +4 -3
- package/dist/accesslayer/credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/relationships/create.d.ts.map +1 -1
- package/dist/accesslayer/inbox-credential/update.d.ts +2 -1
- package/dist/accesslayer/inbox-credential/update.d.ts.map +1 -1
- package/dist/accesslayer/share-link/abandon.d.ts.map +1 -1
- package/dist/accesslayer/share-link/attachment.d.ts +37 -0
- package/dist/accesslayer/share-link/attachment.d.ts.map +1 -0
- package/dist/accesslayer/share-link/finalize.d.ts.map +1 -1
- package/dist/accesslayer/share-link/helpers.d.ts +3 -0
- package/dist/accesslayer/share-link/helpers.d.ts.map +1 -1
- package/dist/accesslayer/share-link/recovery.d.ts.map +1 -1
- package/dist/accesslayer/share-link/reserve.d.ts.map +1 -1
- package/dist/accesslayer/share-link/revoke.d.ts.map +1 -1
- package/dist/accesslayer/share-link/types.d.ts +8 -3
- package/dist/accesslayer/share-link/types.d.ts.map +1 -1
- package/dist/cache/app-store.caches.d.ts +49 -0
- package/dist/cache/app-store.caches.d.ts.map +1 -1
- package/dist/config/applyDockerStageConfig.d.ts +2 -0
- package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
- package/dist/config/environment.d.ts +5 -0
- package/dist/config/environment.d.ts.map +1 -1
- package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
- package/dist/config/stageConfig.d.ts +112 -0
- package/dist/config/stageConfig.d.ts.map +1 -0
- package/dist/config/stageConfig.test.d.ts +2 -0
- package/dist/config/stageConfig.test.d.ts.map +1 -0
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/docker-entry.d.ts +1 -1
- package/dist/docker-entry.d.ts.map +1 -1
- package/dist/helpers/app-manifest.helpers.d.ts +5 -0
- package/dist/helpers/app-manifest.helpers.d.ts.map +1 -0
- package/dist/helpers/claim-hooks.helpers.d.ts +2 -1
- package/dist/helpers/claim-hooks.helpers.d.ts.map +1 -1
- package/dist/helpers/consent-referral.helpers.d.ts +2 -0
- package/dist/helpers/consent-referral.helpers.d.ts.map +1 -0
- package/dist/helpers/consent-scopes.helpers.d.ts +7 -0
- package/dist/helpers/consent-scopes.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts +3 -0
- package/dist/helpers/contract-event-maintenance.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-events.helpers.d.ts +25 -0
- package/dist/helpers/contract-events.helpers.d.ts.map +1 -0
- package/dist/helpers/contract-requests.helpers.d.ts +19 -0
- package/dist/helpers/contract-requests.helpers.d.ts.map +1 -0
- package/dist/helpers/finalize-inbox.helpers.d.ts +3 -3
- package/dist/helpers/finalize-inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inbox.helpers.d.ts.map +1 -1
- package/dist/helpers/inline-template.helpers.d.ts +45 -0
- package/dist/helpers/inline-template.helpers.d.ts.map +1 -0
- package/dist/helpers/notificationMessages.d.ts +1 -1
- package/dist/helpers/notificationMessages.d.ts.map +1 -1
- package/dist/helpers/notificationQueue.helpers.d.ts.map +1 -1
- package/dist/helpers/notifications.helpers.d.ts +6 -0
- package/dist/helpers/notifications.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-link-attachment-access.d.ts +4 -0
- package/dist/helpers/share-link-attachment-access.d.ts.map +1 -0
- package/dist/helpers/share-link-attachment.d.ts +18 -0
- package/dist/helpers/share-link-attachment.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts +5 -0
- package/dist/helpers/share-link-coordinator/attachment-stat.d.ts.map +1 -0
- package/dist/helpers/share-link-coordinator/coordinator.d.ts.map +1 -1
- package/dist/helpers/share-link-coordinator/recovery-runner.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/runner.d.ts +1 -0
- package/dist/helpers/share-link-maintenance/runner.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/runtime.d.ts +2 -0
- package/dist/helpers/share-link-maintenance/runtime.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/telemetry.d.ts.map +1 -1
- package/dist/helpers/share-link-maintenance/types.d.ts +4 -1
- package/dist/helpers/share-link-maintenance/types.d.ts.map +1 -1
- package/dist/helpers/share-link-owner-projection.d.ts.map +1 -1
- package/dist/helpers/share-link-policy/production.d.ts +3 -5
- package/dist/helpers/share-link-policy/production.d.ts.map +1 -1
- package/dist/helpers/share-link-policy/resolver.d.ts +1 -1
- package/dist/helpers/share-link-policy/resolver.d.ts.map +1 -1
- package/dist/helpers/share-link-policy/types.d.ts +8 -2
- package/dist/helpers/share-link-policy/types.d.ts.map +1 -1
- package/dist/helpers/signingAuthority.helpers.d.ts.map +1 -1
- package/dist/helpers/smartResume.helpers.d.ts +3 -0
- package/dist/helpers/smartResume.helpers.d.ts.map +1 -0
- package/dist/index.js +2 -2
- package/dist/models/AppManifestVersion.d.ts +20 -0
- package/dist/models/AppManifestVersion.d.ts.map +1 -0
- package/dist/models/AppStoreListing.d.ts +13 -0
- package/dist/models/AppStoreListing.d.ts.map +1 -1
- package/dist/models/ConsentFlowContract.d.ts +13 -2
- package/dist/models/ConsentFlowContract.d.ts.map +1 -1
- package/dist/models/ConsentFlowTerms.d.ts.map +1 -1
- package/dist/models/ConsentFlowTransaction.d.ts.map +1 -1
- package/dist/models/Integration.d.ts +2 -0
- package/dist/models/Integration.d.ts.map +1 -1
- package/dist/models/ShareLink.d.ts +2 -0
- package/dist/models/ShareLink.d.ts.map +1 -1
- 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.map +1 -1
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/routes/app-store.d.ts +619 -0
- package/dist/routes/app-store.d.ts.map +1 -1
- package/dist/routes/boosts.d.ts +2 -2
- package/dist/routes/boosts.d.ts.map +1 -1
- package/dist/routes/contracts.d.ts +146 -3
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/credentials.d.ts +34 -0
- package/dist/routes/credentials.d.ts.map +1 -1
- package/dist/routes/inbox.d.ts +52 -2
- package/dist/routes/inbox.d.ts.map +1 -1
- package/dist/routes/index.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 +55 -0
- package/dist/routes/public-share-links.d.ts.map +1 -1
- package/dist/routes/share-links.d.ts +175 -14
- package/dist/routes/share-links.d.ts.map +1 -1
- package/dist/routes/test.d.ts +5 -0
- package/dist/routes/test.d.ts.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/types/app-manifest.d.ts +339 -0
- package/dist/types/app-manifest.d.ts.map +1 -0
- package/dist/types/app-store-listing.d.ts +34 -0
- package/dist/types/app-store-listing.d.ts.map +1 -1
- package/dist/types/consentflowcontract.d.ts +30 -3
- package/dist/types/consentflowcontract.d.ts.map +1 -1
- package/dist/types/credential-refresh.d.ts +1 -1
- package/esbuildPlugins.cjs +1 -0
- package/lambda.ts +49 -195
- package/lambdaApp.ts +203 -0
- package/package.json +20 -18
- package/scripts/export-openapi.ts +20 -0
- package/scripts/offline-schema-environment.ts +18 -0
- package/scripts/verify-python-contracts.ts +72 -0
- package/serverless-local.yml +3 -6
- package/serverless.function-env.cjs +50 -0
- package/serverless.local.yml +0 -1
- package/serverless.yml +27 -74
- package/shareLinkMaintenanceLambda.ts +13 -44
- package/shareLinkMaintenanceLambdaApp.ts +50 -0
- package/src/accesslayer/app-manifest-version/create.ts +57 -0
- package/src/accesslayer/app-manifest-version/read.ts +129 -0
- package/src/accesslayer/app-manifest-version/update.ts +61 -0
- package/src/accesslayer/app-store-listing/read.ts +6 -0
- package/src/accesslayer/app-store-listing/relationships/create.ts +20 -0
- package/src/accesslayer/app-store-listing/relationships/read.ts +24 -1
- package/src/accesslayer/app-store-listing/update.ts +15 -0
- package/src/accesslayer/boost/relationships/create.ts +8 -2
- package/src/accesslayer/consentflowcontract/create.ts +81 -8
- package/src/accesslayer/consentflowcontract/read.ts +84 -10
- package/src/accesslayer/consentflowcontract/relationships/create.ts +116 -93
- package/src/accesslayer/consentflowcontract/relationships/delete.ts +1 -0
- package/src/accesslayer/consentflowcontract/relationships/read.ts +184 -108
- package/src/accesslayer/consentflowcontract/relationships/recipients.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/smartResume.ts +125 -0
- package/src/accesslayer/consentflowcontract/relationships/update.ts +240 -189
- package/src/accesslayer/credential/create.test.ts +2 -115
- package/src/accesslayer/credential/create.ts +11 -6
- package/src/accesslayer/credential/relationships/create.ts +17 -10
- package/src/accesslayer/inbox-credential/relationships/create.ts +70 -48
- package/src/accesslayer/inbox-credential/update.ts +4 -2
- package/src/accesslayer/share-link/abandon.ts +20 -0
- package/src/accesslayer/share-link/attachment.ts +360 -0
- package/src/accesslayer/share-link/finalize.ts +55 -7
- package/src/accesslayer/share-link/helpers.ts +33 -2
- package/src/accesslayer/share-link/recovery.ts +20 -0
- package/src/accesslayer/share-link/reserve.ts +85 -6
- package/src/accesslayer/share-link/revoke.ts +35 -0
- package/src/accesslayer/share-link/types.ts +9 -4
- package/src/cache/app-store.caches.ts +35 -0
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/environment.test.ts +17 -12
- package/src/config/environment.ts +1 -0
- package/src/config/serverlessFunctionEnv.test.ts +105 -0
- package/src/config/stageConfig.test.ts +25 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/docker-entry.ts +24 -0
- package/src/helpers/app-manifest.helpers.ts +185 -0
- package/src/helpers/claim-hooks.helpers.ts +20 -7
- package/src/helpers/consent-referral.helpers.ts +6 -0
- package/src/helpers/consent-scopes.helpers.ts +58 -0
- package/src/helpers/contract-event-maintenance.helpers.ts +43 -0
- package/src/helpers/contract-events.helpers.ts +540 -0
- package/src/helpers/contract-requests.helpers.ts +211 -0
- package/src/helpers/finalize-inbox.helpers.ts +163 -44
- package/src/helpers/inbox.helpers.ts +51 -66
- package/src/helpers/inline-template.helpers.ts +560 -0
- package/src/helpers/notificationMessages.ts +59 -0
- package/src/helpers/notificationQueue.helpers.ts +23 -5
- package/src/helpers/notifications.helpers.ts +25 -1
- package/src/helpers/profile.helpers.ts +6 -0
- package/src/helpers/share-link-attachment-access.ts +87 -0
- package/src/helpers/share-link-attachment.ts +115 -0
- package/src/helpers/share-link-coordinator/attachment-stat.ts +41 -0
- package/src/helpers/share-link-coordinator/coordinator.ts +15 -2
- package/src/helpers/share-link-coordinator/recovery-runner.ts +15 -0
- package/src/helpers/share-link-maintenance/runner.ts +28 -0
- package/src/helpers/share-link-maintenance/runtime.ts +6 -0
- package/src/helpers/share-link-maintenance/telemetry.ts +3 -0
- package/src/helpers/share-link-maintenance/types.ts +2 -0
- package/src/helpers/share-link-owner-projection.ts +2 -0
- package/src/helpers/share-link-policy/production.ts +37 -17
- package/src/helpers/share-link-policy/resolver.ts +16 -11
- package/src/helpers/share-link-policy/types.ts +15 -7
- package/src/helpers/signingAuthority.helpers.ts +23 -14
- package/src/helpers/smartResume.helpers.ts +123 -0
- package/src/models/AppManifestVersion.ts +43 -0
- package/src/models/AppStoreListing.ts +32 -0
- package/src/models/ConsentFlowContract.ts +34 -3
- package/src/models/ConsentFlowTerms.ts +10 -0
- package/src/models/ConsentFlowTransaction.ts +3 -0
- package/src/models/Integration.ts +2 -0
- package/src/models/ShareLink.ts +4 -0
- package/src/models/index.ts +23 -0
- package/src/models/share-link-constraints.ts +5 -0
- package/src/openapi.ts +44 -8
- package/src/routes/app-store.ts +1054 -27
- package/src/routes/boosts.ts +12 -14
- package/src/routes/contracts.ts +382 -262
- package/src/routes/inbox.ts +4 -2
- package/src/routes/index.ts +4 -1
- package/src/routes/profile-manager.ts +19 -0
- package/src/routes/profiles.ts +12 -0
- package/src/routes/public-share-links.ts +169 -0
- package/src/routes/share-links.ts +69 -0
- package/src/routes/workflows.ts +3 -2
- package/src/types/app-manifest.ts +28 -0
- package/src/types/app-store-listing.ts +13 -0
- package/src/types/consentflowcontract.ts +25 -4
- package/test/app-manifest.helpers.spec.ts +170 -0
- package/test/app-manifest.routes.spec.ts +299 -0
- package/test/app-store-listing-updates.spec.ts +236 -0
- package/test/app-store-listing.spec.ts +65 -3
- package/test/consent-data-boundaries.spec.ts +352 -0
- package/test/consent-recipients.spec.ts +513 -0
- package/test/consent-scopes.helpers.spec.ts +67 -0
- package/test/consentflow.spec.ts +63 -2
- package/test/contract-requests.spec.ts +1588 -0
- package/test/credential-storage.neo4j.spec.ts +122 -0
- package/test/guardian-gated-routes.spec.ts +36 -0
- package/test/inbox-security.e2e.ts +26 -10
- package/test/inbox.spec.ts +184 -8
- package/test/inline-template.helpers.spec.ts +49 -0
- package/test/managed-recipient-inbox.spec.ts +24 -2
- package/test/openapi.spec.ts +14 -73
- package/test/profiles.spec.ts +30 -0
- package/test/share-link-attachment.neo4j.spec.ts +262 -0
- package/test/share-link-attachment.unit.spec.ts +244 -0
- package/test/share-link-lifecycle.neo4j.spec.ts +145 -2
- package/test/share-link-maintenance.unit.spec.ts +32 -5
- package/test/share-link-owner-route.unit.spec.ts +70 -2
- package/test/share-link-policy.unit.spec.ts +18 -4
- package/test/share-link-production-policy.unit.spec.ts +58 -9
- package/test/share-link-public-route.unit.spec.ts +279 -0
- package/test/share-link-service-policy.neo4j.spec.ts +403 -0
- package/test/signing-authority-encryption.spec.ts +75 -0
- package/test/smart-resume-consent.spec.ts +233 -0
- package/test/smart-resume-upload.spec.ts +65 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -0
- package/vitest.config.ts +13 -0
- package/vitest.inbox-e2e.config.ts +2 -0
- package/workerLambda.test.ts +100 -0
|
@@ -7,10 +7,12 @@ import {
|
|
|
7
7
|
DEFAULT_SHARE_LINK_POLICY,
|
|
8
8
|
type ShareLinkPolicySnapshot,
|
|
9
9
|
} from '@helpers/share-link-policy/types';
|
|
10
|
+
import { mergeShareLinkPolicyConservatively } from '@helpers/share-link-policy/resolver';
|
|
10
11
|
|
|
11
12
|
import { ensureShareLinkConstraints } from '../../models/share-link-constraints';
|
|
12
13
|
import type { ShareLinkRecord } from '../../models/ShareLink';
|
|
13
14
|
import { failShareLink } from './errors';
|
|
15
|
+
import { pinShareAttachment, queueShareAttachmentCleanup } from './attachment';
|
|
14
16
|
import {
|
|
15
17
|
deleteReservation,
|
|
16
18
|
enqueueCleanupJob,
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
readOperation,
|
|
22
24
|
readReservation,
|
|
23
25
|
readShareById,
|
|
26
|
+
toShareLinkOperationPolicy,
|
|
24
27
|
toShareLinkOperationRecord,
|
|
25
28
|
toShareLinkRecord,
|
|
26
29
|
toShareLinkReservationRecord,
|
|
@@ -60,6 +63,8 @@ const parseRecordedResult = (
|
|
|
60
63
|
};
|
|
61
64
|
|
|
62
65
|
type ReservationProps = {
|
|
66
|
+
attachmentId?: string | null;
|
|
67
|
+
attachmentChunkCount?: number | null;
|
|
63
68
|
shareId: string;
|
|
64
69
|
namespace: string;
|
|
65
70
|
ownerProfileId: string;
|
|
@@ -96,6 +101,20 @@ const writeReservation = async (
|
|
|
96
101
|
tx: ShareLinkTransaction,
|
|
97
102
|
props: ReservationProps
|
|
98
103
|
): Promise<void> => {
|
|
104
|
+
if (props.contentVersion !== null && props.attachmentId && props.attachmentChunkCount) {
|
|
105
|
+
await pinShareAttachment(
|
|
106
|
+
tx,
|
|
107
|
+
{
|
|
108
|
+
namespace: props.namespace,
|
|
109
|
+
ownerProfileId: props.ownerProfileId,
|
|
110
|
+
shareId: props.shareId,
|
|
111
|
+
contentVersion: props.contentVersion,
|
|
112
|
+
attachmentId: props.attachmentId,
|
|
113
|
+
chunkCount: props.attachmentChunkCount,
|
|
114
|
+
},
|
|
115
|
+
props.requestHash
|
|
116
|
+
);
|
|
117
|
+
}
|
|
99
118
|
await tx.run('CREATE (r:ShareLinkReservation) SET r = $props', { props });
|
|
100
119
|
};
|
|
101
120
|
|
|
@@ -120,9 +139,30 @@ const bumpShareGeneration = async (
|
|
|
120
139
|
const supersedeReservation = async (
|
|
121
140
|
tx: ShareLinkTransaction,
|
|
122
141
|
reservationProps: Record<string, unknown>,
|
|
123
|
-
now: string
|
|
142
|
+
now: string,
|
|
143
|
+
retainAttachment = false
|
|
124
144
|
): Promise<ShareLinkReservationRecord> => {
|
|
125
145
|
const reservation = toShareLinkReservationRecord(reservationProps);
|
|
146
|
+
if (
|
|
147
|
+
!retainAttachment &&
|
|
148
|
+
reservation.attachmentId &&
|
|
149
|
+
reservation.attachmentChunkCount &&
|
|
150
|
+
reservation.contentVersion
|
|
151
|
+
) {
|
|
152
|
+
await queueShareAttachmentCleanup(
|
|
153
|
+
tx,
|
|
154
|
+
{
|
|
155
|
+
namespace: reservation.namespace,
|
|
156
|
+
ownerProfileId: reservation.ownerProfileId,
|
|
157
|
+
shareId: reservation.shareId,
|
|
158
|
+
contentVersion: reservation.contentVersion,
|
|
159
|
+
attachmentId: reservation.attachmentId,
|
|
160
|
+
chunkCount: reservation.attachmentChunkCount,
|
|
161
|
+
},
|
|
162
|
+
'abandoned',
|
|
163
|
+
now
|
|
164
|
+
);
|
|
165
|
+
}
|
|
126
166
|
|
|
127
167
|
if (reservation.objectRef) {
|
|
128
168
|
await enqueueCleanupJob(tx, {
|
|
@@ -214,6 +254,7 @@ export const reserveCreate = async (input: ReserveCreateInput): Promise<ReserveS
|
|
|
214
254
|
};
|
|
215
255
|
}
|
|
216
256
|
|
|
257
|
+
let retryPolicy = toShareLinkOperationPolicy(existingOperation);
|
|
217
258
|
const existingReservationProps = await readReservation(tx, input.shareId);
|
|
218
259
|
|
|
219
260
|
if (existingReservationProps) {
|
|
@@ -230,6 +271,10 @@ export const reserveCreate = async (input: ReserveCreateInput): Promise<ReserveS
|
|
|
230
271
|
);
|
|
231
272
|
}
|
|
232
273
|
|
|
274
|
+
retryPolicy = retryPolicy
|
|
275
|
+
? mergeShareLinkPolicyConservatively(retryPolicy, existingReservation.policy)
|
|
276
|
+
: existingReservation.policy;
|
|
277
|
+
|
|
233
278
|
if (isLeaseActive(existingReservation.leaseExpiresAt, now)) {
|
|
234
279
|
if (!share) {
|
|
235
280
|
failShareLink('CONFLICT', 'in-flight reservation exists without a share');
|
|
@@ -245,7 +290,7 @@ export const reserveCreate = async (input: ReserveCreateInput): Promise<ReserveS
|
|
|
245
290
|
|
|
246
291
|
// Same logical operation, expired lease: retain its expiry.
|
|
247
292
|
supersededExpiresAt = { value: existingReservation.expiresAt };
|
|
248
|
-
await supersedeReservation(tx, existingReservationProps, nowIso);
|
|
293
|
+
await supersedeReservation(tx, existingReservationProps, nowIso, true);
|
|
249
294
|
}
|
|
250
295
|
|
|
251
296
|
if (!share) {
|
|
@@ -255,6 +300,10 @@ export const reserveCreate = async (input: ReserveCreateInput): Promise<ReserveS
|
|
|
255
300
|
failShareLink('CONFLICT', 'cannot re-drive create for a stopped share');
|
|
256
301
|
}
|
|
257
302
|
|
|
303
|
+
const policy = mergeShareLinkPolicyConservatively(
|
|
304
|
+
retryPolicy ?? DEFAULT_SHARE_LINK_POLICY,
|
|
305
|
+
input.policy ?? retryPolicy ?? DEFAULT_SHARE_LINK_POLICY
|
|
306
|
+
);
|
|
258
307
|
return createReservationForShare(tx, {
|
|
259
308
|
share,
|
|
260
309
|
opKey,
|
|
@@ -264,14 +313,17 @@ export const reserveCreate = async (input: ReserveCreateInput): Promise<ReserveS
|
|
|
264
313
|
contentVersion: 1,
|
|
265
314
|
baseContentVersion: share.contentVersion,
|
|
266
315
|
content: input.content,
|
|
316
|
+
attachment: input.attachment,
|
|
267
317
|
selectedCount: input.selectedCount,
|
|
268
318
|
title: input.title,
|
|
269
319
|
note: input.note ?? null,
|
|
270
320
|
expiresAt:
|
|
271
321
|
supersededExpiresAt !== null ? supersededExpiresAt.value : share.expiresAt,
|
|
272
322
|
passcodeHash: share.passcodeHash ?? input.passcodeHash ?? null,
|
|
273
|
-
notifyOnView:
|
|
274
|
-
|
|
323
|
+
notifyOnView:
|
|
324
|
+
(share.notifyOnView ?? input.notifyOnView ?? false) &&
|
|
325
|
+
policy.viewCountingEnabled,
|
|
326
|
+
policy,
|
|
275
327
|
leaseOwner: input.leaseOwner,
|
|
276
328
|
leaseExpiresAt,
|
|
277
329
|
nowIso,
|
|
@@ -335,6 +387,7 @@ export const reserveCreate = async (input: ReserveCreateInput): Promise<ReserveS
|
|
|
335
387
|
operationId,
|
|
336
388
|
shareId: input.shareId,
|
|
337
389
|
requestHash: input.requestHash,
|
|
390
|
+
policy,
|
|
338
391
|
now: nowIso,
|
|
339
392
|
pruneAfter,
|
|
340
393
|
});
|
|
@@ -349,6 +402,8 @@ export const reserveCreate = async (input: ReserveCreateInput): Promise<ReserveS
|
|
|
349
402
|
objectRef,
|
|
350
403
|
contentVersion: 1,
|
|
351
404
|
baseVersion: 1,
|
|
405
|
+
attachmentId: input.attachment?.id ?? null,
|
|
406
|
+
attachmentChunkCount: input.attachment?.chunkCount ?? null,
|
|
352
407
|
baseContentVersion: 1,
|
|
353
408
|
contentHash: input.content.contentHash,
|
|
354
409
|
contentBytes: input.content.contentBytes,
|
|
@@ -388,6 +443,7 @@ export const reserveCreate = async (input: ReserveCreateInput): Promise<ReserveS
|
|
|
388
443
|
};
|
|
389
444
|
|
|
390
445
|
type CreateReservationForShareInput = {
|
|
446
|
+
attachment?: import('@learncard/types').ShareLinkAttachment;
|
|
391
447
|
share: ShareLinkRecord;
|
|
392
448
|
opKey: ShareLinkOperationKey;
|
|
393
449
|
operationId: string;
|
|
@@ -420,6 +476,7 @@ const createReservationForShare = async (
|
|
|
420
476
|
operationId: input.operationId,
|
|
421
477
|
shareId: input.share.id,
|
|
422
478
|
requestHash: input.requestHash,
|
|
479
|
+
policy: input.policy,
|
|
423
480
|
now: input.nowIso,
|
|
424
481
|
pruneAfter: input.pruneAfter,
|
|
425
482
|
});
|
|
@@ -434,6 +491,14 @@ const createReservationForShare = async (
|
|
|
434
491
|
operationId: input.operationId,
|
|
435
492
|
objectRef: input.objectRef,
|
|
436
493
|
contentVersion: input.contentVersion,
|
|
494
|
+
attachmentId:
|
|
495
|
+
input.contentVersion !== null
|
|
496
|
+
? (input.attachment?.id ?? null)
|
|
497
|
+
: (input.share.attachmentId ?? null),
|
|
498
|
+
attachmentChunkCount:
|
|
499
|
+
input.contentVersion !== null
|
|
500
|
+
? (input.attachment?.chunkCount ?? null)
|
|
501
|
+
: (input.share.attachmentChunkCount ?? null),
|
|
437
502
|
baseVersion: input.share.version,
|
|
438
503
|
baseContentVersion: input.baseContentVersion,
|
|
439
504
|
contentHash: input.content ? input.content.contentHash : null,
|
|
@@ -539,6 +604,7 @@ export const reserveReplacement = async (
|
|
|
539
604
|
};
|
|
540
605
|
}
|
|
541
606
|
|
|
607
|
+
let retryPolicy = toShareLinkOperationPolicy(existingOperation);
|
|
542
608
|
const existingReservationProps = await readReservation(tx, input.shareId);
|
|
543
609
|
|
|
544
610
|
if (existingReservationProps) {
|
|
@@ -555,6 +621,10 @@ export const reserveReplacement = async (
|
|
|
555
621
|
);
|
|
556
622
|
}
|
|
557
623
|
|
|
624
|
+
retryPolicy = retryPolicy
|
|
625
|
+
? mergeShareLinkPolicyConservatively(retryPolicy, existingReservation.policy)
|
|
626
|
+
: existingReservation.policy;
|
|
627
|
+
|
|
558
628
|
if (isLeaseActive(existingReservation.leaseExpiresAt, now)) {
|
|
559
629
|
return {
|
|
560
630
|
outcome: 'reserved',
|
|
@@ -564,12 +634,20 @@ export const reserveReplacement = async (
|
|
|
564
634
|
};
|
|
565
635
|
}
|
|
566
636
|
|
|
567
|
-
await supersedeReservation(tx, existingReservationProps, nowIso);
|
|
637
|
+
await supersedeReservation(tx, existingReservationProps, nowIso, true);
|
|
568
638
|
}
|
|
569
639
|
|
|
570
640
|
return reserveReplacementForLockedShare(tx, {
|
|
571
641
|
share,
|
|
572
|
-
input
|
|
642
|
+
input: {
|
|
643
|
+
...input,
|
|
644
|
+
// The ceiling belongs to the logical request, not its lease.
|
|
645
|
+
// Missing legacy state fails closed after reservation cleanup.
|
|
646
|
+
policy: mergeShareLinkPolicyConservatively(
|
|
647
|
+
retryPolicy ?? DEFAULT_SHARE_LINK_POLICY,
|
|
648
|
+
input.policy ?? retryPolicy ?? DEFAULT_SHARE_LINK_POLICY
|
|
649
|
+
),
|
|
650
|
+
},
|
|
573
651
|
opKey,
|
|
574
652
|
leaseExpiresAt,
|
|
575
653
|
nowIso,
|
|
@@ -659,6 +737,7 @@ const reserveReplacementForLockedShare = async (
|
|
|
659
737
|
contentVersion: input.content ? input.content.contentVersion : null,
|
|
660
738
|
baseContentVersion: share.contentVersion,
|
|
661
739
|
content: input.content,
|
|
740
|
+
attachment: input.content?.attachment,
|
|
662
741
|
selectedCount: input.content ? input.content.selectedCount : share.selectedCount,
|
|
663
742
|
title: input.title ?? share.title,
|
|
664
743
|
note: input.note !== undefined ? input.note : share.note,
|
|
@@ -2,6 +2,7 @@ import { SHARE_LINK_OPERATION_RETENTION_MS } from '@helpers/share-link-lifecycle
|
|
|
2
2
|
|
|
3
3
|
import { ensureShareLinkConstraints } from '../../models/share-link-constraints';
|
|
4
4
|
import { failShareLink } from './errors';
|
|
5
|
+
import { queueShareAttachmentCleanup } from './attachment';
|
|
5
6
|
import {
|
|
6
7
|
deleteReservation,
|
|
7
8
|
enqueueCleanupJob,
|
|
@@ -105,6 +106,25 @@ export const revokeShareLink = async (
|
|
|
105
106
|
|
|
106
107
|
if (reservationProps) {
|
|
107
108
|
const reservation = toShareLinkReservationRecord(reservationProps);
|
|
109
|
+
if (
|
|
110
|
+
reservation.attachmentId &&
|
|
111
|
+
reservation.attachmentChunkCount &&
|
|
112
|
+
reservation.contentVersion
|
|
113
|
+
) {
|
|
114
|
+
await queueShareAttachmentCleanup(
|
|
115
|
+
tx,
|
|
116
|
+
{
|
|
117
|
+
namespace: reservation.namespace,
|
|
118
|
+
ownerProfileId: reservation.ownerProfileId,
|
|
119
|
+
shareId: reservation.shareId,
|
|
120
|
+
contentVersion: reservation.contentVersion,
|
|
121
|
+
attachmentId: reservation.attachmentId,
|
|
122
|
+
chunkCount: reservation.attachmentChunkCount,
|
|
123
|
+
},
|
|
124
|
+
'abandoned',
|
|
125
|
+
nowIso
|
|
126
|
+
);
|
|
127
|
+
}
|
|
108
128
|
|
|
109
129
|
if (reservation.objectRef) {
|
|
110
130
|
await enqueueCleanupJob(tx, {
|
|
@@ -131,6 +151,21 @@ export const revokeShareLink = async (
|
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
if (share.activeObjectRef) {
|
|
154
|
+
if (share.attachmentId && share.attachmentChunkCount) {
|
|
155
|
+
await queueShareAttachmentCleanup(
|
|
156
|
+
tx,
|
|
157
|
+
{
|
|
158
|
+
namespace: share.namespace,
|
|
159
|
+
ownerProfileId: share.ownerProfileId,
|
|
160
|
+
shareId: share.id,
|
|
161
|
+
contentVersion: share.contentVersion,
|
|
162
|
+
attachmentId: share.attachmentId,
|
|
163
|
+
chunkCount: share.attachmentChunkCount,
|
|
164
|
+
},
|
|
165
|
+
'stopped',
|
|
166
|
+
nowIso
|
|
167
|
+
);
|
|
168
|
+
}
|
|
134
169
|
await enqueueCleanupJob(tx, {
|
|
135
170
|
objectRef: share.activeObjectRef,
|
|
136
171
|
operationId: share.activeObjectOperationId!,
|
|
@@ -3,10 +3,10 @@ import type {
|
|
|
3
3
|
ShareContentCleanupStatus,
|
|
4
4
|
ShareLinkOperationKind,
|
|
5
5
|
} from '@helpers/share-link-lifecycle';
|
|
6
|
-
import type { ShareLinkPolicySnapshot } from '@helpers/share-link-policy';
|
|
6
|
+
import type { CurrentShareLinkPolicy, ShareLinkPolicySnapshot } from '@helpers/share-link-policy';
|
|
7
7
|
|
|
8
8
|
import type { ShareContentState, ShareLinkRecord, ShareLinkStatus } from '../../models/ShareLink';
|
|
9
|
-
import type { ShareLinkTransactionOptions } from './transaction';
|
|
9
|
+
import type { ShareLinkTransaction, ShareLinkTransactionOptions } from './transaction';
|
|
10
10
|
|
|
11
11
|
/** Namespace + owner + share id binding used by every lifecycle operation. */
|
|
12
12
|
export type ShareLinkKey = {
|
|
@@ -55,6 +55,8 @@ export type ShareLinkOperationRecord = {
|
|
|
55
55
|
* a bounded lease so a restart can either resume or supersede it.
|
|
56
56
|
*/
|
|
57
57
|
export type ShareLinkReservationRecord = {
|
|
58
|
+
attachmentId?: string | null;
|
|
59
|
+
attachmentChunkCount?: number | null;
|
|
58
60
|
shareId: string;
|
|
59
61
|
namespace: string;
|
|
60
62
|
ownerProfileId: string;
|
|
@@ -122,6 +124,7 @@ export type ShareContentBinding = {
|
|
|
122
124
|
};
|
|
123
125
|
|
|
124
126
|
export type ReserveCreateInput = {
|
|
127
|
+
attachment?: import('@learncard/types').ShareLinkAttachment;
|
|
125
128
|
namespace: string;
|
|
126
129
|
ownerProfileId: string;
|
|
127
130
|
clientRequestId: string;
|
|
@@ -143,6 +146,7 @@ export type ReserveCreateInput = {
|
|
|
143
146
|
};
|
|
144
147
|
|
|
145
148
|
export type ReserveReplacementContent = ShareContentBinding & {
|
|
149
|
+
attachment?: import('@learncard/types').ShareLinkAttachment;
|
|
146
150
|
/** Must equal the current visible content version + 1. */
|
|
147
151
|
contentVersion: number;
|
|
148
152
|
selectedCount: number;
|
|
@@ -194,6 +198,7 @@ export type ReserveShareLinkResult =
|
|
|
194
198
|
};
|
|
195
199
|
|
|
196
200
|
export type FinalizeReservationInput = {
|
|
201
|
+
verifiedAttachmentId?: string;
|
|
197
202
|
namespace: string;
|
|
198
203
|
ownerProfileId: string;
|
|
199
204
|
shareId: string;
|
|
@@ -210,10 +215,10 @@ export type FinalizeReservationInput = {
|
|
|
210
215
|
verifiedContentHash?: string;
|
|
211
216
|
/** Production-only graph-local policy recheck under the share write lock. */
|
|
212
217
|
resolveCurrentPolicy?: (
|
|
213
|
-
tx:
|
|
218
|
+
tx: ShareLinkTransaction,
|
|
214
219
|
ownerProfileId: string,
|
|
215
220
|
now: Date
|
|
216
|
-
) => Promise<
|
|
221
|
+
) => Promise<CurrentShareLinkPolicy>;
|
|
217
222
|
now?: Date;
|
|
218
223
|
/** Maintenance-only: bounded transaction timeout for this unit. */
|
|
219
224
|
transactionTimeoutMs?: number;
|
|
@@ -3,6 +3,7 @@ import { readAppStoreListingById } from '@accesslayer/app-store-listing/read';
|
|
|
3
3
|
import {
|
|
4
4
|
getIntegrationForListing,
|
|
5
5
|
getBoostForListingByTemplateAlias,
|
|
6
|
+
getConsentContractForListingByScopeHash,
|
|
6
7
|
} from '@accesslayer/app-store-listing/relationships/read';
|
|
7
8
|
import { getOwnerProfileForIntegration } from '@accesslayer/integration/relationships/read';
|
|
8
9
|
import {
|
|
@@ -28,6 +29,7 @@ const LISTING_CACHE_TTL_MS = 30_000;
|
|
|
28
29
|
const INTEGRATION_CACHE_TTL_MS = 30_000;
|
|
29
30
|
const OWNER_CACHE_TTL_MS = 30_000;
|
|
30
31
|
const SA_CACHE_TTL_MS = 30_000;
|
|
32
|
+
const CONTRACT_CACHE_TTL_MS = 30_000;
|
|
31
33
|
|
|
32
34
|
const boostByAliasCache =
|
|
33
35
|
getLRUCache<Awaited<ReturnType<typeof getBoostForListingByTemplateAlias>>>(200);
|
|
@@ -40,6 +42,8 @@ const listingSaCache =
|
|
|
40
42
|
getLRUCache<Awaited<ReturnType<typeof getPrimarySigningAuthorityForListing>>>(200);
|
|
41
43
|
const integrationSaCache =
|
|
42
44
|
getLRUCache<Awaited<ReturnType<typeof getPrimarySigningAuthorityForIntegration>>>(200);
|
|
45
|
+
const consentContractByScopeCache =
|
|
46
|
+
getLRUCache<Awaited<ReturnType<typeof getConsentContractForListingByScopeHash>>>(200);
|
|
43
47
|
|
|
44
48
|
export const getBoostForListingCached = async (
|
|
45
49
|
listingId: string,
|
|
@@ -54,6 +58,33 @@ export const getBoostForListingCached = async (
|
|
|
54
58
|
return value;
|
|
55
59
|
};
|
|
56
60
|
|
|
61
|
+
export const invalidateBoostForListingCache = (
|
|
62
|
+
listingId: string,
|
|
63
|
+
templateAlias: string,
|
|
64
|
+
domain: string
|
|
65
|
+
) => {
|
|
66
|
+
boostByAliasCache.invalidate(`${listingId}|${templateAlias}|${domain}`);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const getConsentContractForListingByScopeHashCached = async (
|
|
70
|
+
listingId: string,
|
|
71
|
+
scopeHash: string
|
|
72
|
+
) => {
|
|
73
|
+
const key = `${listingId}|${scopeHash}`;
|
|
74
|
+
const hit = consentContractByScopeCache.get(key);
|
|
75
|
+
if (hit !== undefined) return hit;
|
|
76
|
+
const value = await getConsentContractForListingByScopeHash(listingId, scopeHash);
|
|
77
|
+
consentContractByScopeCache.add(key, value, CONTRACT_CACHE_TTL_MS);
|
|
78
|
+
return value;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const invalidateConsentContractForListingByScopeHashCache = (
|
|
82
|
+
listingId: string,
|
|
83
|
+
scopeHash: string
|
|
84
|
+
) => {
|
|
85
|
+
consentContractByScopeCache.invalidate(`${listingId}|${scopeHash}`);
|
|
86
|
+
};
|
|
87
|
+
|
|
57
88
|
export const readAppStoreListingByIdCached = async (listingId: string) => {
|
|
58
89
|
const hit = listingByIdCache.get(listingId);
|
|
59
90
|
if (hit !== undefined) return hit;
|
|
@@ -87,6 +118,10 @@ export const getPrimarySigningAuthorityForListingCached = async (listing: AppSto
|
|
|
87
118
|
return value;
|
|
88
119
|
};
|
|
89
120
|
|
|
121
|
+
export const invalidateListingSigningAuthorityCache = (listingId: string) => {
|
|
122
|
+
listingSaCache.invalidate(listingId);
|
|
123
|
+
};
|
|
124
|
+
|
|
90
125
|
export const getPrimarySigningAuthorityForIntegrationCached = async (integrationId: string) => {
|
|
91
126
|
const hit = integrationSaCache.get(integrationId);
|
|
92
127
|
if (hit !== undefined) return hit;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { applyBrainServiceStageConfig } from './stageConfig';
|
|
2
|
+
|
|
3
|
+
// Side-effect import: runs before @environment is evaluated so Docker/local processes
|
|
4
|
+
// pick up checked-in per-stage config. Keyed on CONFIG_STAGE (Lambda uses LAMBDA_STAGE);
|
|
5
|
+
// explicit non-empty env values from the developer's .env still win.
|
|
6
|
+
applyBrainServiceStageConfig(process.env.CONFIG_STAGE);
|
|
@@ -83,18 +83,23 @@ describe('credential refresh configuration', () => {
|
|
|
83
83
|
}
|
|
84
84
|
);
|
|
85
85
|
|
|
86
|
-
it
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
it('resolves the deployed function environment through the shared provider file', () => {
|
|
87
|
+
const manifestPath = fileURLToPath(new URL('../../serverless.yml', import.meta.url));
|
|
88
|
+
const manifest = parse(readFileSync(manifestPath, 'utf8'));
|
|
89
|
+
|
|
90
|
+
expect(manifest.provider.environment).toBe(
|
|
91
|
+
'${file(./serverless.function-env.cjs):provider}'
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it.each(['config.dev.json', 'config.production.json'])(
|
|
96
|
+
'delivers non-secret refresh settings through checked-in stage config in %s',
|
|
97
|
+
stageFile => {
|
|
98
|
+
const configPath = fileURLToPath(new URL(`../../config/${stageFile}`, import.meta.url));
|
|
99
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
100
|
+
|
|
101
|
+
expect(config.CREDENTIAL_REFRESH_ENABLED).toBeDefined();
|
|
102
|
+
expect(config.CREDENTIAL_REFRESH_NOTIFICATION_WINDOW_HOURS).toBeDefined();
|
|
98
103
|
}
|
|
99
104
|
);
|
|
100
105
|
});
|
|
@@ -27,6 +27,7 @@ export const brainServiceEnvironmentShape = {
|
|
|
27
27
|
CLIENT_APP_DOMAIN_NAME: optionalEnvironmentString,
|
|
28
28
|
CLIENT_APP_PORT: optionalEnvironmentPort,
|
|
29
29
|
LOGIN_PROVIDER_DID: optionalEnvironmentString,
|
|
30
|
+
SIGNING_AUTHORITY_ENDPOINT: optionalEnvironmentUrl,
|
|
30
31
|
APP_STORE_ADMIN_PROFILE_IDS: optionalEnvironmentString,
|
|
31
32
|
SKILL_FRAMEWORK_ADMIN_PROFILE_IDS: optionalEnvironmentString,
|
|
32
33
|
SKILL_FRAMEWORK_SEED_OWNER_PROFILE_ID: optionalEnvironmentString,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const functions: {
|
|
6
|
+
provider: (input?: {
|
|
7
|
+
options?: Record<string, unknown>;
|
|
8
|
+
env?: NodeJS.ProcessEnv;
|
|
9
|
+
}) => Record<string, unknown>;
|
|
10
|
+
} = require('../../serverless.function-env.cjs');
|
|
11
|
+
|
|
12
|
+
const INFRA_KEYS = {
|
|
13
|
+
LAMBDA_STAGE: 'dev',
|
|
14
|
+
PORT: '3000',
|
|
15
|
+
REDIS_HOST: { 'Fn::GetAtt': ['ElasticCacheCluster', 'RedisEndpoint.Address'] },
|
|
16
|
+
REDIS_PORT: { 'Fn::GetAtt': ['ElasticCacheCluster', 'RedisEndpoint.Port'] },
|
|
17
|
+
NOTIFICATIONS_QUEUE_URL: '${construct:notifications-queue.queueUrl}',
|
|
18
|
+
INBOX_QUEUE_URL: '${construct:inbox-queue.queueUrl}',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.stubEnv('RUNTIME_SECRETS_ID', '');
|
|
23
|
+
vi.stubEnv('GIT_SHA', '');
|
|
24
|
+
vi.stubEnv('SEED', 'seed');
|
|
25
|
+
vi.stubEnv('NEO4J_PASSWORD', 'neo4j-password');
|
|
26
|
+
vi.stubEnv('SHARE_LINK_REQUEST_HASH_SECRET', 'hash-secret');
|
|
27
|
+
vi.stubEnv('SENTRY_AUTH_TOKEN', 'build-token');
|
|
28
|
+
vi.stubEnv('TRUSTED_ISSUERS_WHITELIST', 'did:web:issuer');
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => vi.unstubAllEnvs());
|
|
31
|
+
|
|
32
|
+
describe('brain-service serverless provider environment', () => {
|
|
33
|
+
it('always emits Redis and queue infrastructure as unresolved references', () => {
|
|
34
|
+
expect(functions.provider({ options: { stage: 'dev' } })).toMatchObject(INFRA_KEYS);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('derives LAMBDA_STAGE and PORT from the CLI options, never a stage passthrough', () => {
|
|
38
|
+
const result = functions.provider({ options: { stage: 'production', httpPort: 4000 } });
|
|
39
|
+
expect(result.LAMBDA_STAGE).toBe('production');
|
|
40
|
+
expect(result.PORT).toBe('4000');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('defaults LAMBDA_STAGE and PORT when no CLI options are supplied', () => {
|
|
44
|
+
const result = functions.provider();
|
|
45
|
+
expect(result.LAMBDA_STAGE).toBe('dev');
|
|
46
|
+
expect(result.PORT).toBe('3000');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('emits GIT_SHA only when it is set to a non-empty value', () => {
|
|
50
|
+
expect(functions.provider()).not.toHaveProperty('GIT_SHA');
|
|
51
|
+
vi.stubEnv('GIT_SHA', 'deadbeef');
|
|
52
|
+
expect(functions.provider()).toMatchObject({ GIT_SHA: 'deadbeef' });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('omits every fallback credential and the pointer stays set in bundle mode', () => {
|
|
56
|
+
vi.stubEnv('RUNTIME_SECRETS_ID', 'brain-service/dev/runtime-secrets');
|
|
57
|
+
const result = functions.provider({ options: { stage: 'dev' } });
|
|
58
|
+
expect(result).toMatchObject({
|
|
59
|
+
...INFRA_KEYS,
|
|
60
|
+
RUNTIME_SECRETS_ID: 'brain-service/dev/runtime-secrets',
|
|
61
|
+
});
|
|
62
|
+
expect(result).not.toHaveProperty('SEED');
|
|
63
|
+
expect(result).not.toHaveProperty('NEO4J_PASSWORD');
|
|
64
|
+
expect(result).not.toHaveProperty('SHARE_LINK_REQUEST_HASH_SECRET');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('retains only the approved fallback credentials when no bundle is configured', () => {
|
|
68
|
+
const result = functions.provider();
|
|
69
|
+
expect(result.SEED).toBe('seed');
|
|
70
|
+
expect(result.NEO4J_PASSWORD).toBe('neo4j-password');
|
|
71
|
+
expect(result.SHARE_LINK_REQUEST_HASH_SECRET).toBe('hash-secret');
|
|
72
|
+
expect(result.RUNTIME_SECRETS_ID).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('never forwards Sentry build credentials or non-secret allowlists as fallbacks', () => {
|
|
76
|
+
const result = functions.provider();
|
|
77
|
+
expect(result).not.toHaveProperty('SENTRY_AUTH_TOKEN');
|
|
78
|
+
expect(result).not.toHaveProperty('SENTRY_ORG');
|
|
79
|
+
expect(result).not.toHaveProperty('SENTRY_PROJECT');
|
|
80
|
+
expect(result).not.toHaveProperty('TRUSTED_ISSUERS_WHITELIST');
|
|
81
|
+
expect(result).not.toHaveProperty('TRUSTED_BRAIN_SERVICES');
|
|
82
|
+
expect(result).not.toHaveProperty('BRAIN_SERVICE_REGISTRY_URL');
|
|
83
|
+
expect(result).not.toHaveProperty('DCC_KNOWN_REGISTRIES_URL');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('resolves the provider through Serverless v3, preserving nested references in both modes', async () => {
|
|
87
|
+
const source = require('serverless/lib/configuration/variables/sources/file');
|
|
88
|
+
for (const bundle of ['', 'bundle']) {
|
|
89
|
+
vi.stubEnv('RUNTIME_SECRETS_ID', bundle);
|
|
90
|
+
const result = await source.resolve({
|
|
91
|
+
serviceDir: new URL('../../', import.meta.url).pathname,
|
|
92
|
+
params: [new URL('../../serverless.function-env.cjs', import.meta.url).pathname],
|
|
93
|
+
address: 'provider',
|
|
94
|
+
options: { stage: 'dev' },
|
|
95
|
+
resolveConfigurationProperty: vi.fn(),
|
|
96
|
+
resolveVariable: vi.fn(),
|
|
97
|
+
});
|
|
98
|
+
expect(result.value).toEqual(functions.provider({ options: { stage: 'dev' } }));
|
|
99
|
+
expect(result.value.NOTIFICATIONS_QUEUE_URL).toBe(
|
|
100
|
+
'${construct:notifications-queue.queueUrl}'
|
|
101
|
+
);
|
|
102
|
+
expect(result.value.INBOX_QUEUE_URL).toBe('${construct:inbox-queue.queueUrl}');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { afterEach, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('dotenv/config', () => {
|
|
4
|
+
vi.stubEnv('SENTRY_ENV', 'self-hosted');
|
|
5
|
+
return {};
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
afterEach(() => vi.unstubAllEnvs());
|
|
9
|
+
|
|
10
|
+
it('loads local env before applying statically imported stage defaults', async () => {
|
|
11
|
+
const { applyBrainServiceStageConfig, stages } = await import('./stageConfig');
|
|
12
|
+
for (const key of Object.keys(stages.dev)) {
|
|
13
|
+
if (key !== 'SENTRY_ENV') vi.stubEnv(key, '');
|
|
14
|
+
}
|
|
15
|
+
applyBrainServiceStageConfig('dev');
|
|
16
|
+
expect(process.env.SENTRY_ENV).toBe('self-hosted');
|
|
17
|
+
expect(process.env.DOMAIN_NAME).toBe(stages.dev.DOMAIN_NAME);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('does not select a deployed stage for an unknown local stage', async () => {
|
|
21
|
+
const { applyBrainServiceStageConfig } = await import('./stageConfig');
|
|
22
|
+
vi.stubEnv('DOMAIN_NAME', '');
|
|
23
|
+
applyBrainServiceStageConfig('unregistered-self-hosted-stage');
|
|
24
|
+
expect(process.env.DOMAIN_NAME).toBe('');
|
|
25
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { applyStageConfig } from '@learncard/service-config';
|
|
3
|
+
|
|
4
|
+
import baseConfig from '../../config/config.json';
|
|
5
|
+
import devConfig from '../../config/config.dev.json';
|
|
6
|
+
import productionConfig from '../../config/config.production.json';
|
|
7
|
+
import scoutsDevConfig from '../../config/config.scouts.dev.json';
|
|
8
|
+
import scoutsProductionConfig from '../../config/config.scouts.production.json';
|
|
9
|
+
|
|
10
|
+
export const base: Record<string, string> = baseConfig;
|
|
11
|
+
|
|
12
|
+
export const stages = {
|
|
13
|
+
dev: devConfig,
|
|
14
|
+
production: productionConfig,
|
|
15
|
+
// ScoutPass deploys the same services and stage names; CONFIG_TENANT=scouts selects these.
|
|
16
|
+
'scouts.dev': scoutsDevConfig,
|
|
17
|
+
'scouts.production': scoutsProductionConfig,
|
|
18
|
+
} satisfies Record<string, Record<string, string>>;
|
|
19
|
+
|
|
20
|
+
// Must run before any module that reads configuration on import (e.g. @environment):
|
|
21
|
+
// merges base + the selected stage into process.env; explicit non-empty env still wins.
|
|
22
|
+
export const applyBrainServiceStageConfig = (stage: string | undefined): void => {
|
|
23
|
+
applyStageConfig({ base, stages, stage });
|
|
24
|
+
};
|
package/src/docker-entry.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import './config/applyDockerStageConfig';
|
|
2
|
+
|
|
3
|
+
import { dispatchContractEvents } from '@helpers/contract-events.helpers';
|
|
1
4
|
import { configureInboxBatchBodyLimit } from '@helpers/inbox-batch-http.helpers';
|
|
2
5
|
import { environment } from '@environment';
|
|
3
6
|
import Fastify from 'fastify';
|
|
@@ -41,8 +44,27 @@ const server = Fastify({ routerOptions: { maxParamLength: 5000 } });
|
|
|
41
44
|
* graph/remote/signing dependency is initialized at all.
|
|
42
45
|
*/
|
|
43
46
|
let shareLinkMaintenanceScheduler: ShareLinkMaintenanceScheduler | null = null;
|
|
47
|
+
let contractEventTimer: ReturnType<typeof setTimeout> | null = null;
|
|
48
|
+
let contractEventPass: Promise<unknown> | null = null;
|
|
49
|
+
let contractEventsStopped = false;
|
|
50
|
+
|
|
51
|
+
const runContractEventPass = (): void => {
|
|
52
|
+
if (contractEventsStopped) return;
|
|
53
|
+
contractEventPass = dispatchContractEvents()
|
|
54
|
+
.catch(() => {
|
|
55
|
+
console.warn('contract_events: recovery_pending');
|
|
56
|
+
})
|
|
57
|
+
.finally(() => {
|
|
58
|
+
contractEventPass = null;
|
|
59
|
+
if (!contractEventsStopped)
|
|
60
|
+
contractEventTimer = setTimeout(runContractEventPass, 60_000);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
44
63
|
|
|
45
64
|
server.addHook('onClose', async () => {
|
|
65
|
+
contractEventsStopped = true;
|
|
66
|
+
if (contractEventTimer) clearTimeout(contractEventTimer);
|
|
67
|
+
await contractEventPass;
|
|
46
68
|
await shareLinkMaintenanceScheduler?.stop();
|
|
47
69
|
});
|
|
48
70
|
// Register before either OpenAPI or tRPC registers its wildcard route. The hook raises the
|
|
@@ -167,6 +189,8 @@ server.register(credentialRefreshFastifyPlugin);
|
|
|
167
189
|
console.error('Skill embedding backfill failed', error);
|
|
168
190
|
}
|
|
169
191
|
|
|
192
|
+
contractEventTimer = setTimeout(runContractEventPass, 60_000);
|
|
193
|
+
|
|
170
194
|
// Explicit startup after readiness. Disabled/invalid config stays inert.
|
|
171
195
|
const maintenanceRuntime = createShareLinkMaintenanceRuntime();
|
|
172
196
|
|