@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,8 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export { createShareLinkCoordinator } from './coordinator';
|
|
3
|
+
export { runShareContentCleanupOnce } from './cleanup-runner';
|
|
4
|
+
export { recoverShareLinkOperation, runShareLinkRecoveryOnce } from './recovery-runner';
|
|
5
|
+
|
|
6
|
+
// Runtime wiring over the real C3 repository lives in
|
|
7
|
+
// `@helpers/share-link-coordinator/runtime` so unit tests and the coordinator
|
|
8
|
+
// barrel never load `@instance`/Neo4j.
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { isShareLinkRepositoryError } from '../../accesslayer/share-link/errors';
|
|
2
|
+
import type {
|
|
3
|
+
RecoveryClaimRejection,
|
|
4
|
+
ShareLinkReservationRecord,
|
|
5
|
+
} from '../../accesslayer/share-link';
|
|
6
|
+
import type { ShareLinkRecord } from '../../models/ShareLink';
|
|
7
|
+
import { isTransientShareContentError } from '../share-content-client/types';
|
|
8
|
+
import { budgetAllows, budgetHasUnitReserve, boundTransactionTimeout } from './budget-helpers';
|
|
9
|
+
import { verifyShareContentActiveStat } from './stat';
|
|
10
|
+
import {
|
|
11
|
+
ShareLinkCoordinatorError,
|
|
12
|
+
type RecoveryRunnerDependencies,
|
|
13
|
+
type ScopedShareRecoveryResult,
|
|
14
|
+
type ShareRecoveryCategory,
|
|
15
|
+
type ShareRecoveryRunSummary,
|
|
16
|
+
} from './types';
|
|
17
|
+
|
|
18
|
+
const OPAQUE_IDENTIFIER_RE = /^[A-Za-z0-9._~-]+$/;
|
|
19
|
+
|
|
20
|
+
const isOpaqueIdentifier = (value: unknown, maxLength = 128): value is string =>
|
|
21
|
+
typeof value === 'string' &&
|
|
22
|
+
value.length >= 1 &&
|
|
23
|
+
value.length <= maxLength &&
|
|
24
|
+
OPAQUE_IDENTIFIER_RE.test(value);
|
|
25
|
+
|
|
26
|
+
const failInvalidInput = (message: string): never => {
|
|
27
|
+
throw new ShareLinkCoordinatorError('INVALID_INPUT', message);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const assertRunnerScope = (
|
|
31
|
+
dependencies: RecoveryRunnerDependencies,
|
|
32
|
+
key?: { namespace: string; ownerProfileId: string; shareId: string; operationId: string }
|
|
33
|
+
): void => {
|
|
34
|
+
if (!isOpaqueIdentifier(dependencies.claimant)) {
|
|
35
|
+
failInvalidInput('claimant must be an opaque configured identifier');
|
|
36
|
+
}
|
|
37
|
+
if (!isOpaqueIdentifier(dependencies.namespace)) {
|
|
38
|
+
failInvalidInput('namespace must be an opaque configured identifier');
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
key &&
|
|
42
|
+
(key.namespace !== dependencies.namespace ||
|
|
43
|
+
!isOpaqueIdentifier(key.namespace) ||
|
|
44
|
+
!isOpaqueIdentifier(key.ownerProfileId) ||
|
|
45
|
+
!isOpaqueIdentifier(key.shareId) ||
|
|
46
|
+
!isOpaqueIdentifier(key.operationId))
|
|
47
|
+
) {
|
|
48
|
+
failInvalidInput('recovery key must be well-formed opaque identifiers');
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const recordCategory = (
|
|
53
|
+
categories: Partial<Record<ShareRecoveryCategory, number>>,
|
|
54
|
+
category: ShareRecoveryCategory
|
|
55
|
+
): void => {
|
|
56
|
+
categories[category] = (categories[category] ?? 0) + 1;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type RecoveryDriveOutcome =
|
|
60
|
+
| { kind: 'finalized'; share: ShareLinkRecord }
|
|
61
|
+
| { kind: 'abandoned'; category: ShareRecoveryCategory }
|
|
62
|
+
| { kind: 'deferred'; category: ShareRecoveryCategory }
|
|
63
|
+
| { kind: 'claim_lost'; reason: RecoveryClaimRejection };
|
|
64
|
+
|
|
65
|
+
/** Bounded graph cost of one recovery transaction (claim, finalize or abandon). */
|
|
66
|
+
const graphCostMs = (dependencies: RecoveryRunnerDependencies): number =>
|
|
67
|
+
dependencies.transactionTimeoutMs ?? 0;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Worst-case bounded cost of claiming and driving one reservation:
|
|
71
|
+
* claim (graph) + stat (HTTP) + finalize/abandon (graph).
|
|
72
|
+
* The HTTP allowance is fixed when the transport client is constructed, so it
|
|
73
|
+
* cannot be reduced per call; the graph calls are bounded per call below.
|
|
74
|
+
*/
|
|
75
|
+
const recoveryUnitCostMs = (dependencies: RecoveryRunnerDependencies): number =>
|
|
76
|
+
(dependencies.requestTimeoutMs ?? 0) + 2 * graphCostMs(dependencies);
|
|
77
|
+
|
|
78
|
+
/** Cost of the stat/verify tail once a reservation has already been claimed. */
|
|
79
|
+
const recoveryRemoteCostMs = (dependencies: RecoveryRunnerDependencies): number =>
|
|
80
|
+
(dependencies.requestTimeoutMs ?? 0) + graphCostMs(dependencies);
|
|
81
|
+
|
|
82
|
+
const finalizeFailureReason = (error: unknown): RecoveryClaimRejection | null => {
|
|
83
|
+
if (!isShareLinkRepositoryError(error)) return null;
|
|
84
|
+
|
|
85
|
+
switch (error.code) {
|
|
86
|
+
case 'STALE_GENERATION':
|
|
87
|
+
return 'stale_generation';
|
|
88
|
+
case 'LEASE_EXPIRED':
|
|
89
|
+
return 'lease_lapsed';
|
|
90
|
+
case 'NOT_FOUND':
|
|
91
|
+
return 'absent';
|
|
92
|
+
case 'CONFLICT':
|
|
93
|
+
case 'PRECONDITION_FAILED':
|
|
94
|
+
return 'version_changed';
|
|
95
|
+
default:
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const finalizeClaimedReservation = async (
|
|
101
|
+
reservation: ShareLinkReservationRecord,
|
|
102
|
+
dependencies: RecoveryRunnerDependencies
|
|
103
|
+
): Promise<RecoveryDriveOutcome> => {
|
|
104
|
+
try {
|
|
105
|
+
const finalized = await dependencies.repository.finalizeReservation({
|
|
106
|
+
namespace: reservation.namespace,
|
|
107
|
+
ownerProfileId: reservation.ownerProfileId,
|
|
108
|
+
shareId: reservation.shareId,
|
|
109
|
+
operationId: reservation.operationId,
|
|
110
|
+
objectRef: reservation.objectRef,
|
|
111
|
+
generation: reservation.generation,
|
|
112
|
+
leaseOwner: reservation.leaseOwner,
|
|
113
|
+
verifiedContentHash:
|
|
114
|
+
reservation.objectRef === null || reservation.contentHash === null
|
|
115
|
+
? undefined
|
|
116
|
+
: reservation.contentHash,
|
|
117
|
+
now: dependencies.now?.(),
|
|
118
|
+
transactionTimeoutMs: boundTransactionTimeout(
|
|
119
|
+
dependencies.transactionTimeoutMs,
|
|
120
|
+
dependencies.budget
|
|
121
|
+
),
|
|
122
|
+
noInlineRetry: dependencies.noInlineRetry,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return { kind: 'finalized', share: finalized.share };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const reason = finalizeFailureReason(error);
|
|
128
|
+
|
|
129
|
+
// A typed fence failure means another worker may own the reservation now;
|
|
130
|
+
// an unexpected failure keeps the work recoverable until the lease lapses.
|
|
131
|
+
return reason
|
|
132
|
+
? { kind: 'claim_lost', reason }
|
|
133
|
+
: { kind: 'deferred', category: 'deferred_finalize' };
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const abandonClaimedReservation = async (
|
|
138
|
+
reservation: ShareLinkReservationRecord,
|
|
139
|
+
dependencies: RecoveryRunnerDependencies,
|
|
140
|
+
category: ShareRecoveryCategory
|
|
141
|
+
): Promise<RecoveryDriveOutcome> => {
|
|
142
|
+
try {
|
|
143
|
+
const abandoned = await dependencies.repository.abandonRecoveredReservation({
|
|
144
|
+
namespace: reservation.namespace,
|
|
145
|
+
ownerProfileId: reservation.ownerProfileId,
|
|
146
|
+
shareId: reservation.shareId,
|
|
147
|
+
operationId: reservation.operationId,
|
|
148
|
+
generation: reservation.generation,
|
|
149
|
+
leaseOwner: reservation.leaseOwner,
|
|
150
|
+
now: dependencies.now?.(),
|
|
151
|
+
transactionTimeoutMs: boundTransactionTimeout(
|
|
152
|
+
dependencies.transactionTimeoutMs,
|
|
153
|
+
dependencies.budget
|
|
154
|
+
),
|
|
155
|
+
noInlineRetry: dependencies.noInlineRetry,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (abandoned.outcome === 'abandoned') return { kind: 'abandoned', category };
|
|
159
|
+
if (abandoned.outcome === 'already_finalized') {
|
|
160
|
+
return { kind: 'finalized', share: abandoned.share };
|
|
161
|
+
}
|
|
162
|
+
if (abandoned.outcome === 'not_claimable') {
|
|
163
|
+
return { kind: 'claim_lost', reason: abandoned.reason };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { kind: 'claim_lost', reason: 'absent' };
|
|
167
|
+
} catch {
|
|
168
|
+
return { kind: 'deferred', category: 'deferred_finalize' };
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Drives one claimed reservation to a finalize or a fenced abandon using the
|
|
174
|
+
* persisted immutable tuple. All remote stat I/O happens here, outside every
|
|
175
|
+
* graph transaction. A mismatch or dependency failure never destroys content.
|
|
176
|
+
*/
|
|
177
|
+
const driveClaimedReservation = async (
|
|
178
|
+
reservation: ShareLinkReservationRecord,
|
|
179
|
+
dependencies: RecoveryRunnerDependencies
|
|
180
|
+
): Promise<RecoveryDriveOutcome> => {
|
|
181
|
+
if (reservation.objectRef === null || reservation.contentVersion === null) {
|
|
182
|
+
// Metadata-only reservation: finalize without any storage I/O.
|
|
183
|
+
if (
|
|
184
|
+
budgetHasUnitReserve(dependencies.budget) &&
|
|
185
|
+
!budgetAllows(dependencies.budget, graphCostMs(dependencies))
|
|
186
|
+
) {
|
|
187
|
+
return { kind: 'deferred', category: 'budget_exhausted' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return finalizeClaimedReservation(reservation, dependencies);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
budgetHasUnitReserve(dependencies.budget) &&
|
|
195
|
+
!budgetAllows(dependencies.budget, recoveryRemoteCostMs(dependencies))
|
|
196
|
+
) {
|
|
197
|
+
return { kind: 'deferred', category: 'budget_exhausted' };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const stat = await dependencies.client.stat({
|
|
201
|
+
namespace: reservation.namespace,
|
|
202
|
+
ownerProfileId: reservation.ownerProfileId,
|
|
203
|
+
shareId: reservation.shareId,
|
|
204
|
+
contentVersion: reservation.contentVersion,
|
|
205
|
+
objectId: reservation.objectRef,
|
|
206
|
+
operationId: reservation.operationId,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (!stat.ok) {
|
|
210
|
+
if (stat.error === 'NOT_FOUND') {
|
|
211
|
+
if (
|
|
212
|
+
budgetHasUnitReserve(dependencies.budget) &&
|
|
213
|
+
!budgetAllows(dependencies.budget, graphCostMs(dependencies))
|
|
214
|
+
) {
|
|
215
|
+
return { kind: 'deferred', category: 'budget_exhausted' };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return abandonClaimedReservation(reservation, dependencies, 'abandoned_missing');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
kind: 'deferred',
|
|
223
|
+
category: isTransientShareContentError(stat.error)
|
|
224
|
+
? 'deferred_stat_transient'
|
|
225
|
+
: 'deferred_stat_error',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (verifyShareContentActiveStat(stat.value, reservation)) {
|
|
230
|
+
if (
|
|
231
|
+
budgetHasUnitReserve(dependencies.budget) &&
|
|
232
|
+
!budgetAllows(dependencies.budget, graphCostMs(dependencies))
|
|
233
|
+
) {
|
|
234
|
+
return { kind: 'deferred', category: 'budget_exhausted' };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return finalizeClaimedReservation(reservation, dependencies);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (stat.value.kind === 'tombstone') {
|
|
241
|
+
if (
|
|
242
|
+
budgetHasUnitReserve(dependencies.budget) &&
|
|
243
|
+
!budgetAllows(dependencies.budget, graphCostMs(dependencies))
|
|
244
|
+
) {
|
|
245
|
+
return { kind: 'deferred', category: 'budget_exhausted' };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return abandonClaimedReservation(reservation, dependencies, 'abandoned_tombstone');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return { kind: 'deferred', category: 'deferred_stat_mismatch' };
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Wraps the remote drive so a throwing storage dependency is reported as a
|
|
256
|
+
* bounded deferred category. A thrown stat must never escape the recovery pass or
|
|
257
|
+
* be mistaken for a proven deletion.
|
|
258
|
+
*/
|
|
259
|
+
const safeDriveClaimedReservation = async (
|
|
260
|
+
reservation: ShareLinkReservationRecord,
|
|
261
|
+
dependencies: RecoveryRunnerDependencies
|
|
262
|
+
): Promise<RecoveryDriveOutcome> => {
|
|
263
|
+
try {
|
|
264
|
+
return await driveClaimedReservation(reservation, dependencies);
|
|
265
|
+
} catch {
|
|
266
|
+
return { kind: 'deferred', category: 'deferred_stat_error' };
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* One bounded, one-shot recovery pass.
|
|
272
|
+
*
|
|
273
|
+
* It performs ONE discovery pass for the explicit namespace and then services
|
|
274
|
+
* each persisted key at most once, so a contended or failed item is never
|
|
275
|
+
* re-selected within the same pass. Every per-item failure is reported as a
|
|
276
|
+
* bounded category; the summary carries no owner ids, share ids, object refs,
|
|
277
|
+
* titles, notes, payloads, keys or URIs. Failed work stays recoverable and can be
|
|
278
|
+
* retried after its lease lapses.
|
|
279
|
+
*/
|
|
280
|
+
export const runShareLinkRecoveryOnce = async (
|
|
281
|
+
dependencies: RecoveryRunnerDependencies
|
|
282
|
+
): Promise<ShareRecoveryRunSummary> => {
|
|
283
|
+
assertRunnerScope(dependencies);
|
|
284
|
+
|
|
285
|
+
const summary: ShareRecoveryRunSummary = {
|
|
286
|
+
discovered: 0,
|
|
287
|
+
claimed: 0,
|
|
288
|
+
finalized: 0,
|
|
289
|
+
abandoned: 0,
|
|
290
|
+
deferred: 0,
|
|
291
|
+
claimLost: 0,
|
|
292
|
+
anomalies: 0,
|
|
293
|
+
categories: {},
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Bound discovery itself: a bounded maintenance pass must not start a read
|
|
297
|
+
// it cannot cover. Ordinary callers omit the reserve and keep the reviewed
|
|
298
|
+
// behaviour.
|
|
299
|
+
if (
|
|
300
|
+
budgetHasUnitReserve(dependencies.budget) &&
|
|
301
|
+
!budgetAllows(dependencies.budget, graphCostMs(dependencies))
|
|
302
|
+
) {
|
|
303
|
+
recordCategory(summary.categories, 'budget_exhausted');
|
|
304
|
+
|
|
305
|
+
return summary;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const discovery = await dependencies.repository.discoverRecoverableReservations({
|
|
309
|
+
namespace: dependencies.namespace,
|
|
310
|
+
limit: dependencies.limit,
|
|
311
|
+
now: dependencies.now?.(),
|
|
312
|
+
transactionTimeoutMs: boundTransactionTimeout(
|
|
313
|
+
dependencies.transactionTimeoutMs,
|
|
314
|
+
dependencies.budget
|
|
315
|
+
),
|
|
316
|
+
noInlineRetry: dependencies.noInlineRetry,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
summary.discovered = discovery.keys.length;
|
|
320
|
+
|
|
321
|
+
for (const key of discovery.keys) {
|
|
322
|
+
// Whole-unit admission before each claim so a discovered item is never
|
|
323
|
+
// claimed only to be left mid-flight; a later run retries.
|
|
324
|
+
if (!budgetAllows(dependencies.budget, recoveryUnitCostMs(dependencies))) break;
|
|
325
|
+
|
|
326
|
+
let claim: Awaited<
|
|
327
|
+
ReturnType<RecoveryRunnerDependencies['repository']['claimRecoverableReservation']>
|
|
328
|
+
>;
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
claim = await dependencies.repository.claimRecoverableReservation({
|
|
332
|
+
...key,
|
|
333
|
+
leaseOwner: dependencies.claimant,
|
|
334
|
+
leaseMs: dependencies.leaseMs,
|
|
335
|
+
now: dependencies.now?.(),
|
|
336
|
+
transactionTimeoutMs: boundTransactionTimeout(
|
|
337
|
+
dependencies.transactionTimeoutMs,
|
|
338
|
+
dependencies.budget
|
|
339
|
+
),
|
|
340
|
+
noInlineRetry: dependencies.noInlineRetry,
|
|
341
|
+
});
|
|
342
|
+
} catch {
|
|
343
|
+
// Could not confirm a claim; a later pass retries after the lease.
|
|
344
|
+
summary.claimLost += 1;
|
|
345
|
+
recordCategory(summary.categories, 'claim_lost');
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (claim.outcome !== 'claimed') {
|
|
350
|
+
if (claim.reason === 'malformed_binding') {
|
|
351
|
+
summary.anomalies += 1;
|
|
352
|
+
recordCategory(summary.categories, 'malformed_binding');
|
|
353
|
+
} else {
|
|
354
|
+
summary.claimLost += 1;
|
|
355
|
+
recordCategory(summary.categories, 'claim_lost');
|
|
356
|
+
}
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
summary.claimed += 1;
|
|
361
|
+
const outcome = await safeDriveClaimedReservation(claim.reservation, dependencies);
|
|
362
|
+
|
|
363
|
+
switch (outcome.kind) {
|
|
364
|
+
case 'finalized':
|
|
365
|
+
summary.finalized += 1;
|
|
366
|
+
recordCategory(summary.categories, 'finalized');
|
|
367
|
+
break;
|
|
368
|
+
case 'abandoned':
|
|
369
|
+
summary.abandoned += 1;
|
|
370
|
+
recordCategory(summary.categories, outcome.category);
|
|
371
|
+
break;
|
|
372
|
+
case 'deferred':
|
|
373
|
+
summary.deferred += 1;
|
|
374
|
+
recordCategory(summary.categories, outcome.category);
|
|
375
|
+
break;
|
|
376
|
+
case 'claim_lost':
|
|
377
|
+
summary.claimLost += 1;
|
|
378
|
+
recordCategory(summary.categories, 'claim_lost');
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return summary;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Scoped exact-key recovery retry.
|
|
388
|
+
*
|
|
389
|
+
* The reservation is loaded and claimed server-side; the caller supplies only the
|
|
390
|
+
* exact key, never an object ref, hash, generation or lease. When the finalize
|
|
391
|
+
* already committed but its response was lost, the committed current state is
|
|
392
|
+
* returned without re-uploading or reconstructing the deleted reservation.
|
|
393
|
+
*/
|
|
394
|
+
export const recoverShareLinkOperation = async (
|
|
395
|
+
dependencies: RecoveryRunnerDependencies,
|
|
396
|
+
key: { namespace: string; ownerProfileId: string; shareId: string; operationId: string }
|
|
397
|
+
): Promise<ScopedShareRecoveryResult> => {
|
|
398
|
+
assertRunnerScope(dependencies, key);
|
|
399
|
+
|
|
400
|
+
const target = await dependencies.repository.readShareLinkRecoveryTarget(key);
|
|
401
|
+
|
|
402
|
+
if (target.state === 'committed') return { status: 'committed', share: target.share };
|
|
403
|
+
if (target.state === 'absent') return { status: 'absent' };
|
|
404
|
+
|
|
405
|
+
const claim = await dependencies.repository.claimRecoverableReservation({
|
|
406
|
+
...key,
|
|
407
|
+
leaseOwner: dependencies.claimant,
|
|
408
|
+
leaseMs: dependencies.leaseMs,
|
|
409
|
+
now: dependencies.now?.(),
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (claim.outcome !== 'claimed') {
|
|
413
|
+
return { status: 'not_claimable', reason: claim.reason };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const outcome = await safeDriveClaimedReservation(claim.reservation, dependencies);
|
|
417
|
+
|
|
418
|
+
switch (outcome.kind) {
|
|
419
|
+
case 'finalized':
|
|
420
|
+
return { status: 'committed', share: outcome.share };
|
|
421
|
+
case 'abandoned':
|
|
422
|
+
return { status: 'abandoned' };
|
|
423
|
+
case 'deferred':
|
|
424
|
+
return { status: 'deferred', category: outcome.category };
|
|
425
|
+
case 'claim_lost':
|
|
426
|
+
return { status: 'not_claimable', reason: outcome.reason };
|
|
427
|
+
}
|
|
428
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
abandonRecoveredReservation,
|
|
3
|
+
abandonReservation,
|
|
4
|
+
claimCleanupJobs,
|
|
5
|
+
claimRecoverableReservation,
|
|
6
|
+
completeCleanupJob,
|
|
7
|
+
discoverRecoverableReservations,
|
|
8
|
+
finalizeReservation,
|
|
9
|
+
getCurrentShareContent,
|
|
10
|
+
getShareLink,
|
|
11
|
+
readShareLinkRecoveryTarget,
|
|
12
|
+
reserveCreate,
|
|
13
|
+
reserveReplacement,
|
|
14
|
+
revokeShareLink,
|
|
15
|
+
} from '@accesslayer/share-link';
|
|
16
|
+
import { resolveCurrentShareLinkPolicy } from '@helpers/share-link-policy/production';
|
|
17
|
+
|
|
18
|
+
import { createShareLinkCoordinator } from './coordinator';
|
|
19
|
+
import type {
|
|
20
|
+
CleanupRunnerDependencies,
|
|
21
|
+
RecoveryRunnerDependencies,
|
|
22
|
+
ShareLinkCoordinator,
|
|
23
|
+
ShareLinkCoordinatorDependencies,
|
|
24
|
+
ShareLinkLifecycleRepository,
|
|
25
|
+
ShareLinkRecoveryRepository,
|
|
26
|
+
} from './types';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Production wiring for the coordinator over the reviewed C3 lifecycle
|
|
30
|
+
* repository. Kept out of the coordinator barrel so unit tests never import
|
|
31
|
+
* `@instance`/the Neo4j graph.
|
|
32
|
+
*
|
|
33
|
+
* The scheduler is deliberately absent: nothing here starts a timer or worker.
|
|
34
|
+
*/
|
|
35
|
+
const runtimeRepository: ShareLinkLifecycleRepository = {
|
|
36
|
+
reserveCreate,
|
|
37
|
+
reserveReplacement,
|
|
38
|
+
finalizeReservation: input =>
|
|
39
|
+
finalizeReservation({ ...input, resolveCurrentPolicy: resolveCurrentShareLinkPolicy }),
|
|
40
|
+
abandonReservation,
|
|
41
|
+
revokeShareLink,
|
|
42
|
+
getShareLink,
|
|
43
|
+
getCurrentShareContent,
|
|
44
|
+
claimCleanupJobs,
|
|
45
|
+
completeCleanupJob,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Production wiring for durable reservation recovery. It is separate from the
|
|
50
|
+
* lifecycle repository so no runner can accidentally accept a serialized caller
|
|
51
|
+
* reservation as authority.
|
|
52
|
+
*/
|
|
53
|
+
const runtimeRecoveryRepository: ShareLinkRecoveryRepository = {
|
|
54
|
+
discoverRecoverableReservations,
|
|
55
|
+
readShareLinkRecoveryTarget,
|
|
56
|
+
claimRecoverableReservation,
|
|
57
|
+
abandonRecoveredReservation,
|
|
58
|
+
finalizeReservation: input =>
|
|
59
|
+
finalizeReservation({ ...input, resolveCurrentPolicy: resolveCurrentShareLinkPolicy }),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const createRuntimeShareLinkCoordinator = (
|
|
63
|
+
options: Omit<ShareLinkCoordinatorDependencies, 'repository'> & {
|
|
64
|
+
repository?: ShareLinkLifecycleRepository;
|
|
65
|
+
}
|
|
66
|
+
): ShareLinkCoordinator =>
|
|
67
|
+
createShareLinkCoordinator({
|
|
68
|
+
...options,
|
|
69
|
+
repository: options.repository ?? runtimeRepository,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const createRuntimeCleanupRunnerDependencies = (
|
|
73
|
+
options: Omit<CleanupRunnerDependencies, 'repository'> & {
|
|
74
|
+
repository?: CleanupRunnerDependencies['repository'];
|
|
75
|
+
}
|
|
76
|
+
): CleanupRunnerDependencies => ({
|
|
77
|
+
...options,
|
|
78
|
+
repository: options.repository ?? runtimeRepository,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const createRuntimeRecoveryRunnerDependencies = (
|
|
82
|
+
options: Omit<RecoveryRunnerDependencies, 'repository'> & {
|
|
83
|
+
repository?: ShareLinkRecoveryRepository;
|
|
84
|
+
}
|
|
85
|
+
): RecoveryRunnerDependencies => ({
|
|
86
|
+
...options,
|
|
87
|
+
repository: options.repository ?? runtimeRecoveryRepository,
|
|
88
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ShareLinkReservationRecord } from '../../accesslayer/share-link';
|
|
2
|
+
import type { ShareContentStatValue } from '../share-content-client/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* True only when an exact-object `stat` proves the immutable reserved object is
|
|
6
|
+
* still active with the full expected tuple, payload hash and byte sizes.
|
|
7
|
+
*
|
|
8
|
+
* Both the create/update coordinator and the durable recovery runner use this
|
|
9
|
+
* predicate so a recovered finalize applies exactly the same acceptance rule: a
|
|
10
|
+
* mismatch is never finalized and never destroys the staged content.
|
|
11
|
+
*/
|
|
12
|
+
export const verifyShareContentActiveStat = (
|
|
13
|
+
stat: ShareContentStatValue,
|
|
14
|
+
reservation: ShareLinkReservationRecord
|
|
15
|
+
): boolean => {
|
|
16
|
+
if (stat.kind !== 'active') return false;
|
|
17
|
+
if (reservation.objectRef === null || reservation.contentVersion === null) return false;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
stat.namespace === reservation.namespace &&
|
|
21
|
+
stat.ownerProfileId === reservation.ownerProfileId &&
|
|
22
|
+
stat.shareId === reservation.shareId &&
|
|
23
|
+
stat.objectId === reservation.objectRef &&
|
|
24
|
+
stat.operationId === reservation.operationId &&
|
|
25
|
+
stat.contentVersion === reservation.contentVersion &&
|
|
26
|
+
stat.payloadHash === reservation.contentHash &&
|
|
27
|
+
(reservation.contentBytes === null || stat.ciphertextBytes === reservation.contentBytes) &&
|
|
28
|
+
(reservation.recoveryBytes === null || stat.recoveryBytes === reservation.recoveryBytes)
|
|
29
|
+
);
|
|
30
|
+
};
|