@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
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { int } from 'neo4j-driver';
|
|
3
|
+
import type { ShareLinkAttachment } from '@learncard/types';
|
|
4
|
+
import { ensureShareLinkConstraints } from '../../models/share-link-constraints';
|
|
5
|
+
import { failShareLink } from './errors';
|
|
6
|
+
import {
|
|
7
|
+
enqueueCleanupJob,
|
|
8
|
+
lockShare,
|
|
9
|
+
readNodeProperties,
|
|
10
|
+
readReservation,
|
|
11
|
+
toShareLinkRecord,
|
|
12
|
+
toShareLinkReservationRecord,
|
|
13
|
+
normalizeNeo4jValue,
|
|
14
|
+
} from './helpers';
|
|
15
|
+
import {
|
|
16
|
+
withShareLinkTransaction,
|
|
17
|
+
type ShareLinkTransaction,
|
|
18
|
+
type ShareLinkTransactionOptions,
|
|
19
|
+
} from './transaction';
|
|
20
|
+
|
|
21
|
+
export type ShareAttachmentBinding = {
|
|
22
|
+
namespace: string;
|
|
23
|
+
ownerProfileId: string;
|
|
24
|
+
shareId: string;
|
|
25
|
+
contentVersion: number;
|
|
26
|
+
attachmentId: string;
|
|
27
|
+
chunkCount: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const shareAttachmentObjectId = (attachmentId: string, chunkIndex: number): string =>
|
|
31
|
+
`resume-pdf-${attachmentId}-${chunkIndex}`;
|
|
32
|
+
export const shareAttachmentTuple = (binding: ShareAttachmentBinding, chunkIndex: number) => ({
|
|
33
|
+
namespace: binding.namespace,
|
|
34
|
+
ownerProfileId: binding.ownerProfileId,
|
|
35
|
+
shareId: binding.shareId,
|
|
36
|
+
contentVersion: binding.contentVersion,
|
|
37
|
+
objectId: shareAttachmentObjectId(binding.attachmentId, chunkIndex),
|
|
38
|
+
operationId: binding.attachmentId,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const stageKey = (binding: ShareAttachmentBinding): string =>
|
|
42
|
+
createHash('sha256')
|
|
43
|
+
.update(
|
|
44
|
+
JSON.stringify([
|
|
45
|
+
binding.namespace,
|
|
46
|
+
binding.ownerProfileId,
|
|
47
|
+
binding.shareId,
|
|
48
|
+
binding.contentVersion,
|
|
49
|
+
binding.attachmentId,
|
|
50
|
+
])
|
|
51
|
+
)
|
|
52
|
+
.digest('hex');
|
|
53
|
+
|
|
54
|
+
const lockStage = async (tx: ShareLinkTransaction, binding: ShareAttachmentBinding) => {
|
|
55
|
+
const result = await tx.run(
|
|
56
|
+
`MATCH (a:ShareLinkAttachmentStage {key: $key})
|
|
57
|
+
SET a.lockTick = coalesce(a.lockTick, 0) + 1 RETURN a`,
|
|
58
|
+
{ key: stageKey(binding) }
|
|
59
|
+
);
|
|
60
|
+
const props = readNodeProperties(result, 'a');
|
|
61
|
+
if (props && props.chunkCount !== binding.chunkCount) {
|
|
62
|
+
failShareLink('CONFLICT', 'attachment staging conflict');
|
|
63
|
+
}
|
|
64
|
+
return props;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Stage guard is durable; remote I/O happens only after this transaction closes. */
|
|
68
|
+
export const beginShareAttachmentChunk = async (
|
|
69
|
+
binding: ShareAttachmentBinding,
|
|
70
|
+
chunkIndex: number
|
|
71
|
+
): Promise<void> => {
|
|
72
|
+
if (
|
|
73
|
+
!Number.isInteger(binding.chunkCount) ||
|
|
74
|
+
binding.chunkCount < 1 ||
|
|
75
|
+
binding.chunkCount > 16 ||
|
|
76
|
+
!Number.isInteger(chunkIndex) ||
|
|
77
|
+
chunkIndex < 0 ||
|
|
78
|
+
chunkIndex >= binding.chunkCount
|
|
79
|
+
)
|
|
80
|
+
failShareLink('INVALID_INPUT', 'invalid attachment chunk');
|
|
81
|
+
await ensureShareLinkConstraints();
|
|
82
|
+
await withShareLinkTransaction(async tx => {
|
|
83
|
+
const shareProps = await lockShare(tx, binding.shareId);
|
|
84
|
+
if (shareProps) {
|
|
85
|
+
const share = toShareLinkRecord(shareProps);
|
|
86
|
+
if (
|
|
87
|
+
share.namespace !== binding.namespace ||
|
|
88
|
+
share.ownerProfileId !== binding.ownerProfileId
|
|
89
|
+
) {
|
|
90
|
+
failShareLink('NOT_FOUND', 'share not found');
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
share.status === 'stopped' ||
|
|
94
|
+
!(
|
|
95
|
+
(share.status === 'pending' && binding.contentVersion === 1) ||
|
|
96
|
+
binding.contentVersion === share.contentVersion + 1 ||
|
|
97
|
+
(binding.contentVersion === share.contentVersion &&
|
|
98
|
+
share.attachmentId === binding.attachmentId &&
|
|
99
|
+
share.attachmentChunkCount === binding.chunkCount)
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
failShareLink('CONFLICT', 'attachment version conflict');
|
|
103
|
+
const pending = await readReservation(tx, binding.shareId);
|
|
104
|
+
if (pending) {
|
|
105
|
+
const reservation = toShareLinkReservationRecord(pending);
|
|
106
|
+
if (
|
|
107
|
+
reservation.contentVersion !== binding.contentVersion ||
|
|
108
|
+
reservation.attachmentId !== binding.attachmentId ||
|
|
109
|
+
reservation.attachmentChunkCount !== binding.chunkCount
|
|
110
|
+
) {
|
|
111
|
+
failShareLink('CONFLICT', 'another publication is pending');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} else if (binding.contentVersion !== 1) {
|
|
115
|
+
failShareLink('NOT_FOUND', 'share not found');
|
|
116
|
+
}
|
|
117
|
+
// Serialize pre-create stages by owner as well as by share, so simultaneous
|
|
118
|
+
// random IDs cannot exceed the bounded uncommitted storage allowance.
|
|
119
|
+
const quotaKey = createHash('sha256')
|
|
120
|
+
.update(JSON.stringify([binding.namespace, binding.ownerProfileId]))
|
|
121
|
+
.digest('hex');
|
|
122
|
+
await tx.run(
|
|
123
|
+
`MERGE (q:ShareAttachmentOwnerQuota {key: $quotaKey})
|
|
124
|
+
ON CREATE SET q.namespace = $namespace, q.ownerProfileId = $ownerProfileId
|
|
125
|
+
SET q.lockTick = coalesce(q.lockTick, 0) + 1`,
|
|
126
|
+
{
|
|
127
|
+
quotaKey,
|
|
128
|
+
namespace: binding.namespace,
|
|
129
|
+
ownerProfileId: binding.ownerProfileId,
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
const existing = await lockStage(tx, binding);
|
|
133
|
+
if (!existing) {
|
|
134
|
+
const result = await tx.run(
|
|
135
|
+
`MATCH (a:ShareLinkAttachmentStage)
|
|
136
|
+
WHERE a.namespace = $namespace AND a.ownerProfileId = $ownerProfileId
|
|
137
|
+
AND a.status = 'staging' AND a.expiresAt > $now
|
|
138
|
+
RETURN count(a) AS count`,
|
|
139
|
+
{
|
|
140
|
+
namespace: binding.namespace,
|
|
141
|
+
ownerProfileId: binding.ownerProfileId,
|
|
142
|
+
now: new Date().toISOString(),
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
const count = normalizeNeo4jValue(result.records[0]?.get('count'));
|
|
146
|
+
if (typeof count !== 'number' || count >= 8)
|
|
147
|
+
failShareLink('PRECONDITION_FAILED', 'attachment staging limit reached');
|
|
148
|
+
}
|
|
149
|
+
await tx.run(
|
|
150
|
+
`MERGE (a:ShareLinkAttachmentStage {key: $key})
|
|
151
|
+
ON CREATE SET a += $props
|
|
152
|
+
SET a.lockTick = coalesce(a.lockTick, 0) + 1 RETURN a`,
|
|
153
|
+
{
|
|
154
|
+
key: stageKey(binding),
|
|
155
|
+
props: {
|
|
156
|
+
...binding,
|
|
157
|
+
status: 'staging',
|
|
158
|
+
completedChunks: [],
|
|
159
|
+
createdAt: new Date().toISOString(),
|
|
160
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
const stage = await lockStage(tx, binding);
|
|
165
|
+
if (!stage || stage.status === 'deleted')
|
|
166
|
+
failShareLink('CONFLICT', 'attachment staging conflict');
|
|
167
|
+
if (
|
|
168
|
+
stage.status === 'staging' &&
|
|
169
|
+
typeof stage.expiresAt === 'string' &&
|
|
170
|
+
stage.expiresAt <= new Date().toISOString()
|
|
171
|
+
) {
|
|
172
|
+
failShareLink('CONFLICT', 'attachment staging expired');
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Record successful immutable puts even if staging expired during remote I/O.
|
|
179
|
+
* This is bookkeeping, not publication authority: pin/finalize enforce expiry,
|
|
180
|
+
* and maintenance cleans up an expired, uncommitted stage.
|
|
181
|
+
*/
|
|
182
|
+
export const completeShareAttachmentChunk = async (
|
|
183
|
+
binding: ShareAttachmentBinding,
|
|
184
|
+
chunkIndex: number
|
|
185
|
+
): Promise<void> => {
|
|
186
|
+
await withShareLinkTransaction(async tx => {
|
|
187
|
+
const stage = await lockStage(tx, binding);
|
|
188
|
+
if (!stage || stage.status === 'deleted')
|
|
189
|
+
failShareLink('CONFLICT', 'attachment staging conflict');
|
|
190
|
+
await tx.run(
|
|
191
|
+
`MATCH (a:ShareLinkAttachmentStage {key: $key})
|
|
192
|
+
SET a.completedChunks = CASE WHEN $chunkIndex IN a.completedChunks THEN a.completedChunks
|
|
193
|
+
ELSE a.completedChunks + $chunkIndex END`,
|
|
194
|
+
{ key: stageKey(binding), chunkIndex }
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/** Called while the share lock is held; pins complete staged chunks to request intent. */
|
|
200
|
+
export const pinShareAttachment = async (
|
|
201
|
+
tx: ShareLinkTransaction,
|
|
202
|
+
binding: ShareAttachmentBinding,
|
|
203
|
+
requestHash: string
|
|
204
|
+
): Promise<void> => {
|
|
205
|
+
const stage = await lockStage(tx, binding);
|
|
206
|
+
if (
|
|
207
|
+
!stage ||
|
|
208
|
+
stage.status === 'deleted' ||
|
|
209
|
+
(stage.requestHash !== undefined &&
|
|
210
|
+
stage.requestHash !== null &&
|
|
211
|
+
stage.requestHash !== requestHash)
|
|
212
|
+
) {
|
|
213
|
+
failShareLink('PRECONDITION_FAILED', 'attachment is not ready');
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
stage.status === 'staging' &&
|
|
217
|
+
typeof stage.expiresAt === 'string' &&
|
|
218
|
+
stage.expiresAt <= new Date().toISOString()
|
|
219
|
+
) {
|
|
220
|
+
failShareLink('PRECONDITION_FAILED', 'attachment staging expired');
|
|
221
|
+
}
|
|
222
|
+
const completed = stage.completedChunks;
|
|
223
|
+
if (
|
|
224
|
+
!Array.isArray(completed) ||
|
|
225
|
+
!Array.from({ length: binding.chunkCount }, (_, index) => index).every(index =>
|
|
226
|
+
completed.includes(index)
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
failShareLink('PRECONDITION_FAILED', 'attachment is incomplete');
|
|
230
|
+
await tx.run(
|
|
231
|
+
`MATCH (a:ShareLinkAttachmentStage {key: $key}) SET a.status = 'pinned',
|
|
232
|
+
a.requestHash = $requestHash`,
|
|
233
|
+
{ key: stageKey(binding), requestHash }
|
|
234
|
+
);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const verifyPinnedShareAttachment = async (
|
|
238
|
+
tx: ShareLinkTransaction,
|
|
239
|
+
binding: ShareAttachmentBinding,
|
|
240
|
+
requestHash: string
|
|
241
|
+
): Promise<void> => {
|
|
242
|
+
const stage = await lockStage(tx, binding);
|
|
243
|
+
if (!stage || stage.status !== 'pinned' || stage.requestHash !== requestHash) {
|
|
244
|
+
failShareLink('PRECONDITION_FAILED', 'attachment is not ready');
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/** Queues only exact tuples, after withdrawing graph authority to commit those bytes. */
|
|
249
|
+
export const queueShareAttachmentCleanup = async (
|
|
250
|
+
tx: ShareLinkTransaction,
|
|
251
|
+
binding: ShareAttachmentBinding,
|
|
252
|
+
reason: 'superseded' | 'abandoned' | 'stopped',
|
|
253
|
+
now: string
|
|
254
|
+
): Promise<void> => {
|
|
255
|
+
const stage = await lockStage(tx, binding);
|
|
256
|
+
if (stage)
|
|
257
|
+
await tx.run(`MATCH (a:ShareLinkAttachmentStage {key: $key}) SET a.status = 'deleted'`, {
|
|
258
|
+
key: stageKey(binding),
|
|
259
|
+
});
|
|
260
|
+
for (let chunkIndex = 0; chunkIndex < binding.chunkCount; chunkIndex++) {
|
|
261
|
+
const tuple = shareAttachmentTuple(binding, chunkIndex);
|
|
262
|
+
await enqueueCleanupJob(tx, { ...tuple, objectRef: tuple.objectId, reason, now });
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/** Refuses active or in-flight assets; a tombstoned stage can never be repinned. */
|
|
267
|
+
export const deleteUnreferencedShareAttachment = async (
|
|
268
|
+
binding: ShareAttachmentBinding
|
|
269
|
+
): Promise<boolean> => {
|
|
270
|
+
await ensureShareLinkConstraints();
|
|
271
|
+
return withShareLinkTransaction(async tx => {
|
|
272
|
+
const shareProps = await lockShare(tx, binding.shareId);
|
|
273
|
+
if (shareProps) {
|
|
274
|
+
const share = toShareLinkRecord(shareProps);
|
|
275
|
+
if (
|
|
276
|
+
share.namespace !== binding.namespace ||
|
|
277
|
+
share.ownerProfileId !== binding.ownerProfileId
|
|
278
|
+
) {
|
|
279
|
+
failShareLink('NOT_FOUND', 'share not found');
|
|
280
|
+
}
|
|
281
|
+
if (
|
|
282
|
+
share.attachmentId === binding.attachmentId &&
|
|
283
|
+
share.contentVersion === binding.contentVersion
|
|
284
|
+
)
|
|
285
|
+
return false;
|
|
286
|
+
const props = await readReservation(tx, binding.shareId);
|
|
287
|
+
const reservation = props ? toShareLinkReservationRecord(props) : null;
|
|
288
|
+
if (
|
|
289
|
+
reservation?.attachmentId === binding.attachmentId &&
|
|
290
|
+
reservation.contentVersion === binding.contentVersion
|
|
291
|
+
)
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
const stage = await lockStage(tx, binding);
|
|
295
|
+
if (!stage) return false;
|
|
296
|
+
if (stage.status === 'pinned') return false;
|
|
297
|
+
await queueShareAttachmentCleanup(tx, binding, 'abandoned', new Date().toISOString());
|
|
298
|
+
return true;
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export const attachmentBindingFor = (
|
|
303
|
+
scope: { namespace: string; ownerProfileId: string; shareId: string },
|
|
304
|
+
contentVersion: number,
|
|
305
|
+
attachment: ShareLinkAttachment
|
|
306
|
+
): ShareAttachmentBinding => ({
|
|
307
|
+
...scope,
|
|
308
|
+
contentVersion,
|
|
309
|
+
attachmentId: attachment.id,
|
|
310
|
+
chunkCount: attachment.chunkCount,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
/** Bounded periodic collection of never-committed stages after their 24-hour retry window. */
|
|
314
|
+
export const expireShareAttachmentStages = async (input: {
|
|
315
|
+
namespace: string;
|
|
316
|
+
limit?: number;
|
|
317
|
+
now?: Date;
|
|
318
|
+
transaction?: ShareLinkTransactionOptions;
|
|
319
|
+
}): Promise<number> => {
|
|
320
|
+
if (!input.namespace) failShareLink('INVALID_INPUT', 'namespace is required');
|
|
321
|
+
const limit = Math.max(1, Math.min(10, input.limit ?? 10));
|
|
322
|
+
return withShareLinkTransaction(async tx => {
|
|
323
|
+
const candidates = await tx.run(
|
|
324
|
+
`MATCH (a:ShareLinkAttachmentStage)
|
|
325
|
+
WHERE a.namespace = $namespace AND a.status = 'staging' AND a.expiresAt <= $now
|
|
326
|
+
RETURN a ORDER BY a.expiresAt LIMIT $limit`,
|
|
327
|
+
{
|
|
328
|
+
namespace: input.namespace,
|
|
329
|
+
now: (input.now ?? new Date()).toISOString(),
|
|
330
|
+
limit: int(limit),
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
let queued = 0;
|
|
334
|
+
for (let index = 0; index < candidates.records.length; index++) {
|
|
335
|
+
const props = readNodeProperties(candidates, 'a', index);
|
|
336
|
+
if (!props) continue;
|
|
337
|
+
const binding: ShareAttachmentBinding = {
|
|
338
|
+
namespace: String(props.namespace),
|
|
339
|
+
ownerProfileId: String(props.ownerProfileId),
|
|
340
|
+
shareId: String(props.shareId),
|
|
341
|
+
attachmentId: String(props.attachmentId),
|
|
342
|
+
contentVersion: Number(props.contentVersion),
|
|
343
|
+
chunkCount: Number(props.chunkCount),
|
|
344
|
+
};
|
|
345
|
+
await lockShare(tx, binding.shareId);
|
|
346
|
+
const stage = await lockStage(tx, binding);
|
|
347
|
+
const now = (input.now ?? new Date()).toISOString();
|
|
348
|
+
if (
|
|
349
|
+
!stage ||
|
|
350
|
+
stage.status !== 'staging' ||
|
|
351
|
+
typeof stage.expiresAt !== 'string' ||
|
|
352
|
+
stage.expiresAt > now
|
|
353
|
+
)
|
|
354
|
+
continue;
|
|
355
|
+
await queueShareAttachmentCleanup(tx, binding, 'abandoned', now);
|
|
356
|
+
queued++;
|
|
357
|
+
}
|
|
358
|
+
return queued;
|
|
359
|
+
}, input.transaction);
|
|
360
|
+
};
|
|
@@ -5,6 +5,7 @@ import type { ShareLinkPolicySnapshot } from '@helpers/share-link-policy/types';
|
|
|
5
5
|
|
|
6
6
|
import { ensureShareLinkConstraints } from '../../models/share-link-constraints';
|
|
7
7
|
import { failShareLink } from './errors';
|
|
8
|
+
import { verifyPinnedShareAttachment, queueShareAttachmentCleanup } from './attachment';
|
|
8
9
|
import {
|
|
9
10
|
deleteReservation,
|
|
10
11
|
enqueueCleanupJob,
|
|
@@ -157,12 +158,33 @@ export const finalizeReservation = async (
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
const previousObjectRef = share.activeObjectRef;
|
|
161
|
+
if (
|
|
162
|
+
reservation.contentVersion !== null &&
|
|
163
|
+
reservation.attachmentId &&
|
|
164
|
+
reservation.attachmentChunkCount
|
|
165
|
+
) {
|
|
166
|
+
if (input.verifiedAttachmentId !== reservation.attachmentId) {
|
|
167
|
+
failShareLink('PRECONDITION_FAILED', 'attachment was not verified');
|
|
168
|
+
}
|
|
169
|
+
await verifyPinnedShareAttachment(
|
|
170
|
+
tx,
|
|
171
|
+
{
|
|
172
|
+
namespace: reservation.namespace,
|
|
173
|
+
ownerProfileId: reservation.ownerProfileId,
|
|
174
|
+
shareId: reservation.shareId,
|
|
175
|
+
contentVersion: reservation.contentVersion,
|
|
176
|
+
attachmentId: reservation.attachmentId,
|
|
177
|
+
chunkCount: reservation.attachmentChunkCount,
|
|
178
|
+
},
|
|
179
|
+
reservation.requestHash
|
|
180
|
+
);
|
|
181
|
+
}
|
|
160
182
|
const nextObjectRef = reservation.objectRef ?? share.activeObjectRef;
|
|
161
183
|
const nextContentVersion = reservation.contentVersion ?? share.contentVersion;
|
|
162
184
|
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
185
|
+
// Unknown snapshots and old service restrictions may be refreshed on
|
|
186
|
+
// explicit edits, but only after a fresh graph-local check. The
|
|
187
|
+
// reservation still caps eligibility for stale replays and recovery.
|
|
166
188
|
const currentPolicy: ShareLinkPolicySnapshot = {
|
|
167
189
|
isMinor: share.minorPolicyIsMinor,
|
|
168
190
|
policyResolved: share.minorPolicyResolved,
|
|
@@ -172,18 +194,23 @@ export const finalizeReservation = async (
|
|
|
172
194
|
const checkedPolicy = input.resolveCurrentPolicy
|
|
173
195
|
? await input
|
|
174
196
|
.resolveCurrentPolicy(tx, share.ownerProfileId, now)
|
|
175
|
-
.catch(() => DEFAULT_SHARE_LINK_POLICY)
|
|
197
|
+
.catch(() => ({ ...DEFAULT_SHARE_LINK_POLICY, isServiceProfile: false }))
|
|
176
198
|
: null;
|
|
177
199
|
const reservationPolicy = checkedPolicy
|
|
178
200
|
? mergeShareLinkPolicyConservatively(reservation.policy, checkedPolicy)
|
|
179
201
|
: mergeShareLinkPolicyConservatively(reservation.policy, currentPolicy);
|
|
202
|
+
const mayRefreshExplicitService =
|
|
203
|
+
checkedPolicy?.isServiceProfile === true &&
|
|
204
|
+
checkedPolicy.isMinor === false &&
|
|
205
|
+
reservation.opKind === 'update';
|
|
180
206
|
const mayReplaceUnknown =
|
|
181
207
|
checkedPolicy !== null &&
|
|
182
208
|
!currentPolicy.policyResolved &&
|
|
183
209
|
(reservation.opKind === 'create' || reservation.opKind === 'update');
|
|
184
|
-
const effectivePolicy =
|
|
185
|
-
|
|
186
|
-
|
|
210
|
+
const effectivePolicy =
|
|
211
|
+
mayRefreshExplicitService || mayReplaceUnknown
|
|
212
|
+
? reservationPolicy
|
|
213
|
+
: mergeShareLinkPolicyConservatively(currentPolicy, reservationPolicy);
|
|
187
214
|
|
|
188
215
|
await tx.run(
|
|
189
216
|
`MATCH (s:ShareLink {id: $shareId})
|
|
@@ -193,6 +220,12 @@ export const finalizeReservation = async (
|
|
|
193
220
|
props: {
|
|
194
221
|
version: share.version + 1,
|
|
195
222
|
contentVersion: nextContentVersion,
|
|
223
|
+
attachmentId: reservation.objectRef
|
|
224
|
+
? (reservation.attachmentId ?? null)
|
|
225
|
+
: (share.attachmentId ?? null),
|
|
226
|
+
attachmentChunkCount: reservation.objectRef
|
|
227
|
+
? (reservation.attachmentChunkCount ?? null)
|
|
228
|
+
: (share.attachmentChunkCount ?? null),
|
|
196
229
|
status: 'active',
|
|
197
230
|
contentState: 'finalized',
|
|
198
231
|
activeObjectRef: nextObjectRef,
|
|
@@ -244,6 +277,21 @@ export const finalizeReservation = async (
|
|
|
244
277
|
let cleanupQueuedFor: string | null = null;
|
|
245
278
|
|
|
246
279
|
if (previousObjectRef && previousObjectRef !== nextObjectRef) {
|
|
280
|
+
if (share.attachmentId && share.attachmentChunkCount) {
|
|
281
|
+
await queueShareAttachmentCleanup(
|
|
282
|
+
tx,
|
|
283
|
+
{
|
|
284
|
+
namespace: share.namespace,
|
|
285
|
+
ownerProfileId: share.ownerProfileId,
|
|
286
|
+
shareId: share.id,
|
|
287
|
+
contentVersion: share.contentVersion,
|
|
288
|
+
attachmentId: share.attachmentId,
|
|
289
|
+
chunkCount: share.attachmentChunkCount,
|
|
290
|
+
},
|
|
291
|
+
'superseded',
|
|
292
|
+
nowIso
|
|
293
|
+
);
|
|
294
|
+
}
|
|
247
295
|
await enqueueCleanupJob(tx, {
|
|
248
296
|
objectRef: previousObjectRef,
|
|
249
297
|
operationId: share.activeObjectOperationId!,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import type { ShareLinkOperationKind } from '@helpers/share-link-lifecycle';
|
|
4
|
+
import type { ShareLinkPolicySnapshot } from '@helpers/share-link-policy/types';
|
|
4
5
|
|
|
5
6
|
import type { ShareContentState, ShareLinkRecord, ShareLinkStatus } from '../../models/ShareLink';
|
|
6
7
|
import type { Neo4jQueryResult, ShareLinkTransaction } from './transaction';
|
|
@@ -94,6 +95,8 @@ export const toShareLinkRecord = (props: Record<string, unknown>): ShareLinkReco
|
|
|
94
95
|
ownerProfileId: asString(props.ownerProfileId),
|
|
95
96
|
version: asNumber(props.version, 1),
|
|
96
97
|
contentVersion: asNumber(props.contentVersion, 1),
|
|
98
|
+
attachmentId: asNullableString(props.attachmentId),
|
|
99
|
+
attachmentChunkCount: asNullableNumber(props.attachmentChunkCount),
|
|
97
100
|
generation: asNumber(props.generation, 1),
|
|
98
101
|
status: isOneOf(props.status, SHARE_LINK_STATUSES) ? props.status : 'pending',
|
|
99
102
|
contentState: isOneOf(props.contentState, SHARE_CONTENT_STATES)
|
|
@@ -135,6 +138,8 @@ export const toShareLinkReservationRecord = (
|
|
|
135
138
|
operationId: asString(props.operationId),
|
|
136
139
|
objectRef: asNullableString(props.objectRef),
|
|
137
140
|
contentVersion: asNullableNumber(props.contentVersion),
|
|
141
|
+
attachmentId: asNullableString(props.attachmentId),
|
|
142
|
+
attachmentChunkCount: asNullableNumber(props.attachmentChunkCount),
|
|
138
143
|
baseVersion: asNumber(props.baseVersion, 1),
|
|
139
144
|
baseContentVersion: asNumber(props.baseContentVersion, 1),
|
|
140
145
|
contentHash: asNullableString(props.contentHash),
|
|
@@ -160,6 +165,20 @@ export const toShareLinkReservationRecord = (
|
|
|
160
165
|
updatedAt: asString(props.updatedAt),
|
|
161
166
|
});
|
|
162
167
|
|
|
168
|
+
/** Legacy operation records have no policy ceiling; callers fall back to their reservation. */
|
|
169
|
+
export const toShareLinkOperationPolicy = (
|
|
170
|
+
props: Record<string, unknown>
|
|
171
|
+
): ShareLinkPolicySnapshot | null => {
|
|
172
|
+
if (typeof props.policyViewCountingEnabled !== 'boolean') return null;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
isMinor: asNullableBoolean(props.policyIsMinor),
|
|
176
|
+
policyResolved: asBoolean(props.policyResolved, false),
|
|
177
|
+
defaultExpiryDays: asNumber(props.policyDefaultExpiryDays, 30) === 365 ? 365 : 30,
|
|
178
|
+
viewCountingEnabled: props.policyViewCountingEnabled,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
|
|
163
182
|
export const toShareLinkOperationRecord = (
|
|
164
183
|
props: Record<string, unknown>
|
|
165
184
|
): ShareLinkOperationRecord => ({
|
|
@@ -296,6 +315,7 @@ export type WriteOperationInput = ShareLinkOperationKey & {
|
|
|
296
315
|
requestHash: string;
|
|
297
316
|
now: string;
|
|
298
317
|
pruneAfter: string;
|
|
318
|
+
policy?: ShareLinkPolicySnapshot;
|
|
299
319
|
};
|
|
300
320
|
|
|
301
321
|
/**
|
|
@@ -322,8 +342,19 @@ export const writeOperationInProgress = async (
|
|
|
322
342
|
o.status = 'in_progress',
|
|
323
343
|
o.resultJson = null,
|
|
324
344
|
o.resultVersion = null,
|
|
325
|
-
o.updatedAt = $now
|
|
326
|
-
|
|
345
|
+
o.updatedAt = $now
|
|
346
|
+
SET o += $policyProps`,
|
|
347
|
+
{
|
|
348
|
+
...input,
|
|
349
|
+
policyProps: input.policy
|
|
350
|
+
? {
|
|
351
|
+
policyIsMinor: input.policy.isMinor,
|
|
352
|
+
policyResolved: input.policy.policyResolved,
|
|
353
|
+
policyDefaultExpiryDays: input.policy.defaultExpiryDays,
|
|
354
|
+
policyViewCountingEnabled: input.policy.viewCountingEnabled,
|
|
355
|
+
}
|
|
356
|
+
: {},
|
|
357
|
+
}
|
|
327
358
|
);
|
|
328
359
|
};
|
|
329
360
|
|
|
@@ -11,6 +11,7 @@ import { neogma } from '@instance';
|
|
|
11
11
|
|
|
12
12
|
import { ensureShareLinkConstraints } from '../../models/share-link-constraints';
|
|
13
13
|
import { failShareLink } from './errors';
|
|
14
|
+
import { queueShareAttachmentCleanup } from './attachment';
|
|
14
15
|
import {
|
|
15
16
|
deleteReservation,
|
|
16
17
|
enqueueCleanupJob,
|
|
@@ -514,6 +515,25 @@ export const abandonRecoveredReservation = async (
|
|
|
514
515
|
}
|
|
515
516
|
|
|
516
517
|
let cleanupQueuedFor: string | null = null;
|
|
518
|
+
if (
|
|
519
|
+
reservation.attachmentId &&
|
|
520
|
+
reservation.attachmentChunkCount &&
|
|
521
|
+
reservation.contentVersion
|
|
522
|
+
) {
|
|
523
|
+
await queueShareAttachmentCleanup(
|
|
524
|
+
tx,
|
|
525
|
+
{
|
|
526
|
+
namespace: reservation.namespace,
|
|
527
|
+
ownerProfileId: reservation.ownerProfileId,
|
|
528
|
+
shareId: reservation.shareId,
|
|
529
|
+
contentVersion: reservation.contentVersion,
|
|
530
|
+
attachmentId: reservation.attachmentId,
|
|
531
|
+
chunkCount: reservation.attachmentChunkCount,
|
|
532
|
+
},
|
|
533
|
+
'abandoned',
|
|
534
|
+
nowIso
|
|
535
|
+
);
|
|
536
|
+
}
|
|
517
537
|
|
|
518
538
|
if (reservation.objectRef) {
|
|
519
539
|
await enqueueCleanupJob(tx, {
|