@learncard/learn-cloud-service 2.5.34 → 2.5.36

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.
Files changed (124) hide show
  1. package/.env.example +24 -12
  2. package/CHANGELOG.md +43 -0
  3. package/bootstrapLambda.test.ts +68 -0
  4. package/config/config.dev.json +8 -0
  5. package/config/config.example.json +8 -0
  6. package/config/config.json +1 -0
  7. package/config/config.production.json +8 -0
  8. package/config/config.scouts.dev.json +3 -0
  9. package/config/config.scouts.production.json +4 -0
  10. package/didWebLambda.test.ts +41 -0
  11. package/didWebLambda.ts +13 -19
  12. package/didWebLambdaApp.ts +23 -0
  13. package/dist/accesslayer/share-content/delete.d.ts +5 -0
  14. package/dist/accesslayer/share-content/delete.d.ts.map +1 -0
  15. package/dist/accesslayer/share-content/index.d.ts +8 -0
  16. package/dist/accesslayer/share-content/index.d.ts.map +1 -0
  17. package/dist/accesslayer/share-content/initialize.d.ts +5 -0
  18. package/dist/accesslayer/share-content/initialize.d.ts.map +1 -0
  19. package/dist/accesslayer/share-content/internal.d.ts +12 -0
  20. package/dist/accesslayer/share-content/internal.d.ts.map +1 -0
  21. package/dist/accesslayer/share-content/put.d.ts +5 -0
  22. package/dist/accesslayer/share-content/put.d.ts.map +1 -0
  23. package/dist/accesslayer/share-content/read.d.ts +7 -0
  24. package/dist/accesslayer/share-content/read.d.ts.map +1 -0
  25. package/dist/accesslayer/share-content/types.d.ts +85 -0
  26. package/dist/accesslayer/share-content/types.d.ts.map +1 -0
  27. package/dist/config/applyDockerStageConfig.d.ts +2 -0
  28. package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
  29. package/dist/config/environment.d.ts +12 -0
  30. package/dist/config/environment.d.ts.map +1 -1
  31. package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
  32. package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
  33. package/dist/config/stageConfig.d.ts +10 -0
  34. package/dist/config/stageConfig.d.ts.map +1 -0
  35. package/dist/config/stageConfig.test.d.ts +2 -0
  36. package/dist/config/stageConfig.test.d.ts.map +1 -0
  37. package/dist/didkit_wasm_bg.wasm +0 -0
  38. package/dist/docker-entry.d.ts +1 -1
  39. package/dist/docker-entry.d.ts.map +1 -1
  40. package/dist/helpers/share-content-auth/adapters.d.ts +12 -0
  41. package/dist/helpers/share-content-auth/adapters.d.ts.map +1 -0
  42. package/dist/helpers/share-content-auth/canonical.d.ts +15 -0
  43. package/dist/helpers/share-content-auth/canonical.d.ts.map +1 -0
  44. package/dist/helpers/share-content-auth/claims.d.ts +19 -0
  45. package/dist/helpers/share-content-auth/claims.d.ts.map +1 -0
  46. package/dist/helpers/share-content-auth/config.d.ts +12 -0
  47. package/dist/helpers/share-content-auth/config.d.ts.map +1 -0
  48. package/dist/helpers/share-content-auth/index.d.ts +8 -0
  49. package/dist/helpers/share-content-auth/index.d.ts.map +1 -0
  50. package/dist/helpers/share-content-auth/token.d.ts +28 -0
  51. package/dist/helpers/share-content-auth/token.d.ts.map +1 -0
  52. package/dist/helpers/share-content-auth/types.d.ts +70 -0
  53. package/dist/helpers/share-content-auth/types.d.ts.map +1 -0
  54. package/dist/helpers/share-content-auth/verifier.d.ts +9 -0
  55. package/dist/helpers/share-content-auth/verifier.d.ts.map +1 -0
  56. package/dist/index.js +2 -2
  57. package/dist/models/ShareContent.d.ts +160 -0
  58. package/dist/models/ShareContent.d.ts.map +1 -0
  59. package/dist/models/index.d.ts +1 -0
  60. package/dist/models/index.d.ts.map +1 -1
  61. package/dist/share-content/config.d.ts +28 -0
  62. package/dist/share-content/config.d.ts.map +1 -0
  63. package/dist/share-content/index.d.ts +6 -0
  64. package/dist/share-content/index.d.ts.map +1 -0
  65. package/dist/share-content/plugin.d.ts +5 -0
  66. package/dist/share-content/plugin.d.ts.map +1 -0
  67. package/dist/share-content/production.d.ts +15 -0
  68. package/dist/share-content/production.d.ts.map +1 -0
  69. package/dist/share-content/runtime.d.ts +23 -0
  70. package/dist/share-content/runtime.d.ts.map +1 -0
  71. package/dist/share-content/types.d.ts +23 -0
  72. package/dist/share-content/types.d.ts.map +1 -0
  73. package/esbuildPlugins.cjs +1 -0
  74. package/lambda.ts +22 -92
  75. package/lambdaApp.ts +163 -0
  76. package/oidcLambda.test.ts +41 -0
  77. package/oidcLambda.ts +13 -19
  78. package/oidcLambdaApp.ts +23 -0
  79. package/package.json +18 -16
  80. package/serverless-local.yml +3 -1
  81. package/serverless.function-env.cjs +31 -0
  82. package/serverless.yml +31 -22
  83. package/src/accesslayer/share-content/delete.ts +124 -0
  84. package/src/accesslayer/share-content/index.ts +70 -0
  85. package/src/accesslayer/share-content/initialize.ts +24 -0
  86. package/src/accesslayer/share-content/internal.ts +89 -0
  87. package/src/accesslayer/share-content/put.ts +93 -0
  88. package/src/accesslayer/share-content/read.ts +124 -0
  89. package/src/accesslayer/share-content/types.ts +155 -0
  90. package/src/config/applyDockerStageConfig.ts +6 -0
  91. package/src/config/environment.ts +33 -0
  92. package/src/config/serverlessFunctionEnv.test.ts +81 -0
  93. package/src/config/stageConfig.test.ts +26 -0
  94. package/src/config/stageConfig.ts +24 -0
  95. package/src/docker-entry.ts +21 -0
  96. package/src/helpers/share-content-auth/adapters.ts +48 -0
  97. package/src/helpers/share-content-auth/canonical.ts +91 -0
  98. package/src/helpers/share-content-auth/claims.ts +164 -0
  99. package/src/helpers/share-content-auth/config.ts +160 -0
  100. package/src/helpers/share-content-auth/index.ts +59 -0
  101. package/src/helpers/share-content-auth/token.ts +148 -0
  102. package/src/helpers/share-content-auth/types.ts +195 -0
  103. package/src/helpers/share-content-auth/verifier.ts +220 -0
  104. package/src/models/ShareContent.ts +198 -0
  105. package/src/models/index.ts +1 -0
  106. package/src/share-content/config.ts +199 -0
  107. package/src/share-content/index.ts +5 -0
  108. package/src/share-content/plugin.ts +317 -0
  109. package/src/share-content/production.ts +107 -0
  110. package/src/share-content/runtime.ts +90 -0
  111. package/src/share-content/types.ts +59 -0
  112. package/test/share-content-auth.spec.ts +1161 -0
  113. package/test/share-content-config.spec.ts +252 -0
  114. package/test/share-content-didkit.spec.ts +411 -0
  115. package/test/share-content-endpoint.spec.ts +825 -0
  116. package/test/share-content-redis-setup.ts +18 -0
  117. package/test/share-content.spec.ts +681 -0
  118. package/vitest.config.ts +4 -0
  119. package/vitest.share-content-config-unit.config.ts +8 -0
  120. package/vitest.share-content-endpoint.config.ts +21 -0
  121. package/vitest.share-content.config.ts +19 -0
  122. package/xApiLambda.test.ts +41 -0
  123. package/xApiLambda.ts +13 -19
  124. package/xApiLambdaApp.ts +23 -0
@@ -0,0 +1,198 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ JWEValidator,
4
+ MAX_SHARE_CIPHERTEXT_BYTES,
5
+ MAX_SHARE_RECOVERY_JWE_BYTES,
6
+ ShareEnvelopeValidator,
7
+ ShareLinkIdValidator,
8
+ ShareOwnerRecoveryValidator,
9
+ decodedBase64UrlByteLength,
10
+ type ShareEnvelope,
11
+ type ShareOwnerRecovery,
12
+ utf8ByteLength,
13
+ } from '@learncard/types';
14
+
15
+ import {
16
+ computeShareContentRequestHash,
17
+ isOpaqueIdentifier,
18
+ isSafeContentVersion,
19
+ } from '@helpers/share-content-auth/canonical';
20
+
21
+ /**
22
+ * LC-2187 dedicated LearnCloud `share_content` collection.
23
+ *
24
+ * This module defines the *storage* grammar for an immutable encrypted share
25
+ * object plus its permanent deletion tombstone. It is deliberately separate
26
+ * from the recipient manifest owners in `@learncard/types`: it never models
27
+ * plaintext, source URIs or keys, only opaque bindings and the already-encrypted
28
+ * envelope/recovery bytes.
29
+ *
30
+ * Security invariants encoded here:
31
+ * - The immutable identity is `(namespace, objectId)`. Every other binding
32
+ * (`ownerProfileId`, `shareId`, `contentVersion`, `operationId`) is part of
33
+ * the exact tuple and must match on every read/mutation.
34
+ * - `contentHash` is *derived* by {@link computeShareContentHash} over the full
35
+ * validated immutable record (all bindings plus the ciphertext envelope and
36
+ * the owner-encrypted recovery). A caller-supplied hash is never accepted, so
37
+ * a forged hash cannot mask divergent bytes.
38
+ * - The recovery projection is deliberately owner-only at the route layer. The
39
+ * stored recovery JWE is strict at the top level: unknown metadata/URL fields
40
+ * are rejected, not persisted.
41
+ * - Bound sizes: decoded ciphertext (tag included) is at most
42
+ * {@link MAX_SHARE_CIPHERTEXT_BYTES}; the serialized recovery JWE is at most
43
+ * {@link MAX_SHARE_RECOVERY_JWE_BYTES}.
44
+ */
45
+
46
+ /** MongoDB collection name for immutable encrypted share content. */
47
+ export const SHARE_CONTENT_COLLECTION = 'share_content';
48
+
49
+ const SHARE_CONTENT_HASH_PATTERN = /^[0-9a-f]{64}$/;
50
+
51
+ /** Opaque, URL-safe binding identifier. Deliberately not a UUID-only check. */
52
+ const opaqueIdentifier = (label: string, maxLength = 128) =>
53
+ z.string().refine(value => isOpaqueIdentifier(value, maxLength), {
54
+ message: `${label} must be an opaque URL-safe identifier of 1..${maxLength} characters`,
55
+ });
56
+
57
+ /** Service/network/tenant namespace. Opaque; not a URL or host. */
58
+ export const ShareContentNamespaceValidator = opaqueIdentifier('namespace');
59
+ export type ShareContentNamespace = z.infer<typeof ShareContentNamespaceValidator>;
60
+
61
+ /** Opaque owner profile id (existing ids are opaque strings, not necessarily UUIDs). */
62
+ export const ShareContentOwnerProfileIdValidator = opaqueIdentifier('ownerProfileId');
63
+ export type ShareContentOwnerProfileId = z.infer<typeof ShareContentOwnerProfileIdValidator>;
64
+
65
+ /** Opaque immutable object id reserved by Brain. Never a URI. */
66
+ export const ShareContentObjectIdValidator = opaqueIdentifier('objectId');
67
+ export type ShareContentObjectId = z.infer<typeof ShareContentObjectIdValidator>;
68
+
69
+ /** Opaque operation/idempotency id that owns the immutable object. */
70
+ export const ShareContentOperationIdValidator = opaqueIdentifier('operationId');
71
+ export type ShareContentOperationId = z.infer<typeof ShareContentOperationIdValidator>;
72
+
73
+ /** Positive safe-integer content version (`1..2^31-1`). */
74
+ export const ShareContentVersionValidator = z
75
+ .number()
76
+ .refine(isSafeContentVersion, { message: 'contentVersion must be a safe integer >= 1' });
77
+ export type ShareContentContentVersion = z.infer<typeof ShareContentVersionValidator>;
78
+
79
+ /** Lowercase hex SHA-256 of the derived immutable record. */
80
+ export const ShareContentHashValidator = z
81
+ .string()
82
+ .regex(SHARE_CONTENT_HASH_PATTERN, { message: 'contentHash must be lowercase hex sha256' });
83
+ export type ShareContentHash = z.infer<typeof ShareContentHashValidator>;
84
+
85
+ /**
86
+ * Strict top-level owner recovery JWE. Wraps the shared
87
+ * {@link ShareOwnerRecoveryValidator} (which enforces the serialized 64 KiB cap)
88
+ * and additionally rejects unknown top-level keys so arbitrary metadata or URLs
89
+ * can never be persisted alongside the opaque recovery.
90
+ */
91
+ export const ShareContentOwnerRecoveryValidator = JWEValidator.strict().refine(
92
+ value => ShareOwnerRecoveryValidator.safeParse(value).success,
93
+ { message: 'must be a valid bounded owner-encrypted recovery JWE' }
94
+ );
95
+ export type ShareContentOwnerRecovery = z.infer<typeof ShareContentOwnerRecoveryValidator>;
96
+
97
+ /**
98
+ * The exact, immutable object binding. The repository applies this to every
99
+ * operation; a mutation of any single field fails closed.
100
+ */
101
+ export const ShareContentObjectBindingValidator = z
102
+ .object({
103
+ namespace: ShareContentNamespaceValidator,
104
+ ownerProfileId: ShareContentOwnerProfileIdValidator,
105
+ shareId: ShareLinkIdValidator,
106
+ contentVersion: ShareContentVersionValidator,
107
+ objectId: ShareContentObjectIdValidator,
108
+ operationId: ShareContentOperationIdValidator,
109
+ })
110
+ .strict();
111
+ export type ShareContentObjectBinding = z.infer<typeof ShareContentObjectBindingValidator>;
112
+
113
+ /**
114
+ * A create-only write. `contentHash` is intentionally absent: the repository
115
+ * derives it, so a caller cannot supply (and therefore cannot forge) it.
116
+ */
117
+ export const ShareContentPutInputValidator = ShareContentObjectBindingValidator.extend({
118
+ envelope: ShareEnvelopeValidator,
119
+ ownerEncryptedRecovery: ShareContentOwnerRecoveryValidator,
120
+ }).strict();
121
+ export type ShareContentPutInput = z.infer<typeof ShareContentPutInputValidator>;
122
+
123
+ /**
124
+ * Derive the content hash over the complete immutable record: all bindings plus
125
+ * the validated ciphertext envelope and the owner-encrypted recovery. The C1
126
+ * canonical serializer makes the result deterministic and rejects unsupported
127
+ * values instead of coercing them.
128
+ */
129
+ export const computeShareContentHash = (input: ShareContentPutInput): string =>
130
+ computeShareContentRequestHash({
131
+ namespace: input.namespace,
132
+ ownerProfileId: input.ownerProfileId,
133
+ shareId: input.shareId,
134
+ contentVersion: input.contentVersion,
135
+ objectId: input.objectId,
136
+ operationId: input.operationId,
137
+ envelope: input.envelope,
138
+ ownerEncryptedRecovery: input.ownerEncryptedRecovery,
139
+ });
140
+
141
+ /** Hash known before Brain allocates object/operation IDs; reservation binding. */
142
+ export const computeSharePayloadHash = (
143
+ input: Pick<ShareContentPutInput, 'envelope' | 'ownerEncryptedRecovery'>
144
+ ): string =>
145
+ computeShareContentRequestHash({
146
+ envelope: input.envelope,
147
+ ownerEncryptedRecovery: input.ownerEncryptedRecovery,
148
+ });
149
+
150
+ /**
151
+ * Measure the encoded payload for reconciliation/stats. Assumes `input` already
152
+ * passed {@link ShareContentPutInputValidator}.
153
+ */
154
+ export const measureShareContentPayload = (
155
+ input: Pick<ShareContentPutInput, 'envelope' | 'ownerEncryptedRecovery'>
156
+ ): { ciphertextBytes: number; recoveryBytes: number } => {
157
+ const ciphertextBytes = decodedBase64UrlByteLength(input.envelope.ct);
158
+
159
+ if (ciphertextBytes === null) {
160
+ throw new Error('validated envelope ciphertext must be canonical base64url');
161
+ }
162
+
163
+ return {
164
+ ciphertextBytes,
165
+ recoveryBytes: utf8ByteLength(JSON.stringify(input.ownerEncryptedRecovery)),
166
+ };
167
+ };
168
+
169
+ /** Immutable active document. */
170
+ export const MongoShareContentActiveValidator = ShareContentObjectBindingValidator.extend({
171
+ kind: z.literal('active'),
172
+ contentHash: ShareContentHashValidator,
173
+ payloadHash: ShareContentHashValidator,
174
+ envelope: ShareEnvelopeValidator,
175
+ ownerEncryptedRecovery: ShareContentOwnerRecoveryValidator,
176
+ ciphertextBytes: z.number().int().min(16).max(MAX_SHARE_CIPHERTEXT_BYTES),
177
+ recoveryBytes: z.number().int().min(1).max(MAX_SHARE_RECOVERY_JWE_BYTES),
178
+ createdAt: z.date(),
179
+ }).strict();
180
+ export type MongoShareContentActive = z.infer<typeof MongoShareContentActiveValidator>;
181
+
182
+ /**
183
+ * Permanent deletion tombstone. Retains the binding tuple and, where known, the
184
+ * derived hash; never retains ciphertext or recovery. A tombstone blocks every
185
+ * later put for the same `(namespace, objectId)` identity.
186
+ */
187
+ export const MongoShareContentTombstoneValidator = ShareContentObjectBindingValidator.extend({
188
+ kind: z.literal('tombstone'),
189
+ contentHash: ShareContentHashValidator.optional(),
190
+ deletedAt: z.date(),
191
+ }).strict();
192
+ export type MongoShareContentTombstone = z.infer<typeof MongoShareContentTombstoneValidator>;
193
+
194
+ /** Stored document union. */
195
+ export type MongoShareContentDocument = MongoShareContentActive | MongoShareContentTombstone;
196
+
197
+ /** Raw ciphertext envelope carried by an active object. */
198
+ export type { ShareEnvelope, ShareOwnerRecovery };
@@ -1,4 +1,5 @@
1
1
  export * from './Credential';
2
2
  export * from './CredentialRecord';
3
3
  export * from './CustomDocument';
4
+ export * from './ShareContent';
4
5
  export * from './User';
@@ -0,0 +1,199 @@
1
+ import {
2
+ isShareContentTrustConfigActive,
3
+ resolveShareContentTrustConfig,
4
+ type ShareContentTrustConfig,
5
+ } from '@helpers/share-content-auth';
6
+
7
+ import { SHARE_CONTENT_MAX_REQUEST_BYTES, type ShareContentPluginOptions } from './types';
8
+
9
+ /**
10
+ * Fail-closed runtime configuration for the LC-2187 service-only routes.
11
+ *
12
+ * There is deliberately no offline or development bypass: when the configuration
13
+ * is absent, disabled, incomplete or internally inconsistent the resolver
14
+ * returns `disabled`/`invalid` and the routes stay unregistered. Namespace
15
+ * authorization is an explicit per-service binding so a valid service token can
16
+ * never name an arbitrary namespace.
17
+ */
18
+
19
+ export type ShareContentNamespacePolicy = {
20
+ /** True only when this exact allowlisted service may use this exact namespace. */
21
+ isAllowed: (serviceDid: string, namespace: string) => boolean;
22
+ /** Configured namespaces for one service (diagnostics/tests only). */
23
+ allowedNamespacesFor: (serviceDid: string) => readonly string[];
24
+ };
25
+
26
+ export type ResolvedShareContentConfig =
27
+ | { status: 'disabled' }
28
+ | { status: 'invalid'; errors: readonly string[] }
29
+ | {
30
+ status: 'enabled';
31
+ trustConfig: ShareContentTrustConfig;
32
+ namespacePolicy: ShareContentNamespacePolicy;
33
+ };
34
+
35
+ const toStringList = (value: unknown): string[] | null => {
36
+ if (typeof value === 'string') {
37
+ return value
38
+ .split(',')
39
+ .map(part => part.trim())
40
+ .filter(part => part.length > 0);
41
+ }
42
+
43
+ if (Array.isArray(value)) {
44
+ if (!value.every(item => typeof item === 'string')) return null;
45
+
46
+ return value.map(item => item.trim()).filter(item => item.length > 0);
47
+ }
48
+
49
+ return null;
50
+ };
51
+
52
+ /**
53
+ * Parse the per-service namespace bindings. Accepts an object map, or a JSON
54
+ * string of one (deployment manifests cannot express nested maps). Every entry
55
+ * must be a non-empty list of non-empty namespace identifiers.
56
+ */
57
+ export const parseShareContentNamespaceBindings = (
58
+ value: unknown
59
+ ): Map<string, readonly string[]> | null => {
60
+ let source: unknown = value;
61
+
62
+ if (typeof value === 'string') {
63
+ try {
64
+ source = JSON.parse(value);
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ if (typeof source !== 'object' || source === null || Array.isArray(source)) return null;
71
+
72
+ const bindings = new Map<string, readonly string[]>();
73
+
74
+ for (const [serviceDid, namespacesValue] of Object.entries(source as Record<string, unknown>)) {
75
+ if (serviceDid.trim().length === 0) return null;
76
+
77
+ const namespaces = toStringList(namespacesValue);
78
+
79
+ if (!namespaces || namespaces.length === 0) return null;
80
+
81
+ bindings.set(serviceDid.trim(), namespaces);
82
+ }
83
+
84
+ return bindings.size > 0 ? bindings : null;
85
+ };
86
+
87
+ const buildNamespacePolicy = (
88
+ bindings: Map<string, readonly string[]>
89
+ ): ShareContentNamespacePolicy => {
90
+ const frozen = new Map<string, readonly string[]>();
91
+
92
+ for (const [serviceDid, namespaces] of bindings) {
93
+ frozen.set(serviceDid, Object.freeze([...namespaces]));
94
+ }
95
+
96
+ return {
97
+ isAllowed: (serviceDid, namespace) => frozen.get(serviceDid)?.includes(namespace) ?? false,
98
+ allowedNamespacesFor: serviceDid => frozen.get(serviceDid) ?? [],
99
+ };
100
+ };
101
+
102
+ /**
103
+ * Resolve the explicit configuration. An empty allowlist, empty verification
104
+ * methods, or missing namespace bindings all fail closed. A
105
+ * partially supplied configuration is reported as `invalid`; the caller
106
+ * must then refuse to register routes (startup fails explicitly rather than
107
+ * silently widening access).
108
+ */
109
+ export const resolveShareContentConfig = (raw: unknown): ResolvedShareContentConfig => {
110
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
111
+ return { status: 'disabled' };
112
+ }
113
+
114
+ const source = raw as Record<string, unknown>;
115
+
116
+ if (
117
+ !['audience', 'serviceDids', 'verificationMethods', 'namespaceBindings'].some(
118
+ key => source[key] !== undefined && source[key] !== null && source[key] !== ''
119
+ )
120
+ )
121
+ return { status: 'disabled' };
122
+
123
+ const errors: string[] = [];
124
+
125
+ const audience = typeof source.audience === 'string' ? source.audience.trim() : '';
126
+ if (audience.length === 0) errors.push('audience is required');
127
+
128
+ const serviceDids = toStringList(source.serviceDids) ?? [];
129
+ if (serviceDids.length === 0) errors.push('serviceDids must list at least one service DID');
130
+
131
+ const verificationMethods = toStringList(source.verificationMethods) ?? [];
132
+ if (verificationMethods.length === 0) {
133
+ errors.push('verificationMethods must list at least one exact verification method');
134
+ }
135
+
136
+ const bindings = parseShareContentNamespaceBindings(source.namespaceBindings);
137
+ if (!bindings) {
138
+ errors.push('namespaceBindings must map every service DID to at least one namespace');
139
+ }
140
+
141
+ if (bindings) {
142
+ for (const serviceDid of serviceDids) {
143
+ if (!bindings.has(serviceDid)) {
144
+ errors.push(`namespaceBindings is missing an entry for ${serviceDid}`);
145
+ }
146
+ }
147
+
148
+ for (const serviceDid of bindings.keys()) {
149
+ if (!serviceDids.includes(serviceDid)) {
150
+ errors.push(`namespaceBindings references a non-allowlisted service ${serviceDid}`);
151
+ }
152
+ }
153
+ }
154
+
155
+ if (errors.length > 0) return { status: 'invalid', errors };
156
+
157
+ // Reuse the C1 resolver for the cryptographic trust config. Passing a value
158
+ // that C1 would silently disable is an explicit invalid configuration here.
159
+ const trustConfig = resolveShareContentTrustConfig({ ...source, enabled: true });
160
+
161
+ if (!isShareContentTrustConfigActive(trustConfig)) {
162
+ return {
163
+ status: 'invalid',
164
+ errors: ['resolved trust configuration is not active'],
165
+ };
166
+ }
167
+
168
+ return {
169
+ status: 'enabled',
170
+ trustConfig,
171
+ namespacePolicy: buildNamespacePolicy(bindings as Map<string, readonly string[]>),
172
+ };
173
+ };
174
+
175
+ /**
176
+ * Build the exact raw object the resolver expects from an already-parsed
177
+ * LearnCloud environment. Kept tiny so entrypoints cannot drift from each other.
178
+ */
179
+ export const getShareContentRawConfig = (environment: {
180
+ SHARE_CONTENT_AUDIENCE?: string;
181
+ SHARE_CONTENT_SERVICE_DIDS?: string;
182
+ SHARE_CONTENT_VERIFICATION_METHODS?: string;
183
+ SHARE_CONTENT_NAMESPACE_BINDINGS?: string;
184
+ }): Record<string, unknown> => ({
185
+ audience: environment.SHARE_CONTENT_AUDIENCE,
186
+ serviceDids: environment.SHARE_CONTENT_SERVICE_DIDS,
187
+ verificationMethods: environment.SHARE_CONTENT_VERIFICATION_METHODS,
188
+ namespaceBindings: environment.SHARE_CONTENT_NAMESPACE_BINDINGS,
189
+ });
190
+
191
+ /** Build the plugin options from a resolved, enabled configuration. */
192
+ export const toShareContentPluginOptions = (
193
+ config: Extract<ResolvedShareContentConfig, { status: 'enabled' }>,
194
+ dependencies: Pick<ShareContentPluginOptions, 'verifier' | 'repository'>
195
+ ): ShareContentPluginOptions => ({
196
+ ...dependencies,
197
+ namespacePolicy: config.namespacePolicy,
198
+ maxRequestBytes: SHARE_CONTENT_MAX_REQUEST_BYTES,
199
+ });
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export * from './config';
3
+ export * from './plugin';
4
+ export * from './runtime';
5
+ export * from './production';