@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,440 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import { computeShareContentRequestBodyHash } from './canonical';
|
|
4
|
+
import {
|
|
5
|
+
parseShareContentContentProjection,
|
|
6
|
+
parseShareContentDeleteValue,
|
|
7
|
+
parseShareContentPutValue,
|
|
8
|
+
parseShareContentRecoveryProjection,
|
|
9
|
+
parseShareContentResponseEnvelope,
|
|
10
|
+
parseShareContentStatValue,
|
|
11
|
+
} from './responses';
|
|
12
|
+
import {
|
|
13
|
+
MAX_SHARE_CONTENT_RETRY_BACKOFF_MS,
|
|
14
|
+
MAX_SHARE_CONTENT_TOKEN_TTL_SECONDS,
|
|
15
|
+
SHARE_CONTENT_AUTH_PURPOSE,
|
|
16
|
+
isTransientShareContentError,
|
|
17
|
+
type ShareContentAuthorizationClaims,
|
|
18
|
+
type ShareContentClient,
|
|
19
|
+
type ShareContentClientConfig,
|
|
20
|
+
type ShareContentClientErrorCode,
|
|
21
|
+
type ShareContentClientResult,
|
|
22
|
+
type ShareContentContentProjection,
|
|
23
|
+
type ShareContentDeleteValue,
|
|
24
|
+
type ShareContentEnvelope,
|
|
25
|
+
type ShareContentOperation,
|
|
26
|
+
type ShareContentPutRequest,
|
|
27
|
+
type ShareContentPutValue,
|
|
28
|
+
type ShareContentRecoveryProjection,
|
|
29
|
+
type ShareContentStatValue,
|
|
30
|
+
type ShareContentTuple,
|
|
31
|
+
} from './types';
|
|
32
|
+
|
|
33
|
+
const OPAQUE_IDENTIFIER_RE = /^[A-Za-z0-9._~-]+$/;
|
|
34
|
+
|
|
35
|
+
const isOpaqueIdentifier = (value: unknown): value is string =>
|
|
36
|
+
typeof value === 'string' &&
|
|
37
|
+
value.length >= 1 &&
|
|
38
|
+
value.length <= 128 &&
|
|
39
|
+
OPAQUE_IDENTIFIER_RE.test(value);
|
|
40
|
+
|
|
41
|
+
const isSafeVersion = (value: unknown): value is number =>
|
|
42
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value >= 1 && value <= 2 ** 31 - 1;
|
|
43
|
+
|
|
44
|
+
const isValidTuple = (tuple: ShareContentTuple): boolean =>
|
|
45
|
+
isOpaqueIdentifier(tuple.namespace) &&
|
|
46
|
+
isOpaqueIdentifier(tuple.ownerProfileId) &&
|
|
47
|
+
isOpaqueIdentifier(tuple.shareId) &&
|
|
48
|
+
isSafeVersion(tuple.contentVersion) &&
|
|
49
|
+
isOpaqueIdentifier(tuple.objectId) &&
|
|
50
|
+
isOpaqueIdentifier(tuple.operationId);
|
|
51
|
+
|
|
52
|
+
const isValidEnvelope = (envelope: unknown): envelope is ShareContentEnvelope => {
|
|
53
|
+
if (typeof envelope !== 'object' || envelope === null || Array.isArray(envelope)) return false;
|
|
54
|
+
|
|
55
|
+
const candidate = envelope as Record<string, unknown>;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
candidate.v === 1 &&
|
|
59
|
+
candidate.alg === 'A256GCM' &&
|
|
60
|
+
typeof candidate.iv === 'string' &&
|
|
61
|
+
candidate.iv.length > 0 &&
|
|
62
|
+
typeof candidate.ct === 'string' &&
|
|
63
|
+
candidate.ct.length > 0
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Fresh CSPRNG nonce for every transport attempt (never reused across retries). */
|
|
68
|
+
const generateJti = (): string => randomBytes(24).toString('base64url');
|
|
69
|
+
|
|
70
|
+
const buildRequestBody = (
|
|
71
|
+
request: ShareContentPutRequest | ShareContentTuple
|
|
72
|
+
): Record<string, unknown> => {
|
|
73
|
+
const body: Record<string, unknown> = {
|
|
74
|
+
namespace: request.namespace,
|
|
75
|
+
ownerProfileId: request.ownerProfileId,
|
|
76
|
+
shareId: request.shareId,
|
|
77
|
+
contentVersion: request.contentVersion,
|
|
78
|
+
objectId: request.objectId,
|
|
79
|
+
operationId: request.operationId,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if ('envelope' in request && 'ownerEncryptedRecovery' in request) {
|
|
83
|
+
body.envelope = request.envelope;
|
|
84
|
+
body.ownerEncryptedRecovery = request.ownerEncryptedRecovery;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return body;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const buildUrl = (config: ShareContentClientConfig, op: ShareContentOperation): string =>
|
|
91
|
+
`${config.origin}/internal/share-content/${op}`;
|
|
92
|
+
|
|
93
|
+
const mapStatusToError = (status: number): ShareContentClientErrorCode => {
|
|
94
|
+
switch (status) {
|
|
95
|
+
case 400:
|
|
96
|
+
return 'INVALID_INPUT';
|
|
97
|
+
case 401:
|
|
98
|
+
return 'UNAUTHORIZED';
|
|
99
|
+
case 404:
|
|
100
|
+
return 'NOT_FOUND';
|
|
101
|
+
case 409:
|
|
102
|
+
return 'CONFLICT';
|
|
103
|
+
case 413:
|
|
104
|
+
return 'PAYLOAD_TOO_LARGE';
|
|
105
|
+
case 503:
|
|
106
|
+
return 'UNAVAILABLE';
|
|
107
|
+
default:
|
|
108
|
+
return 'UNEXPECTED_STATUS';
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const responseTupleMatches = (
|
|
113
|
+
value: {
|
|
114
|
+
namespace: string;
|
|
115
|
+
ownerProfileId: string;
|
|
116
|
+
shareId: string;
|
|
117
|
+
contentVersion: number;
|
|
118
|
+
objectId: string;
|
|
119
|
+
operationId: string;
|
|
120
|
+
},
|
|
121
|
+
tuple: ShareContentTuple
|
|
122
|
+
): boolean =>
|
|
123
|
+
value.namespace === tuple.namespace &&
|
|
124
|
+
value.ownerProfileId === tuple.ownerProfileId &&
|
|
125
|
+
value.shareId === tuple.shareId &&
|
|
126
|
+
value.contentVersion === tuple.contentVersion &&
|
|
127
|
+
value.objectId === tuple.objectId &&
|
|
128
|
+
value.operationId === tuple.operationId;
|
|
129
|
+
|
|
130
|
+
/** Decode the JOSE header and require it binds exactly the configured signer. */
|
|
131
|
+
const validateSignedTokenHeader = (token: string, signerDid: string): boolean => {
|
|
132
|
+
if (typeof token !== 'string') return false;
|
|
133
|
+
|
|
134
|
+
const parts = token.split('.');
|
|
135
|
+
if (parts.length !== 3) return false;
|
|
136
|
+
|
|
137
|
+
const [headerPart, payloadPart, signaturePart] = parts;
|
|
138
|
+
if (!headerPart || !payloadPart || !signaturePart) return false;
|
|
139
|
+
|
|
140
|
+
let header: unknown;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
header = JSON.parse(Buffer.from(headerPart, 'base64url').toString('utf8'));
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (typeof header !== 'object' || header === null) return false;
|
|
149
|
+
|
|
150
|
+
const candidate = header as Record<string, unknown>;
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
candidate.alg === 'EdDSA' &&
|
|
154
|
+
typeof candidate.kid === 'string' &&
|
|
155
|
+
candidate.kid.startsWith(`${signerDid}#`)
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const createShareContentClient = (config: ShareContentClientConfig): ShareContentClient => {
|
|
160
|
+
const readBoundedBody = async (
|
|
161
|
+
response: Response,
|
|
162
|
+
maxBytes: number
|
|
163
|
+
): Promise<{ ok: true; text: string } | { ok: false; error: ShareContentClientErrorCode }> => {
|
|
164
|
+
const body = response.body;
|
|
165
|
+
|
|
166
|
+
if (!body || typeof body.getReader !== 'function') {
|
|
167
|
+
try {
|
|
168
|
+
const text = await response.text();
|
|
169
|
+
if (Buffer.byteLength(text, 'utf8') > maxBytes) {
|
|
170
|
+
return { ok: false, error: 'RESPONSE_TOO_LARGE' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { ok: true, text };
|
|
174
|
+
} catch {
|
|
175
|
+
return { ok: false, error: 'NETWORK_ERROR' };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const reader = body.getReader();
|
|
180
|
+
const chunks: Uint8Array[] = [];
|
|
181
|
+
let total = 0;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
for (;;) {
|
|
185
|
+
const { done, value } = await reader.read();
|
|
186
|
+
if (done) break;
|
|
187
|
+
if (!value) continue;
|
|
188
|
+
|
|
189
|
+
total += value.byteLength;
|
|
190
|
+
|
|
191
|
+
if (total > maxBytes) {
|
|
192
|
+
await reader.cancel().catch(() => undefined);
|
|
193
|
+
|
|
194
|
+
return { ok: false, error: 'RESPONSE_TOO_LARGE' };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
chunks.push(value);
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
return { ok: false, error: 'NETWORK_ERROR' };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
ok: true,
|
|
205
|
+
text: Buffer.concat(chunks.map(chunk => Buffer.from(chunk))).toString('utf8'),
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const attemptOnce = async <T>(
|
|
210
|
+
op: ShareContentOperation,
|
|
211
|
+
bodyText: string,
|
|
212
|
+
requestHash: string,
|
|
213
|
+
expectedTuple: ShareContentTuple,
|
|
214
|
+
parseValue: (value: unknown) => T | null,
|
|
215
|
+
selectTuple: (value: T) => ShareContentTuple
|
|
216
|
+
): Promise<ShareContentClientResult<T>> => {
|
|
217
|
+
const nowSeconds = Math.floor(config.now() / 1000);
|
|
218
|
+
const claims: ShareContentAuthorizationClaims = {
|
|
219
|
+
iss: config.signerDid,
|
|
220
|
+
aud: config.audience,
|
|
221
|
+
purpose: SHARE_CONTENT_AUTH_PURPOSE,
|
|
222
|
+
namespace: expectedTuple.namespace,
|
|
223
|
+
op,
|
|
224
|
+
shareId: expectedTuple.shareId,
|
|
225
|
+
contentVersion: expectedTuple.contentVersion,
|
|
226
|
+
ownerProfileId: expectedTuple.ownerProfileId,
|
|
227
|
+
objectId: expectedTuple.objectId,
|
|
228
|
+
operationId: expectedTuple.operationId,
|
|
229
|
+
requestHash,
|
|
230
|
+
iat: nowSeconds,
|
|
231
|
+
exp: nowSeconds + MAX_SHARE_CONTENT_TOKEN_TTL_SECONDS,
|
|
232
|
+
jti: generateJti(),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
let token: string;
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
token = await config.signer(claims);
|
|
239
|
+
} catch {
|
|
240
|
+
return { ok: false, error: 'SIGNING_FAILED' };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!validateSignedTokenHeader(token, config.signerDid)) {
|
|
244
|
+
return { ok: false, error: 'SIGNING_FAILED' };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const controller = new AbortController();
|
|
248
|
+
const timer = setTimeout(() => controller.abort(), config.requestTimeoutMs);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
let response: Response;
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
response = await config.fetchImpl(buildUrl(config, op), {
|
|
255
|
+
method: 'POST',
|
|
256
|
+
headers: {
|
|
257
|
+
authorization: `Bearer ${token}`,
|
|
258
|
+
'content-type': 'application/json',
|
|
259
|
+
accept: 'application/json',
|
|
260
|
+
'cache-control': 'no-store',
|
|
261
|
+
},
|
|
262
|
+
body: bodyText,
|
|
263
|
+
redirect: 'manual',
|
|
264
|
+
signal: controller.signal,
|
|
265
|
+
});
|
|
266
|
+
} catch {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: controller.signal.aborted ? 'TIMEOUT' : 'NETWORK_ERROR',
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (response.status >= 300 && response.status < 400) {
|
|
274
|
+
return { ok: false, error: 'REDIRECT_REJECTED' };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (response.status !== 200) {
|
|
278
|
+
// Drain a bounded amount so the socket can be reused; the body is
|
|
279
|
+
// discarded and never surfaces in the returned error.
|
|
280
|
+
await readBoundedBody(response, config.maxResponseBytes).catch(() => undefined);
|
|
281
|
+
|
|
282
|
+
return { ok: false, error: mapStatusToError(response.status) };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const read = await readBoundedBody(response, config.maxResponseBytes);
|
|
286
|
+
|
|
287
|
+
if (!read.ok) {
|
|
288
|
+
if (read.error === 'NETWORK_ERROR' && controller.signal.aborted) {
|
|
289
|
+
return { ok: false, error: 'TIMEOUT' };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return { ok: false, error: read.error };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let parsed: unknown;
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
parsed = JSON.parse(read.text);
|
|
299
|
+
} catch {
|
|
300
|
+
return { ok: false, error: 'MALFORMED_RESPONSE' };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const envelope = parseShareContentResponseEnvelope(parsed);
|
|
304
|
+
|
|
305
|
+
if (!envelope || !envelope.ok) return { ok: false, error: 'MALFORMED_RESPONSE' };
|
|
306
|
+
|
|
307
|
+
const value = parseValue(envelope.value);
|
|
308
|
+
|
|
309
|
+
if (!value) return { ok: false, error: 'MALFORMED_RESPONSE' };
|
|
310
|
+
|
|
311
|
+
if (!responseTupleMatches(selectTuple(value), expectedTuple)) {
|
|
312
|
+
return { ok: false, error: 'RESPONSE_TUPLE_MISMATCH' };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { ok: true, value };
|
|
316
|
+
} finally {
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const perform = async <T>(
|
|
322
|
+
op: ShareContentOperation,
|
|
323
|
+
body: Record<string, unknown>,
|
|
324
|
+
expectedTuple: ShareContentTuple,
|
|
325
|
+
parseValue: (value: unknown) => T | null,
|
|
326
|
+
selectTuple: (value: T) => ShareContentTuple
|
|
327
|
+
): Promise<ShareContentClientResult<T>> => {
|
|
328
|
+
if (!config.enabled) return { ok: false, error: 'DISABLED' };
|
|
329
|
+
if (!isValidTuple(expectedTuple)) return { ok: false, error: 'INVALID_INPUT' };
|
|
330
|
+
|
|
331
|
+
let bodyText: string;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
bodyText = JSON.stringify(body);
|
|
335
|
+
} catch {
|
|
336
|
+
return { ok: false, error: 'INVALID_INPUT' };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (typeof bodyText !== 'string') return { ok: false, error: 'INVALID_INPUT' };
|
|
340
|
+
if (Buffer.byteLength(bodyText, 'utf8') > config.maxRequestBytes) {
|
|
341
|
+
return { ok: false, error: 'PAYLOAD_TOO_LARGE' };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let requestHash: string;
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
requestHash = computeShareContentRequestBodyHash(body, {
|
|
348
|
+
maxBytes: config.maxRequestBytes,
|
|
349
|
+
});
|
|
350
|
+
} catch {
|
|
351
|
+
return { ok: false, error: 'INVALID_INPUT' };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let lastError: ShareContentClientErrorCode = 'NETWORK_ERROR';
|
|
355
|
+
|
|
356
|
+
for (let attempt = 1; attempt <= config.maxAttempts; attempt += 1) {
|
|
357
|
+
const result = await attemptOnce(
|
|
358
|
+
op,
|
|
359
|
+
bodyText,
|
|
360
|
+
requestHash,
|
|
361
|
+
expectedTuple,
|
|
362
|
+
parseValue,
|
|
363
|
+
selectTuple
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
if (result.ok) return result;
|
|
367
|
+
|
|
368
|
+
lastError = result.error;
|
|
369
|
+
|
|
370
|
+
if (!isTransientShareContentError(result.error) || attempt === config.maxAttempts) {
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const backoff = Math.min(
|
|
375
|
+
config.retryBackoffMs * 2 ** (attempt - 1),
|
|
376
|
+
MAX_SHARE_CONTENT_RETRY_BACKOFF_MS
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
if (backoff > 0) await config.sleep(backoff);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return { ok: false, error: lastError };
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
config,
|
|
387
|
+
put: async (request: ShareContentPutRequest) => {
|
|
388
|
+
if (!isValidEnvelope(request.envelope)) return { ok: false, error: 'INVALID_INPUT' };
|
|
389
|
+
|
|
390
|
+
return perform(
|
|
391
|
+
'put',
|
|
392
|
+
buildRequestBody(request),
|
|
393
|
+
request,
|
|
394
|
+
parseShareContentPutValue,
|
|
395
|
+
value => value.record
|
|
396
|
+
);
|
|
397
|
+
},
|
|
398
|
+
get: async (request: ShareContentTuple) =>
|
|
399
|
+
perform(
|
|
400
|
+
'get',
|
|
401
|
+
buildRequestBody(request),
|
|
402
|
+
request,
|
|
403
|
+
parseShareContentContentProjection,
|
|
404
|
+
value => value
|
|
405
|
+
),
|
|
406
|
+
stat: async (request: ShareContentTuple) =>
|
|
407
|
+
perform(
|
|
408
|
+
'stat',
|
|
409
|
+
buildRequestBody(request),
|
|
410
|
+
request,
|
|
411
|
+
parseShareContentStatValue,
|
|
412
|
+
value => value
|
|
413
|
+
),
|
|
414
|
+
delete: async (request: ShareContentTuple) =>
|
|
415
|
+
perform(
|
|
416
|
+
'delete',
|
|
417
|
+
buildRequestBody(request),
|
|
418
|
+
request,
|
|
419
|
+
parseShareContentDeleteValue,
|
|
420
|
+
value => value
|
|
421
|
+
),
|
|
422
|
+
readRecovery: async (request: ShareContentTuple) =>
|
|
423
|
+
perform(
|
|
424
|
+
'readRecovery',
|
|
425
|
+
buildRequestBody(request),
|
|
426
|
+
request,
|
|
427
|
+
parseShareContentRecoveryProjection,
|
|
428
|
+
value => value
|
|
429
|
+
),
|
|
430
|
+
};
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
export type { ShareContentClient, ShareContentClientConfig };
|
|
434
|
+
export type {
|
|
435
|
+
ShareContentContentProjection,
|
|
436
|
+
ShareContentDeleteValue,
|
|
437
|
+
ShareContentPutValue,
|
|
438
|
+
ShareContentRecoveryProjection,
|
|
439
|
+
ShareContentStatValue,
|
|
440
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_MAX_SHARE_CONTENT_REQUEST_BYTES,
|
|
3
|
+
DEFAULT_MAX_SHARE_CONTENT_RESPONSE_BYTES,
|
|
4
|
+
DEFAULT_SHARE_CONTENT_MAX_ATTEMPTS,
|
|
5
|
+
DEFAULT_SHARE_CONTENT_REQUEST_TIMEOUT_MS,
|
|
6
|
+
DEFAULT_SHARE_CONTENT_RETRY_BACKOFF_MS,
|
|
7
|
+
MAX_SHARE_CONTENT_RETRY_BACKOFF_MS,
|
|
8
|
+
ShareContentClientConfigurationError,
|
|
9
|
+
type ShareContentClientConfig,
|
|
10
|
+
type ShareContentTokenSigner,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Explicit, fail-closed client configuration. Nothing here reads environment
|
|
15
|
+
* variables or secrets: the wiring batch constructs it deliberately.
|
|
16
|
+
*
|
|
17
|
+
* A missing/invalid origin, namespace, audience, signer DID or signer function
|
|
18
|
+
* collapses to the disabled configuration, so a partially configured deployment
|
|
19
|
+
* never performs a signed request. There is deliberately no offline or unsigned
|
|
20
|
+
* bypass.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const OPAQUE_IDENTIFIER_RE = /^[A-Za-z0-9._~-]+$/;
|
|
24
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
25
|
+
|
|
26
|
+
const disabledSigner: ShareContentTokenSigner = async () => {
|
|
27
|
+
throw new ShareContentClientConfigurationError('share-content client is disabled');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const DISABLED_SHARE_CONTENT_CLIENT_CONFIG: ShareContentClientConfig = Object.freeze({
|
|
31
|
+
enabled: false,
|
|
32
|
+
origin: '',
|
|
33
|
+
namespace: '',
|
|
34
|
+
audience: '',
|
|
35
|
+
signerDid: '',
|
|
36
|
+
allowInsecureLoopback: false,
|
|
37
|
+
signer: disabledSigner,
|
|
38
|
+
requestTimeoutMs: DEFAULT_SHARE_CONTENT_REQUEST_TIMEOUT_MS,
|
|
39
|
+
maxAttempts: 1,
|
|
40
|
+
retryBackoffMs: 0,
|
|
41
|
+
maxRequestBytes: DEFAULT_MAX_SHARE_CONTENT_REQUEST_BYTES,
|
|
42
|
+
maxResponseBytes: DEFAULT_MAX_SHARE_CONTENT_RESPONSE_BYTES,
|
|
43
|
+
now: () => Date.now(),
|
|
44
|
+
sleep: (ms: number) => new Promise(resolve => setTimeout(resolve, ms)),
|
|
45
|
+
fetchImpl: ((...args: Parameters<typeof fetch>) => fetch(...args)) as typeof fetch,
|
|
46
|
+
}) as ShareContentClientConfig;
|
|
47
|
+
|
|
48
|
+
const isOpaqueIdentifier = (value: unknown, maxLength = 128): value is string =>
|
|
49
|
+
typeof value === 'string' &&
|
|
50
|
+
value.length >= 1 &&
|
|
51
|
+
value.length <= maxLength &&
|
|
52
|
+
OPAQUE_IDENTIFIER_RE.test(value);
|
|
53
|
+
|
|
54
|
+
const isBoundedInteger = (value: unknown, minimum: number, maximum: number): value is number =>
|
|
55
|
+
typeof value === 'number' &&
|
|
56
|
+
Number.isSafeInteger(value) &&
|
|
57
|
+
value >= minimum &&
|
|
58
|
+
value <= maximum;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Validate a configured origin. Only an `https:` origin is trusted; a loopback
|
|
62
|
+
* `http:` origin is accepted only when `allowInsecureLoopback` is explicitly set
|
|
63
|
+
* for local tests/development, and the cryptographic auth path is unchanged.
|
|
64
|
+
*/
|
|
65
|
+
export const validateShareContentOrigin = (
|
|
66
|
+
origin: unknown,
|
|
67
|
+
allowInsecureLoopback: boolean
|
|
68
|
+
): { ok: true; origin: string } | { ok: false; reason: string } => {
|
|
69
|
+
if (typeof origin !== 'string' || origin.trim().length === 0) {
|
|
70
|
+
return { ok: false, reason: 'origin must be a non-empty string' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let url: URL;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
url = new URL(origin);
|
|
77
|
+
} catch {
|
|
78
|
+
return { ok: false, reason: 'origin must be a valid absolute URL' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (url.username || url.password) {
|
|
82
|
+
return { ok: false, reason: 'origin must not contain userinfo' };
|
|
83
|
+
}
|
|
84
|
+
if (url.search || url.hash) {
|
|
85
|
+
return { ok: false, reason: 'origin must not contain a query or fragment' };
|
|
86
|
+
}
|
|
87
|
+
if (url.pathname !== '/' && url.pathname !== '') {
|
|
88
|
+
return { ok: false, reason: 'origin must not contain a path' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (url.protocol === 'https:') {
|
|
92
|
+
return { ok: true, origin: `${url.protocol}//${url.host}` };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (url.protocol === 'http:') {
|
|
96
|
+
if (!allowInsecureLoopback) {
|
|
97
|
+
return { ok: false, reason: 'insecure http origin requires allowInsecureLoopback' };
|
|
98
|
+
}
|
|
99
|
+
if (!LOOPBACK_HOSTS.has(url.hostname)) {
|
|
100
|
+
return { ok: false, reason: 'insecure http origin must be loopback' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { ok: true, origin: `${url.protocol}//${url.host}` };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { ok: false, reason: 'origin must use https (or loopback http for local tests)' };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Build a validated, frozen client configuration, or the disabled config. */
|
|
110
|
+
export const resolveShareContentClientConfig = (raw: unknown): ShareContentClientConfig => {
|
|
111
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
112
|
+
return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const source = raw as Record<string, unknown>;
|
|
116
|
+
|
|
117
|
+
if (source.enabled !== true) return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
|
|
118
|
+
|
|
119
|
+
const allowInsecureLoopback = source.allowInsecureLoopback === true;
|
|
120
|
+
const originResult = validateShareContentOrigin(source.origin, allowInsecureLoopback);
|
|
121
|
+
|
|
122
|
+
if (!originResult.ok) return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
|
|
123
|
+
|
|
124
|
+
if (!isOpaqueIdentifier(source.namespace)) return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
|
|
125
|
+
if (typeof source.audience !== 'string' || source.audience.trim().length === 0) {
|
|
126
|
+
return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
|
|
127
|
+
}
|
|
128
|
+
if (typeof source.signerDid !== 'string' || !source.signerDid.startsWith('did:')) {
|
|
129
|
+
return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
|
|
130
|
+
}
|
|
131
|
+
if (typeof source.signer !== 'function') return DISABLED_SHARE_CONTENT_CLIENT_CONFIG;
|
|
132
|
+
|
|
133
|
+
const signer = source.signer as ShareContentTokenSigner;
|
|
134
|
+
const requestTimeoutMs = isBoundedInteger(source.requestTimeoutMs, 1, 120_000)
|
|
135
|
+
? source.requestTimeoutMs
|
|
136
|
+
: DEFAULT_SHARE_CONTENT_REQUEST_TIMEOUT_MS;
|
|
137
|
+
const maxAttempts = isBoundedInteger(source.maxAttempts, 1, 10)
|
|
138
|
+
? source.maxAttempts
|
|
139
|
+
: DEFAULT_SHARE_CONTENT_MAX_ATTEMPTS;
|
|
140
|
+
const retryBackoffMs = isBoundedInteger(
|
|
141
|
+
source.retryBackoffMs,
|
|
142
|
+
0,
|
|
143
|
+
MAX_SHARE_CONTENT_RETRY_BACKOFF_MS
|
|
144
|
+
)
|
|
145
|
+
? source.retryBackoffMs
|
|
146
|
+
: DEFAULT_SHARE_CONTENT_RETRY_BACKOFF_MS;
|
|
147
|
+
const maxRequestBytes = isBoundedInteger(
|
|
148
|
+
source.maxRequestBytes,
|
|
149
|
+
1024,
|
|
150
|
+
DEFAULT_MAX_SHARE_CONTENT_REQUEST_BYTES
|
|
151
|
+
)
|
|
152
|
+
? source.maxRequestBytes
|
|
153
|
+
: DEFAULT_MAX_SHARE_CONTENT_REQUEST_BYTES;
|
|
154
|
+
const maxResponseBytes = isBoundedInteger(
|
|
155
|
+
source.maxResponseBytes,
|
|
156
|
+
1024,
|
|
157
|
+
DEFAULT_MAX_SHARE_CONTENT_RESPONSE_BYTES
|
|
158
|
+
)
|
|
159
|
+
? source.maxResponseBytes
|
|
160
|
+
: DEFAULT_MAX_SHARE_CONTENT_RESPONSE_BYTES;
|
|
161
|
+
|
|
162
|
+
const now = typeof source.now === 'function' ? (source.now as () => number) : () => Date.now();
|
|
163
|
+
const sleep =
|
|
164
|
+
typeof source.sleep === 'function'
|
|
165
|
+
? (source.sleep as (ms: number) => Promise<void>)
|
|
166
|
+
: (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
167
|
+
const fetchImpl =
|
|
168
|
+
typeof source.fetchImpl === 'function'
|
|
169
|
+
? (source.fetchImpl as typeof fetch)
|
|
170
|
+
: (((...args: Parameters<typeof fetch>) => fetch(...args)) as typeof fetch);
|
|
171
|
+
|
|
172
|
+
return Object.freeze({
|
|
173
|
+
enabled: true,
|
|
174
|
+
origin: originResult.origin,
|
|
175
|
+
namespace: source.namespace,
|
|
176
|
+
audience: source.audience,
|
|
177
|
+
signerDid: source.signerDid,
|
|
178
|
+
allowInsecureLoopback,
|
|
179
|
+
signer,
|
|
180
|
+
requestTimeoutMs,
|
|
181
|
+
maxAttempts,
|
|
182
|
+
retryBackoffMs,
|
|
183
|
+
maxRequestBytes,
|
|
184
|
+
maxResponseBytes,
|
|
185
|
+
now,
|
|
186
|
+
sleep,
|
|
187
|
+
fetchImpl,
|
|
188
|
+
}) as ShareContentClientConfig;
|
|
189
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export {
|
|
3
|
+
canonicalizeShareContentRequestBody,
|
|
4
|
+
computeShareContentRequestBodyHash,
|
|
5
|
+
isSha256Hex,
|
|
6
|
+
ShareContentCanonicalizationError,
|
|
7
|
+
} from './canonical';
|
|
8
|
+
export {
|
|
9
|
+
DISABLED_SHARE_CONTENT_CLIENT_CONFIG,
|
|
10
|
+
resolveShareContentClientConfig,
|
|
11
|
+
validateShareContentOrigin,
|
|
12
|
+
} from './config';
|
|
13
|
+
export { createShareContentClient } from './client';
|
|
14
|
+
export {
|
|
15
|
+
parseShareContentActiveSummary,
|
|
16
|
+
parseShareContentContentProjection,
|
|
17
|
+
parseShareContentDeleteValue,
|
|
18
|
+
parseShareContentPutValue,
|
|
19
|
+
parseShareContentRecoveryProjection,
|
|
20
|
+
parseShareContentResponseEnvelope,
|
|
21
|
+
parseShareContentStatValue,
|
|
22
|
+
parseShareContentTombstoneSummary,
|
|
23
|
+
} from './responses';
|
|
24
|
+
|
|
25
|
+
// The runtime did:web signing adapter deliberately lives outside this barrel:
|
|
26
|
+
// import `@helpers/share-content-client/adapters` from the wiring layer so unit
|
|
27
|
+
// tests of the transport never load the LearnCard/environment graph.
|