@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,545 @@
|
|
|
1
|
+
import { int } from 'neo4j-driver';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
clampShareLinkRecoveryBatch,
|
|
5
|
+
classifyOperationBinding,
|
|
6
|
+
classifyReservationBinding,
|
|
7
|
+
computeLeaseExpiry,
|
|
8
|
+
isLeaseActive,
|
|
9
|
+
} from '@helpers/share-link-lifecycle';
|
|
10
|
+
import { neogma } from '@instance';
|
|
11
|
+
|
|
12
|
+
import { ensureShareLinkConstraints } from '../../models/share-link-constraints';
|
|
13
|
+
import { failShareLink } from './errors';
|
|
14
|
+
import {
|
|
15
|
+
deleteReservation,
|
|
16
|
+
enqueueCleanupJob,
|
|
17
|
+
lockShare,
|
|
18
|
+
markOperationAbandoned,
|
|
19
|
+
readNodeProperties,
|
|
20
|
+
readReservation,
|
|
21
|
+
toShareLinkOperationRecord,
|
|
22
|
+
toShareLinkRecord,
|
|
23
|
+
toShareLinkReservationRecord,
|
|
24
|
+
} from './helpers';
|
|
25
|
+
import { getShareLink } from './read';
|
|
26
|
+
import { withShareLinkRead, withShareLinkTransaction, type Neo4jQueryResult } from './transaction';
|
|
27
|
+
import type {
|
|
28
|
+
AbandonRecoveredReservationInput,
|
|
29
|
+
AbandonRecoveredReservationResult,
|
|
30
|
+
ClaimRecoverableReservationInput,
|
|
31
|
+
ClaimRecoverableReservationResult,
|
|
32
|
+
DiscoverRecoverableReservationsInput,
|
|
33
|
+
DiscoverRecoverableReservationsResult,
|
|
34
|
+
ShareLinkRecoveryKey,
|
|
35
|
+
ShareLinkRecoveryTarget,
|
|
36
|
+
} from './types';
|
|
37
|
+
|
|
38
|
+
type GraphRunner = {
|
|
39
|
+
run: (query: string, params?: Record<string, unknown>) => Promise<Neo4jQueryResult>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const isOpaqueIdentifier = (value: unknown, maxLength = 128): value is string =>
|
|
43
|
+
typeof value === 'string' &&
|
|
44
|
+
value.length >= 1 &&
|
|
45
|
+
value.length <= maxLength &&
|
|
46
|
+
/^[A-Za-z0-9._~-]+$/.test(value);
|
|
47
|
+
|
|
48
|
+
const assertRecoveryKey = (input: ShareLinkRecoveryKey): void => {
|
|
49
|
+
if (
|
|
50
|
+
!isOpaqueIdentifier(input.namespace) ||
|
|
51
|
+
!isOpaqueIdentifier(input.ownerProfileId) ||
|
|
52
|
+
!isOpaqueIdentifier(input.shareId) ||
|
|
53
|
+
!isOpaqueIdentifier(input.operationId)
|
|
54
|
+
) {
|
|
55
|
+
failShareLink('INVALID_INPUT', 'recovery key must be well-formed opaque identifiers');
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function assertLeaseOwner(leaseOwner: unknown): asserts leaseOwner is string {
|
|
60
|
+
if (!isOpaqueIdentifier(leaseOwner)) {
|
|
61
|
+
failShareLink('INVALID_INPUT', 'leaseOwner must be a well-formed opaque identifier');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Do not let permissive legacy record converters normalize corrupt fences. */
|
|
66
|
+
const hasValidShareFence = (props: Record<string, unknown>): boolean =>
|
|
67
|
+
['version', 'contentVersion', 'generation'].every(
|
|
68
|
+
field =>
|
|
69
|
+
Number.isSafeInteger(props[field]) &&
|
|
70
|
+
(props[field] as number) >= 1 &&
|
|
71
|
+
(props[field] as number) < Number.MAX_SAFE_INTEGER
|
|
72
|
+
) &&
|
|
73
|
+
typeof props.status === 'string' &&
|
|
74
|
+
['pending', 'active', 'stopped'].includes(props.status) &&
|
|
75
|
+
typeof props.contentState === 'string' &&
|
|
76
|
+
['staging', 'finalized', 'content_missing'].includes(props.contentState);
|
|
77
|
+
|
|
78
|
+
/** Metadata will be activated without the original request, so validate it before conversion. */
|
|
79
|
+
const hasValidReservationIntent = (props: Record<string, unknown>): boolean =>
|
|
80
|
+
typeof props.title === 'string' &&
|
|
81
|
+
(props.note == null || typeof props.note === 'string') &&
|
|
82
|
+
(props.expiresAt == null ||
|
|
83
|
+
(typeof props.expiresAt === 'string' && Number.isFinite(Date.parse(props.expiresAt)))) &&
|
|
84
|
+
Number.isSafeInteger(props.selectedCount) &&
|
|
85
|
+
(props.selectedCount as number) >= 1 &&
|
|
86
|
+
(props.selectedCount as number) <= 50 &&
|
|
87
|
+
(props.opKind !== 'create' ||
|
|
88
|
+
(typeof props.objectRef === 'string' && props.contentVersion === 1)) &&
|
|
89
|
+
(props.opKind !== 'update' ||
|
|
90
|
+
props.contentVersion == null ||
|
|
91
|
+
props.contentVersion === (props.baseContentVersion as number) + 1);
|
|
92
|
+
|
|
93
|
+
/** Binds the idempotency operation record to the exact scoped key. */
|
|
94
|
+
const readOperationForRecovery = async (
|
|
95
|
+
runner: GraphRunner,
|
|
96
|
+
input: ShareLinkRecoveryKey
|
|
97
|
+
): Promise<Record<string, unknown> | null> => {
|
|
98
|
+
const result = await runner.run(
|
|
99
|
+
`MATCH (o:ShareLinkOperation {operationId: $operationId})
|
|
100
|
+
WHERE o.namespace = $namespace
|
|
101
|
+
AND o.ownerProfileId = $ownerProfileId
|
|
102
|
+
AND o.shareId = $shareId
|
|
103
|
+
RETURN o LIMIT 1`,
|
|
104
|
+
input
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return readNodeProperties(result, 'o');
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read-only scoped recovery lookup derived entirely from persisted Brain state.
|
|
112
|
+
*
|
|
113
|
+
* A reservation that matches the exact namespace/owner/share/operation key is
|
|
114
|
+
* returned with its committed share snapshot. When the reservation is already
|
|
115
|
+
* consumed but the operation committed — a finalize whose response was lost —
|
|
116
|
+
* the current committed share is returned instead, so a retry can observe success
|
|
117
|
+
* without re-uploading or reconstructing the deleted reservation. A wrong
|
|
118
|
+
* namespace/owner/operation returns `absent`; this function performs no remote
|
|
119
|
+
* I/O and never trusts a caller-supplied object ref, hash or generation.
|
|
120
|
+
*/
|
|
121
|
+
export const readShareLinkRecoveryTarget = async (
|
|
122
|
+
input: ShareLinkRecoveryKey
|
|
123
|
+
): Promise<ShareLinkRecoveryTarget> => {
|
|
124
|
+
assertRecoveryKey(input);
|
|
125
|
+
|
|
126
|
+
const share = await getShareLink({
|
|
127
|
+
shareId: input.shareId,
|
|
128
|
+
namespace: input.namespace,
|
|
129
|
+
ownerProfileId: input.ownerProfileId,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!share) return { state: 'absent' };
|
|
133
|
+
|
|
134
|
+
const reservationResult = await neogma.queryRunner.run(
|
|
135
|
+
'MATCH (r:ShareLinkReservation {shareId: $shareId}) RETURN r LIMIT 1',
|
|
136
|
+
{ shareId: input.shareId }
|
|
137
|
+
);
|
|
138
|
+
const reservationProps = readNodeProperties(reservationResult, 'r');
|
|
139
|
+
|
|
140
|
+
if (reservationProps && classifyReservationBinding(reservationProps, input) === 'ok') {
|
|
141
|
+
return {
|
|
142
|
+
state: 'reservation',
|
|
143
|
+
share,
|
|
144
|
+
reservation: toShareLinkReservationRecord(reservationProps),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const operationProps = await readOperationForRecovery(neogma.queryRunner, input);
|
|
149
|
+
|
|
150
|
+
if (operationProps && classifyOperationBinding(operationProps, input) === 'ok') {
|
|
151
|
+
const operation = toShareLinkOperationRecord(operationProps);
|
|
152
|
+
|
|
153
|
+
if (operation.status === 'committed') {
|
|
154
|
+
return { state: 'committed', share };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { state: 'absent' };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Enumerates only *expired in-flight* reservation keys for one explicit
|
|
163
|
+
* namespace. The bounded query joins the share and idempotency operation so it
|
|
164
|
+
* returns neither active leases nor stale/foreign bindings; malformed records
|
|
165
|
+
* fail to match and are therefore never offered for reclamation.
|
|
166
|
+
*/
|
|
167
|
+
export const discoverRecoverableReservations = async (
|
|
168
|
+
input: DiscoverRecoverableReservationsInput
|
|
169
|
+
): Promise<DiscoverRecoverableReservationsResult> => {
|
|
170
|
+
await ensureShareLinkConstraints();
|
|
171
|
+
|
|
172
|
+
if (!isOpaqueIdentifier(input.namespace)) {
|
|
173
|
+
failShareLink('INVALID_INPUT', 'discovery requires an explicit opaque namespace');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const now = input.now ?? new Date();
|
|
177
|
+
const limit = clampShareLinkRecoveryBatch(input.limit);
|
|
178
|
+
|
|
179
|
+
const result = await withShareLinkRead(
|
|
180
|
+
runner =>
|
|
181
|
+
runner.run(
|
|
182
|
+
`MATCH (r:ShareLinkReservation {namespace: $namespace})
|
|
183
|
+
MATCH (s:ShareLink {id: r.shareId})
|
|
184
|
+
MATCH (o:ShareLinkOperation {operationId: r.operationId})
|
|
185
|
+
WHERE r.leaseExpiresAt <= $now
|
|
186
|
+
AND s.namespace = $namespace
|
|
187
|
+
AND s.ownerProfileId = r.ownerProfileId
|
|
188
|
+
AND s.status <> 'stopped'
|
|
189
|
+
AND s.generation = r.generation
|
|
190
|
+
AND s.version = r.baseVersion
|
|
191
|
+
AND s.contentVersion = r.baseContentVersion
|
|
192
|
+
AND o.status = 'in_progress'
|
|
193
|
+
AND o.shareId = r.shareId
|
|
194
|
+
AND o.namespace = r.namespace
|
|
195
|
+
AND o.ownerProfileId = r.ownerProfileId
|
|
196
|
+
RETURN DISTINCT r.namespace AS namespace,
|
|
197
|
+
r.ownerProfileId AS ownerProfileId,
|
|
198
|
+
r.shareId AS shareId,
|
|
199
|
+
r.operationId AS operationId,
|
|
200
|
+
r.leaseExpiresAt AS leaseExpiresAt
|
|
201
|
+
ORDER BY leaseExpiresAt ASC, shareId ASC
|
|
202
|
+
LIMIT $limit`,
|
|
203
|
+
{ namespace: input.namespace, now: now.toISOString(), limit: int(limit) }
|
|
204
|
+
),
|
|
205
|
+
{ timeoutMs: input.transactionTimeoutMs, noInlineRetry: input.noInlineRetry }
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const keys: ShareLinkRecoveryKey[] = [];
|
|
209
|
+
|
|
210
|
+
for (const record of result.records) {
|
|
211
|
+
const namespace = record.get('namespace');
|
|
212
|
+
const ownerProfileId = record.get('ownerProfileId');
|
|
213
|
+
const shareId = record.get('shareId');
|
|
214
|
+
const operationId = record.get('operationId');
|
|
215
|
+
|
|
216
|
+
if (
|
|
217
|
+
typeof namespace === 'string' &&
|
|
218
|
+
typeof ownerProfileId === 'string' &&
|
|
219
|
+
typeof shareId === 'string' &&
|
|
220
|
+
typeof operationId === 'string'
|
|
221
|
+
) {
|
|
222
|
+
keys.push({ namespace, ownerProfileId, shareId, operationId });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { keys };
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Atomically claims one exact expired reservation for a recovery worker.
|
|
231
|
+
*
|
|
232
|
+
* Under the share write lock the persisted reservation, operation and share are
|
|
233
|
+
* re-read and validated against the current version/content version, generation,
|
|
234
|
+
* owner/namespace/operation binding and non-stopped state before anything is
|
|
235
|
+
* written. A claim then bumps the share and reservation generation together and
|
|
236
|
+
* renews only the lease owner/expiry, so two claimers cannot both win and the
|
|
237
|
+
* immutable object tuple, request hashes, metadata and original operation id are
|
|
238
|
+
* preserved. Any malformed persisted binding fails closed.
|
|
239
|
+
*/
|
|
240
|
+
export const claimRecoverableReservation = async (
|
|
241
|
+
input: ClaimRecoverableReservationInput
|
|
242
|
+
): Promise<ClaimRecoverableReservationResult> => {
|
|
243
|
+
await ensureShareLinkConstraints();
|
|
244
|
+
assertRecoveryKey(input);
|
|
245
|
+
assertLeaseOwner(input.leaseOwner);
|
|
246
|
+
|
|
247
|
+
return withShareLinkTransaction(
|
|
248
|
+
async tx => {
|
|
249
|
+
const lockedProps = await lockShare(tx, input.shareId);
|
|
250
|
+
// Production uses the actual clock after the lock is held; an injected
|
|
251
|
+
// test clock is allowed for deterministic lease windows.
|
|
252
|
+
const now = input.now ?? new Date();
|
|
253
|
+
const nowIso = now.toISOString();
|
|
254
|
+
|
|
255
|
+
if (!lockedProps) return { outcome: 'not_claimable', reason: 'absent' };
|
|
256
|
+
|
|
257
|
+
const share = toShareLinkRecord(lockedProps);
|
|
258
|
+
|
|
259
|
+
if (
|
|
260
|
+
share.id !== input.shareId ||
|
|
261
|
+
share.namespace !== input.namespace ||
|
|
262
|
+
share.ownerProfileId !== input.ownerProfileId
|
|
263
|
+
) {
|
|
264
|
+
return { outcome: 'not_claimable', reason: 'absent' };
|
|
265
|
+
}
|
|
266
|
+
if (!hasValidShareFence(lockedProps)) {
|
|
267
|
+
return { outcome: 'not_claimable', reason: 'malformed_binding' };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const reservationProps = await readReservation(tx, input.shareId);
|
|
271
|
+
|
|
272
|
+
if (!reservationProps) {
|
|
273
|
+
return { outcome: 'not_claimable', reason: 'absent' };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const binding = classifyReservationBinding(reservationProps, input);
|
|
277
|
+
|
|
278
|
+
if (binding === 'malformed_binding' || !hasValidReservationIntent(reservationProps)) {
|
|
279
|
+
return { outcome: 'not_claimable', reason: 'malformed_binding' };
|
|
280
|
+
}
|
|
281
|
+
if (binding === 'binding_mismatch') {
|
|
282
|
+
return { outcome: 'not_claimable', reason: 'binding_mismatch' };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const reservation = toShareLinkReservationRecord(reservationProps);
|
|
286
|
+
|
|
287
|
+
if (share.status === 'stopped') {
|
|
288
|
+
return { outcome: 'not_claimable', reason: 'share_not_recoverable' };
|
|
289
|
+
}
|
|
290
|
+
if (share.generation !== reservation.generation) {
|
|
291
|
+
return { outcome: 'not_claimable', reason: 'stale_generation' };
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
share.version !== reservation.baseVersion ||
|
|
295
|
+
share.contentVersion !== reservation.baseContentVersion
|
|
296
|
+
) {
|
|
297
|
+
return { outcome: 'not_claimable', reason: 'version_changed' };
|
|
298
|
+
}
|
|
299
|
+
if (isLeaseActive(reservation.leaseExpiresAt, now)) {
|
|
300
|
+
return { outcome: 'not_claimable', reason: 'lease_active' };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const operationProps = await readOperationForRecovery(tx, input);
|
|
304
|
+
|
|
305
|
+
if (!operationProps) {
|
|
306
|
+
return { outcome: 'not_claimable', reason: 'operation_not_in_progress' };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const operationBinding = classifyOperationBinding(operationProps, {
|
|
310
|
+
...input,
|
|
311
|
+
opKind: reservation.opKind,
|
|
312
|
+
clientRequestId: reservation.clientRequestId,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (operationBinding === 'malformed_binding') {
|
|
316
|
+
return { outcome: 'not_claimable', reason: 'malformed_binding' };
|
|
317
|
+
}
|
|
318
|
+
if (
|
|
319
|
+
!/^[a-f0-9]{64}$/.test(reservation.requestHash) ||
|
|
320
|
+
typeof operationProps.requestHash !== 'string' ||
|
|
321
|
+
!/^[a-f0-9]{64}$/.test(operationProps.requestHash)
|
|
322
|
+
) {
|
|
323
|
+
return { outcome: 'not_claimable', reason: 'malformed_binding' };
|
|
324
|
+
}
|
|
325
|
+
if (reservation.requestHash !== operationProps.requestHash) {
|
|
326
|
+
return { outcome: 'not_claimable', reason: 'binding_mismatch' };
|
|
327
|
+
}
|
|
328
|
+
if (operationBinding === 'binding_mismatch') {
|
|
329
|
+
return { outcome: 'not_claimable', reason: 'binding_mismatch' };
|
|
330
|
+
}
|
|
331
|
+
if (toShareLinkOperationRecord(operationProps).status !== 'in_progress') {
|
|
332
|
+
return { outcome: 'not_claimable', reason: 'operation_not_in_progress' };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const nextGeneration = reservation.generation + 1;
|
|
336
|
+
const leaseExpiresAt = computeLeaseExpiry(now, input.leaseMs);
|
|
337
|
+
|
|
338
|
+
await tx.run(
|
|
339
|
+
`MATCH (s:ShareLink {id: $shareId})
|
|
340
|
+
SET s.generation = $generation,
|
|
341
|
+
s.lockTick = coalesce(s.lockTick, 0) + 1,
|
|
342
|
+
s.updatedAt = $now`,
|
|
343
|
+
{ shareId: input.shareId, generation: nextGeneration, now: nowIso }
|
|
344
|
+
);
|
|
345
|
+
await tx.run(
|
|
346
|
+
`MATCH (r:ShareLinkReservation {shareId: $shareId})
|
|
347
|
+
SET r.generation = $generation,
|
|
348
|
+
r.leaseOwner = $leaseOwner,
|
|
349
|
+
r.leaseExpiresAt = $leaseExpiresAt,
|
|
350
|
+
r.updatedAt = $now`,
|
|
351
|
+
{
|
|
352
|
+
shareId: input.shareId,
|
|
353
|
+
generation: nextGeneration,
|
|
354
|
+
leaseOwner: input.leaseOwner,
|
|
355
|
+
leaseExpiresAt,
|
|
356
|
+
now: nowIso,
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const claimedShareProps = await tx.run(
|
|
361
|
+
'MATCH (s:ShareLink {id: $shareId}) RETURN s LIMIT 1',
|
|
362
|
+
{ shareId: input.shareId }
|
|
363
|
+
);
|
|
364
|
+
const claimedReservationProps = await readReservation(tx, input.shareId);
|
|
365
|
+
|
|
366
|
+
const claimedShare = readNodeProperties(claimedShareProps, 's');
|
|
367
|
+
const claimedReservation = claimedReservationProps;
|
|
368
|
+
|
|
369
|
+
if (!claimedShare || !claimedReservation) {
|
|
370
|
+
failShareLink('CONFLICT', 'failed to persist the recovery claim');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
outcome: 'claimed',
|
|
375
|
+
share: toShareLinkRecord(claimedShare),
|
|
376
|
+
reservation: toShareLinkReservationRecord(claimedReservation),
|
|
377
|
+
};
|
|
378
|
+
},
|
|
379
|
+
{ timeoutMs: input.transactionTimeoutMs, noInlineRetry: input.noInlineRetry }
|
|
380
|
+
);
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Fenced recovery abandon.
|
|
385
|
+
*
|
|
386
|
+
* This is deliberately separate from the internal `abandonReservation`: it
|
|
387
|
+
* requires both the claimed generation and lease owner AND an active lease, so a
|
|
388
|
+
* worker whose lease already lapsed — or a stale generation, even when the
|
|
389
|
+
* claimant string is reused — cannot destroy work another worker now owns.
|
|
390
|
+
* Intentional stop/revoke semantics still use the internal path and are
|
|
391
|
+
* untouched.
|
|
392
|
+
*/
|
|
393
|
+
export const abandonRecoveredReservation = async (
|
|
394
|
+
input: AbandonRecoveredReservationInput
|
|
395
|
+
): Promise<AbandonRecoveredReservationResult> => {
|
|
396
|
+
await ensureShareLinkConstraints();
|
|
397
|
+
assertRecoveryKey(input);
|
|
398
|
+
assertLeaseOwner(input.leaseOwner);
|
|
399
|
+
|
|
400
|
+
if (!Number.isSafeInteger(input.generation) || input.generation < 1) {
|
|
401
|
+
failShareLink('INVALID_INPUT', 'generation must be a positive safe integer');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return withShareLinkTransaction(
|
|
405
|
+
async tx => {
|
|
406
|
+
const lockedProps = await lockShare(tx, input.shareId);
|
|
407
|
+
const now = input.now ?? new Date();
|
|
408
|
+
const nowIso = now.toISOString();
|
|
409
|
+
|
|
410
|
+
if (!lockedProps) return { outcome: 'not_claimable', reason: 'absent' };
|
|
411
|
+
|
|
412
|
+
const share = toShareLinkRecord(lockedProps);
|
|
413
|
+
|
|
414
|
+
if (
|
|
415
|
+
share.id !== input.shareId ||
|
|
416
|
+
share.namespace !== input.namespace ||
|
|
417
|
+
share.ownerProfileId !== input.ownerProfileId
|
|
418
|
+
) {
|
|
419
|
+
return { outcome: 'not_claimable', reason: 'absent' };
|
|
420
|
+
}
|
|
421
|
+
if (!hasValidShareFence(lockedProps)) {
|
|
422
|
+
return { outcome: 'not_claimable', reason: 'malformed_binding' };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const reservationProps = await readReservation(tx, input.shareId);
|
|
426
|
+
|
|
427
|
+
if (!reservationProps) {
|
|
428
|
+
const operationProps = await readOperationForRecovery(tx, input);
|
|
429
|
+
|
|
430
|
+
if (
|
|
431
|
+
operationProps &&
|
|
432
|
+
classifyOperationBinding(operationProps, input) === 'ok' &&
|
|
433
|
+
toShareLinkOperationRecord(operationProps).status === 'committed'
|
|
434
|
+
) {
|
|
435
|
+
return { outcome: 'already_finalized', share };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return { outcome: 'already_absent' };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const binding = classifyReservationBinding(reservationProps, input);
|
|
442
|
+
|
|
443
|
+
if (binding === 'malformed_binding' || !hasValidReservationIntent(reservationProps)) {
|
|
444
|
+
return { outcome: 'not_claimable', reason: 'malformed_binding' };
|
|
445
|
+
}
|
|
446
|
+
if (binding === 'binding_mismatch') {
|
|
447
|
+
return { outcome: 'already_absent' };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const reservation = toShareLinkReservationRecord(reservationProps);
|
|
451
|
+
|
|
452
|
+
if (
|
|
453
|
+
share.status === 'stopped' ||
|
|
454
|
+
(reservation.objectRef && share.activeObjectRef === reservation.objectRef)
|
|
455
|
+
) {
|
|
456
|
+
return { outcome: 'not_claimable', reason: 'share_not_recoverable' };
|
|
457
|
+
}
|
|
458
|
+
if (share.generation !== reservation.generation) {
|
|
459
|
+
return { outcome: 'not_claimable', reason: 'stale_generation' };
|
|
460
|
+
}
|
|
461
|
+
if (
|
|
462
|
+
share.version !== reservation.baseVersion ||
|
|
463
|
+
share.contentVersion !== reservation.baseContentVersion
|
|
464
|
+
) {
|
|
465
|
+
return { outcome: 'not_claimable', reason: 'version_changed' };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (
|
|
469
|
+
reservation.generation !== input.generation ||
|
|
470
|
+
reservation.leaseOwner !== input.leaseOwner
|
|
471
|
+
) {
|
|
472
|
+
return { outcome: 'not_claimable', reason: 'stale_generation' };
|
|
473
|
+
}
|
|
474
|
+
if (!isLeaseActive(reservation.leaseExpiresAt, now)) {
|
|
475
|
+
return { outcome: 'not_claimable', reason: 'lease_lapsed' };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const operationProps = await readOperationForRecovery(tx, input);
|
|
479
|
+
|
|
480
|
+
if (!operationProps) {
|
|
481
|
+
return { outcome: 'not_claimable', reason: 'operation_not_in_progress' };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const operationBinding = classifyOperationBinding(operationProps, {
|
|
485
|
+
...input,
|
|
486
|
+
opKind: reservation.opKind,
|
|
487
|
+
clientRequestId: reservation.clientRequestId,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
if (operationBinding === 'malformed_binding') {
|
|
491
|
+
return { outcome: 'not_claimable', reason: 'malformed_binding' };
|
|
492
|
+
}
|
|
493
|
+
if (
|
|
494
|
+
!/^[a-f0-9]{64}$/.test(reservation.requestHash) ||
|
|
495
|
+
typeof operationProps.requestHash !== 'string' ||
|
|
496
|
+
!/^[a-f0-9]{64}$/.test(operationProps.requestHash)
|
|
497
|
+
) {
|
|
498
|
+
return { outcome: 'not_claimable', reason: 'malformed_binding' };
|
|
499
|
+
}
|
|
500
|
+
if (reservation.requestHash !== operationProps.requestHash) {
|
|
501
|
+
return { outcome: 'not_claimable', reason: 'binding_mismatch' };
|
|
502
|
+
}
|
|
503
|
+
if (operationBinding === 'binding_mismatch') {
|
|
504
|
+
return { outcome: 'not_claimable', reason: 'binding_mismatch' };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const operation = toShareLinkOperationRecord(operationProps);
|
|
508
|
+
|
|
509
|
+
if (operation.status === 'committed') {
|
|
510
|
+
return { outcome: 'already_finalized', share };
|
|
511
|
+
}
|
|
512
|
+
if (operation.status !== 'in_progress') {
|
|
513
|
+
return { outcome: 'not_claimable', reason: 'operation_not_in_progress' };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let cleanupQueuedFor: string | null = null;
|
|
517
|
+
|
|
518
|
+
if (reservation.objectRef) {
|
|
519
|
+
await enqueueCleanupJob(tx, {
|
|
520
|
+
objectRef: reservation.objectRef,
|
|
521
|
+
operationId: reservation.operationId,
|
|
522
|
+
namespace: reservation.namespace,
|
|
523
|
+
ownerProfileId: reservation.ownerProfileId,
|
|
524
|
+
shareId: reservation.shareId,
|
|
525
|
+
contentVersion: reservation.contentVersion ?? reservation.baseContentVersion,
|
|
526
|
+
reason: 'abandoned',
|
|
527
|
+
now: nowIso,
|
|
528
|
+
});
|
|
529
|
+
cleanupQueuedFor = reservation.objectRef;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
await markOperationAbandoned(tx, {
|
|
533
|
+
namespace: reservation.namespace,
|
|
534
|
+
ownerProfileId: reservation.ownerProfileId,
|
|
535
|
+
opKind: reservation.opKind,
|
|
536
|
+
clientRequestId: reservation.clientRequestId,
|
|
537
|
+
now: nowIso,
|
|
538
|
+
});
|
|
539
|
+
await deleteReservation(tx, input.shareId);
|
|
540
|
+
|
|
541
|
+
return { outcome: 'abandoned', cleanupQueuedFor };
|
|
542
|
+
},
|
|
543
|
+
{ timeoutMs: input.transactionTimeoutMs, noInlineRetry: input.noInlineRetry }
|
|
544
|
+
);
|
|
545
|
+
};
|