@bleedingdev/modern-js-app-tools-extensions 3.8.3-ultramodern.2

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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/cjs/cloudflare/artifacts.js +127 -0
  4. package/dist/cjs/cloudflare/constants.js +141 -0
  5. package/dist/cjs/cloudflare/delivery-unit.js +161 -0
  6. package/dist/cjs/cloudflare/i18n-worker.js +135 -0
  7. package/dist/cjs/cloudflare/index.js +156 -0
  8. package/dist/cjs/cloudflare/security-policies.js +205 -0
  9. package/dist/cjs/cloudflare/types.js +18 -0
  10. package/dist/cjs/cloudflare/utils.js +66 -0
  11. package/dist/cjs/cloudflare/worker-manifest.js +239 -0
  12. package/dist/cjs/cloudflare/wrangler-config.js +205 -0
  13. package/dist/cjs/cloudflare-builder.js +394 -0
  14. package/dist/cjs/cloudflare-output-contract.js +108 -0
  15. package/dist/cjs/cloudflare-output-plan.js +93 -0
  16. package/dist/cjs/cloudflare-output-verifier/identity.js +92 -0
  17. package/dist/cjs/cloudflare-output-verifier/index.js +188 -0
  18. package/dist/cjs/cloudflare-output-verifier/issues.js +54 -0
  19. package/dist/cjs/cloudflare-output-verifier/manifest.js +89 -0
  20. package/dist/cjs/cloudflare-output-verifier/mutation-policy.js +151 -0
  21. package/dist/cjs/cloudflare-output-verifier/worker-bundles.js +378 -0
  22. package/dist/cjs/config.js +18 -0
  23. package/dist/cjs/css-extract-runtime-plugin.js +74 -0
  24. package/dist/cjs/index.js +125 -0
  25. package/dist/cjs/read-template.js +56 -0
  26. package/dist/cjs/release-envelope/canonical.js +68 -0
  27. package/dist/cjs/release-envelope/framework-output.js +649 -0
  28. package/dist/cjs/release-envelope/index.js +399 -0
  29. package/dist/cjs/release-envelope/plugin.js +102 -0
  30. package/dist/cjs/release-envelope/types.js +51 -0
  31. package/dist/cjs/release-identity.js +110 -0
  32. package/dist/cjs/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  33. package/dist/cjs/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  34. package/dist/cjs/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  35. package/dist/cjs/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  36. package/dist/cjs/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  37. package/dist/cjs/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  38. package/dist/cjs/templates/cloudflare-worker-fs-promises.mjs +11 -0
  39. package/dist/cjs/templates/cloudflare-worker-loadable-server.mjs +281 -0
  40. package/dist/cjs/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  41. package/dist/cjs/templates/cloudflare-worker-path.mjs +98 -0
  42. package/dist/esm/cloudflare/artifacts.mjs +64 -0
  43. package/dist/esm/cloudflare/constants.mjs +34 -0
  44. package/dist/esm/cloudflare/delivery-unit.mjs +110 -0
  45. package/dist/esm/cloudflare/i18n-worker.mjs +97 -0
  46. package/dist/esm/cloudflare/index.mjs +108 -0
  47. package/dist/esm/cloudflare/security-policies.mjs +167 -0
  48. package/dist/esm/cloudflare/types.mjs +0 -0
  49. package/dist/esm/cloudflare/utils.mjs +12 -0
  50. package/dist/esm/cloudflare/worker-manifest.mjs +179 -0
  51. package/dist/esm/cloudflare/wrangler-config.mjs +151 -0
  52. package/dist/esm/cloudflare-builder.mjs +327 -0
  53. package/dist/esm/cloudflare-output-contract.mjs +31 -0
  54. package/dist/esm/cloudflare-output-plan.mjs +45 -0
  55. package/dist/esm/cloudflare-output-verifier/identity.mjs +54 -0
  56. package/dist/esm/cloudflare-output-verifier/index.mjs +133 -0
  57. package/dist/esm/cloudflare-output-verifier/issues.mjs +10 -0
  58. package/dist/esm/cloudflare-output-verifier/manifest.mjs +51 -0
  59. package/dist/esm/cloudflare-output-verifier/mutation-policy.mjs +99 -0
  60. package/dist/esm/cloudflare-output-verifier/worker-bundles.mjs +313 -0
  61. package/dist/esm/config.mjs +0 -0
  62. package/dist/esm/css-extract-runtime-plugin.mjs +32 -0
  63. package/dist/esm/index.mjs +10 -0
  64. package/dist/esm/read-template.mjs +5 -0
  65. package/dist/esm/release-envelope/canonical.mjs +21 -0
  66. package/dist/esm/release-envelope/framework-output.mjs +573 -0
  67. package/dist/esm/release-envelope/index.mjs +335 -0
  68. package/dist/esm/release-envelope/plugin.mjs +54 -0
  69. package/dist/esm/release-envelope/types.mjs +7 -0
  70. package/dist/esm/release-identity.mjs +66 -0
  71. package/dist/esm/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  72. package/dist/esm/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  73. package/dist/esm/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  74. package/dist/esm/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  75. package/dist/esm/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  76. package/dist/esm/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  77. package/dist/esm/templates/cloudflare-worker-fs-promises.mjs +11 -0
  78. package/dist/esm/templates/cloudflare-worker-loadable-server.mjs +281 -0
  79. package/dist/esm/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  80. package/dist/esm/templates/cloudflare-worker-path.mjs +98 -0
  81. package/dist/esm-node/cloudflare/artifacts.mjs +65 -0
  82. package/dist/esm-node/cloudflare/constants.mjs +35 -0
  83. package/dist/esm-node/cloudflare/delivery-unit.mjs +111 -0
  84. package/dist/esm-node/cloudflare/i18n-worker.mjs +98 -0
  85. package/dist/esm-node/cloudflare/index.mjs +109 -0
  86. package/dist/esm-node/cloudflare/security-policies.mjs +168 -0
  87. package/dist/esm-node/cloudflare/types.mjs +1 -0
  88. package/dist/esm-node/cloudflare/utils.mjs +13 -0
  89. package/dist/esm-node/cloudflare/worker-manifest.mjs +180 -0
  90. package/dist/esm-node/cloudflare/wrangler-config.mjs +152 -0
  91. package/dist/esm-node/cloudflare-builder.mjs +328 -0
  92. package/dist/esm-node/cloudflare-output-contract.mjs +32 -0
  93. package/dist/esm-node/cloudflare-output-plan.mjs +46 -0
  94. package/dist/esm-node/cloudflare-output-verifier/identity.mjs +55 -0
  95. package/dist/esm-node/cloudflare-output-verifier/index.mjs +134 -0
  96. package/dist/esm-node/cloudflare-output-verifier/issues.mjs +11 -0
  97. package/dist/esm-node/cloudflare-output-verifier/manifest.mjs +52 -0
  98. package/dist/esm-node/cloudflare-output-verifier/mutation-policy.mjs +100 -0
  99. package/dist/esm-node/cloudflare-output-verifier/worker-bundles.mjs +314 -0
  100. package/dist/esm-node/config.mjs +1 -0
  101. package/dist/esm-node/css-extract-runtime-plugin.mjs +33 -0
  102. package/dist/esm-node/index.mjs +11 -0
  103. package/dist/esm-node/read-template.mjs +9 -0
  104. package/dist/esm-node/release-envelope/canonical.mjs +22 -0
  105. package/dist/esm-node/release-envelope/framework-output.mjs +574 -0
  106. package/dist/esm-node/release-envelope/index.mjs +336 -0
  107. package/dist/esm-node/release-envelope/plugin.mjs +55 -0
  108. package/dist/esm-node/release-envelope/types.mjs +8 -0
  109. package/dist/esm-node/release-identity.mjs +67 -0
  110. package/dist/esm-node/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  111. package/dist/esm-node/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  112. package/dist/esm-node/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  113. package/dist/esm-node/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  114. package/dist/esm-node/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  115. package/dist/esm-node/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  116. package/dist/esm-node/templates/cloudflare-worker-fs-promises.mjs +11 -0
  117. package/dist/esm-node/templates/cloudflare-worker-loadable-server.mjs +281 -0
  118. package/dist/esm-node/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  119. package/dist/esm-node/templates/cloudflare-worker-path.mjs +98 -0
  120. package/dist/types/cloudflare/artifacts.d.ts +15 -0
  121. package/dist/types/cloudflare/constants.d.ts +24 -0
  122. package/dist/types/cloudflare/delivery-unit.d.ts +23 -0
  123. package/dist/types/cloudflare/i18n-worker.d.ts +22 -0
  124. package/dist/types/cloudflare/index.d.ts +2 -0
  125. package/dist/types/cloudflare/security-policies.d.ts +52 -0
  126. package/dist/types/cloudflare/types.d.ts +28 -0
  127. package/dist/types/cloudflare/utils.d.ts +6 -0
  128. package/dist/types/cloudflare/worker-manifest.d.ts +149 -0
  129. package/dist/types/cloudflare/wrangler-config.d.ts +51 -0
  130. package/dist/types/cloudflare-builder.d.ts +59 -0
  131. package/dist/types/cloudflare-output-contract.d.ts +14 -0
  132. package/dist/types/cloudflare-output-plan.d.ts +34 -0
  133. package/dist/types/cloudflare-output-verifier/identity.d.ts +8 -0
  134. package/dist/types/cloudflare-output-verifier/index.d.ts +18 -0
  135. package/dist/types/cloudflare-output-verifier/issues.d.ts +14 -0
  136. package/dist/types/cloudflare-output-verifier/manifest.d.ts +2 -0
  137. package/dist/types/cloudflare-output-verifier/mutation-policy.d.ts +14 -0
  138. package/dist/types/cloudflare-output-verifier/worker-bundles.d.ts +20 -0
  139. package/dist/types/config.d.ts +175 -0
  140. package/dist/types/css-extract-runtime-plugin.d.ts +6 -0
  141. package/dist/types/index.d.ts +10 -0
  142. package/dist/types/read-template.d.ts +2 -0
  143. package/dist/types/release-envelope/canonical.d.ts +5 -0
  144. package/dist/types/release-envelope/framework-output.d.ts +32 -0
  145. package/dist/types/release-envelope/index.d.ts +6 -0
  146. package/dist/types/release-envelope/plugin.d.ts +30 -0
  147. package/dist/types/release-envelope/types.d.ts +62 -0
  148. package/dist/types/release-identity.d.ts +16 -0
  149. package/dist/types/templates/cloudflare-entry.001-bootstrap-security.d.mts +1 -0
  150. package/dist/types/templates/cloudflare-entry.002-assets-routes.d.mts +1 -0
  151. package/dist/types/templates/cloudflare-entry.003-i18n-locales.d.mts +1 -0
  152. package/dist/types/templates/cloudflare-entry.004-rendering-css.d.mts +1 -0
  153. package/dist/types/templates/cloudflare-entry.005-worker-dispatch.d.mts +1 -0
  154. package/dist/types/templates/cloudflare-entry.006-fetch-handler.d.mts +4 -0
  155. package/dist/types/templates/cloudflare-worker-fs-promises.d.mts +5 -0
  156. package/dist/types/templates/cloudflare-worker-loadable-server.d.mts +48 -0
  157. package/dist/types/templates/cloudflare-worker-mf-ssr-runtime-plugin.d.mts +13 -0
  158. package/dist/types/templates/cloudflare-worker-path.d.mts +21 -0
  159. package/package.json +131 -0
  160. package/src/cloudflare/artifacts.ts +165 -0
  161. package/src/cloudflare/constants.ts +50 -0
  162. package/src/cloudflare/delivery-unit.ts +195 -0
  163. package/src/cloudflare/i18n-worker.ts +197 -0
  164. package/src/cloudflare/index.ts +259 -0
  165. package/src/cloudflare/security-policies.ts +194 -0
  166. package/src/cloudflare/types.ts +33 -0
  167. package/src/cloudflare/utils.ts +40 -0
  168. package/src/cloudflare/worker-manifest.ts +313 -0
  169. package/src/cloudflare/wrangler-config.ts +338 -0
  170. package/src/cloudflare-builder.ts +653 -0
  171. package/src/cloudflare-output-contract.ts +30 -0
  172. package/src/cloudflare-output-plan.ts +108 -0
  173. package/src/cloudflare-output-verifier/identity.ts +87 -0
  174. package/src/cloudflare-output-verifier/index.ts +277 -0
  175. package/src/cloudflare-output-verifier/issues.ts +54 -0
  176. package/src/cloudflare-output-verifier/manifest.ts +76 -0
  177. package/src/cloudflare-output-verifier/mutation-policy.ts +159 -0
  178. package/src/cloudflare-output-verifier/worker-bundles.ts +664 -0
  179. package/src/config.ts +196 -0
  180. package/src/css-extract-runtime-plugin.ts +70 -0
  181. package/src/index.ts +10 -0
  182. package/src/read-template.ts +8 -0
  183. package/src/release-envelope/canonical.ts +58 -0
  184. package/src/release-envelope/framework-output.ts +1121 -0
  185. package/src/release-envelope/index.ts +656 -0
  186. package/src/release-envelope/plugin.ts +125 -0
  187. package/src/release-envelope/types.ts +83 -0
  188. package/src/release-identity.ts +134 -0
  189. package/src/templates/cloudflare-entry.001-bootstrap-security.mjs +251 -0
  190. package/src/templates/cloudflare-entry.002-assets-routes.mjs +191 -0
  191. package/src/templates/cloudflare-entry.003-i18n-locales.mjs +514 -0
  192. package/src/templates/cloudflare-entry.004-rendering-css.mjs +1194 -0
  193. package/src/templates/cloudflare-entry.005-worker-dispatch.mjs +421 -0
  194. package/src/templates/cloudflare-entry.006-fetch-handler.mjs +93 -0
  195. package/src/templates/cloudflare-worker-fs-promises.mjs +11 -0
  196. package/src/templates/cloudflare-worker-loadable-server.mjs +281 -0
  197. package/src/templates/cloudflare-worker-mf-ssr-runtime-plugin.mjs +14 -0
  198. package/src/templates/cloudflare-worker-path.mjs +98 -0
@@ -0,0 +1,656 @@
1
+ import { createHash } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import {
5
+ canonicalSerializeMicroVerticalReleaseEnvelope,
6
+ digestMicroVerticalReleaseEnvelopePayload,
7
+ releaseEnvelopePayload,
8
+ } from './canonical';
9
+ import {
10
+ type CreateMicroVerticalReleaseEnvelopeInput,
11
+ MICROVERTICAL_RELEASE_ENVELOPE_KIND,
12
+ MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION,
13
+ MICROVERTICAL_RELEASE_TARGETS,
14
+ type MicroVerticalReleaseArtifact,
15
+ type MicroVerticalReleaseArtifactInput,
16
+ type MicroVerticalReleaseEnvelope,
17
+ type MicroVerticalReleaseEnvelopePayload,
18
+ type MicroVerticalReleaseIdentity,
19
+ type MicroVerticalReleaseSurfaces,
20
+ type MicroVerticalReleaseTarget,
21
+ type VerifyMicroVerticalReleaseEnvelopeOptions,
22
+ } from './types';
23
+
24
+ export type {
25
+ CreateMicroVerticalReleaseEnvelopeInput,
26
+ MicroVerticalReleaseArtifact,
27
+ MicroVerticalReleaseArtifactInput,
28
+ MicroVerticalReleaseArtifactInputs,
29
+ MicroVerticalReleaseEnvelope,
30
+ MicroVerticalReleaseEnvelopePayload,
31
+ MicroVerticalReleaseFileArtifact,
32
+ MicroVerticalReleaseIdentity,
33
+ MicroVerticalReleaseSurfaces,
34
+ MicroVerticalReleaseSymbolicLinkArtifact,
35
+ MicroVerticalReleaseTarget,
36
+ VerifyMicroVerticalReleaseEnvelopeOptions,
37
+ } from './types';
38
+ export {
39
+ canonicalSerializeMicroVerticalReleaseEnvelope,
40
+ MICROVERTICAL_RELEASE_ENVELOPE_KIND,
41
+ MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION,
42
+ MICROVERTICAL_RELEASE_TARGETS,
43
+ };
44
+
45
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
46
+ const SOURCE_REVISION_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u;
47
+ const TARGETS = new Set<string>(MICROVERTICAL_RELEASE_TARGETS);
48
+
49
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
50
+ typeof value === 'object' && value !== null && !Array.isArray(value);
51
+
52
+ const assertRecord = (
53
+ value: unknown,
54
+ location: string,
55
+ ): Record<string, unknown> => {
56
+ if (!isRecord(value)) {
57
+ throw new Error(`${location} must be an object.`);
58
+ }
59
+ return value;
60
+ };
61
+
62
+ const assertExactKeys = (
63
+ value: Record<string, unknown>,
64
+ expected: readonly string[],
65
+ location: string,
66
+ ) => {
67
+ const expectedKeys = new Set(expected);
68
+ for (const key of Object.keys(value)) {
69
+ if (!expectedKeys.has(key)) {
70
+ throw new Error(`${location} contains unknown field "${key}".`);
71
+ }
72
+ }
73
+ for (const key of expected) {
74
+ if (!(key in value)) {
75
+ throw new Error(`${location}.${key} is required.`);
76
+ }
77
+ }
78
+ };
79
+
80
+ const assertNonEmptyString = (value: unknown, location: string): string => {
81
+ if (typeof value !== 'string' || value.trim().length === 0) {
82
+ throw new Error(`${location} must be a non-empty string.`);
83
+ }
84
+ if (value !== value.trim()) {
85
+ throw new Error(`${location} must not contain surrounding whitespace.`);
86
+ }
87
+ return value;
88
+ };
89
+
90
+ const assertTarget = (
91
+ value: unknown,
92
+ location = 'target',
93
+ ): MicroVerticalReleaseTarget => {
94
+ const target = assertNonEmptyString(value, location);
95
+ if (!TARGETS.has(target)) {
96
+ throw new Error(
97
+ `${location} must be one of: ${MICROVERTICAL_RELEASE_TARGETS.join(', ')}.`,
98
+ );
99
+ }
100
+ return target as MicroVerticalReleaseTarget;
101
+ };
102
+
103
+ const assertReleaseIdentity = (
104
+ value: unknown,
105
+ location = 'identity',
106
+ ): MicroVerticalReleaseIdentity => {
107
+ const identity = assertRecord(value, location);
108
+ assertExactKeys(
109
+ identity,
110
+ ['unitId', 'buildMarker', 'sourceRevision', 'releaseVersion'],
111
+ location,
112
+ );
113
+ const sourceRevision = assertNonEmptyString(
114
+ identity.sourceRevision,
115
+ `${location}.sourceRevision`,
116
+ );
117
+ if (!SOURCE_REVISION_PATTERN.test(sourceRevision)) {
118
+ throw new Error(
119
+ `${location}.sourceRevision must be an exact lowercase 40- or 64-character Git object ID; "${sourceRevision}" is not promotable.`,
120
+ );
121
+ }
122
+ return {
123
+ unitId: assertNonEmptyString(identity.unitId, `${location}.unitId`),
124
+ buildMarker: assertNonEmptyString(
125
+ identity.buildMarker,
126
+ `${location}.buildMarker`,
127
+ ),
128
+ sourceRevision,
129
+ releaseVersion: assertNonEmptyString(
130
+ identity.releaseVersion,
131
+ `${location}.releaseVersion`,
132
+ ),
133
+ };
134
+ };
135
+
136
+ const assertNormalizedLogicalPath = (value: unknown, location: string) => {
137
+ const logicalPath = assertNonEmptyString(value, location);
138
+ if (
139
+ logicalPath.includes('\\') ||
140
+ path.posix.isAbsolute(logicalPath) ||
141
+ path.posix.normalize(logicalPath) !== logicalPath ||
142
+ logicalPath === '.' ||
143
+ logicalPath.split('/').some(segment => segment === '..' || segment === '.')
144
+ ) {
145
+ throw new Error(`${location} must be a normalized relative POSIX path.`);
146
+ }
147
+ return logicalPath;
148
+ };
149
+
150
+ const isPathInside = (root: string, candidate: string) => {
151
+ const relative = path.relative(root, candidate);
152
+ return (
153
+ relative === '' ||
154
+ (!relative.startsWith(`..${path.sep}`) &&
155
+ relative !== '..' &&
156
+ !path.isAbsolute(relative))
157
+ );
158
+ };
159
+
160
+ const assertArtifactInput = (
161
+ value: unknown,
162
+ location: string,
163
+ ): MicroVerticalReleaseArtifactInput => {
164
+ const artifact = assertRecord(value, location);
165
+ assertExactKeys(artifact, ['logicalPath', 'runtime'], location);
166
+ return {
167
+ logicalPath: assertNormalizedLogicalPath(
168
+ artifact.logicalPath,
169
+ `${location}.logicalPath`,
170
+ ),
171
+ runtime: assertNonEmptyString(artifact.runtime, `${location}.runtime`),
172
+ };
173
+ };
174
+
175
+ const readFinalArtifact = async (
176
+ artifactRoot: string,
177
+ input: MicroVerticalReleaseArtifactInput,
178
+ resolvedLogicalPath = input.logicalPath,
179
+ ): Promise<MicroVerticalReleaseArtifact> => {
180
+ const artifactInput = {
181
+ logicalPath: input.logicalPath,
182
+ runtime: input.runtime,
183
+ };
184
+ const logicalPath = assertNormalizedLogicalPath(
185
+ resolvedLogicalPath,
186
+ `staged path for ${input.logicalPath}`,
187
+ );
188
+ const resolvedRoot = path.resolve(artifactRoot);
189
+ const logicalSegments = logicalPath.split('/');
190
+ const lexicalPath = path.resolve(resolvedRoot, ...logicalSegments);
191
+ if (!isPathInside(resolvedRoot, lexicalPath)) {
192
+ throw new Error(
193
+ `Artifact logicalPath "${logicalPath}" resolves outside artifactRoot.`,
194
+ );
195
+ }
196
+ const realRoot = await fs.realpath(resolvedRoot);
197
+ for (let index = 0; index < logicalSegments.length - 1; index++) {
198
+ const linkLogicalPath = logicalSegments.slice(0, index + 1).join('/');
199
+ const candidate = path.join(
200
+ resolvedRoot,
201
+ ...logicalSegments.slice(0, index + 1),
202
+ );
203
+ let candidateStat;
204
+ try {
205
+ candidateStat = await fs.lstat(candidate);
206
+ } catch {
207
+ throw new Error(`Artifact "${logicalPath}" does not exist.`);
208
+ }
209
+ if (candidateStat.isSymbolicLink()) {
210
+ throw new Error(
211
+ `Artifact "${logicalPath}" traverses symbolic-link ancestor "${linkLogicalPath}"; bind the symbolic link itself.`,
212
+ );
213
+ }
214
+ }
215
+
216
+ let lexicalStat;
217
+ try {
218
+ lexicalStat = await fs.lstat(lexicalPath);
219
+ } catch {
220
+ throw new Error(`Artifact "${logicalPath}" does not exist.`);
221
+ }
222
+ if (lexicalStat.isSymbolicLink()) {
223
+ let realTarget: string;
224
+ try {
225
+ realTarget = await fs.realpath(lexicalPath);
226
+ } catch {
227
+ throw new Error(
228
+ `Artifact symbolic link "${logicalPath}" cannot be resolved.`,
229
+ );
230
+ }
231
+ if (!isPathInside(realRoot, realTarget)) {
232
+ throw new Error(
233
+ `Artifact logicalPath "${logicalPath}" resolves outside artifactRoot.`,
234
+ );
235
+ }
236
+ const targetStat = await fs.stat(realTarget);
237
+ const targetKind = targetStat.isFile()
238
+ ? 'file'
239
+ : targetStat.isDirectory()
240
+ ? 'directory'
241
+ : undefined;
242
+ if (!targetKind) {
243
+ throw new Error(
244
+ `Artifact symbolic link "${logicalPath}" must resolve to a file or directory.`,
245
+ );
246
+ }
247
+ const targetLogicalPath = path
248
+ .relative(realRoot, realTarget)
249
+ .split(path.sep)
250
+ .join('/');
251
+ if (
252
+ targetLogicalPath === 'release' ||
253
+ targetLogicalPath.startsWith('release/')
254
+ ) {
255
+ throw new Error(
256
+ `Artifact symbolic link "${logicalPath}" targets private release metadata.`,
257
+ );
258
+ }
259
+ if (
260
+ targetKind === 'directory' &&
261
+ isPathInside(realTarget, await fs.realpath(path.dirname(lexicalPath)))
262
+ ) {
263
+ throw new Error(
264
+ `Artifact symbolic link "${logicalPath}" targets an ancestor directory.`,
265
+ );
266
+ }
267
+ return {
268
+ ...artifactInput,
269
+ kind: 'symbolic-link',
270
+ linkTarget: await fs.readlink(lexicalPath),
271
+ targetKind,
272
+ targetLogicalPath,
273
+ };
274
+ }
275
+ if (!lexicalStat.isFile()) {
276
+ throw new Error(`Artifact "${logicalPath}" must be a file or symlink.`);
277
+ }
278
+ const realPath = await fs.realpath(lexicalPath);
279
+ if (!isPathInside(realRoot, realPath)) {
280
+ throw new Error(
281
+ `Artifact logicalPath "${logicalPath}" resolves outside artifactRoot.`,
282
+ );
283
+ }
284
+ const bytes = await fs.readFile(realPath);
285
+ return {
286
+ ...artifactInput,
287
+ kind: 'file',
288
+ byteLength: bytes.byteLength,
289
+ sha256: createHash('sha256').update(bytes).digest('hex'),
290
+ };
291
+ };
292
+
293
+ const assertReleaseArtifact = (
294
+ value: unknown,
295
+ location: string,
296
+ ): MicroVerticalReleaseArtifact => {
297
+ const artifact = assertRecord(value, location);
298
+ const kind = assertNonEmptyString(artifact.kind, `${location}.kind`);
299
+ const input = {
300
+ logicalPath: artifact.logicalPath,
301
+ runtime: artifact.runtime,
302
+ };
303
+ if (kind === 'file') {
304
+ assertExactKeys(
305
+ artifact,
306
+ ['kind', 'logicalPath', 'runtime', 'byteLength', 'sha256'],
307
+ location,
308
+ );
309
+ const byteLength = artifact.byteLength;
310
+ if (
311
+ typeof byteLength !== 'number' ||
312
+ !Number.isSafeInteger(byteLength) ||
313
+ byteLength < 0
314
+ ) {
315
+ throw new Error(`${location}.byteLength must be a non-negative integer.`);
316
+ }
317
+ const sha256 = assertNonEmptyString(artifact.sha256, `${location}.sha256`);
318
+ if (!SHA256_PATTERN.test(sha256)) {
319
+ throw new Error(`${location}.sha256 must be a lowercase SHA-256 digest.`);
320
+ }
321
+ return {
322
+ ...assertArtifactInput(input, location),
323
+ kind,
324
+ byteLength,
325
+ sha256,
326
+ };
327
+ }
328
+ if (kind === 'symbolic-link') {
329
+ assertExactKeys(
330
+ artifact,
331
+ [
332
+ 'kind',
333
+ 'logicalPath',
334
+ 'runtime',
335
+ 'linkTarget',
336
+ 'targetKind',
337
+ 'targetLogicalPath',
338
+ ],
339
+ location,
340
+ );
341
+ const targetKind = assertNonEmptyString(
342
+ artifact.targetKind,
343
+ `${location}.targetKind`,
344
+ );
345
+ if (targetKind !== 'directory' && targetKind !== 'file') {
346
+ throw new Error(`${location}.targetKind must be "directory" or "file".`);
347
+ }
348
+ return {
349
+ ...assertArtifactInput(input, location),
350
+ kind,
351
+ linkTarget: assertNonEmptyString(
352
+ artifact.linkTarget,
353
+ `${location}.linkTarget`,
354
+ ),
355
+ targetKind,
356
+ targetLogicalPath: assertNormalizedLogicalPath(
357
+ artifact.targetLogicalPath,
358
+ `${location}.targetLogicalPath`,
359
+ ),
360
+ };
361
+ }
362
+ throw new Error(`${location}.kind must be "file" or "symbolic-link".`);
363
+ };
364
+
365
+ const assertUniqueSortedArtifacts = (
366
+ artifacts: readonly { logicalPath: string }[],
367
+ location: string,
368
+ ) => {
369
+ const paths = artifacts.map(artifact => artifact.logicalPath);
370
+ const unique = new Set(paths);
371
+ if (unique.size !== paths.length) {
372
+ const duplicate = paths.find(
373
+ (logicalPath, index) => paths.indexOf(logicalPath) !== index,
374
+ );
375
+ throw new Error(`Duplicate artifact logicalPath "${duplicate}".`);
376
+ }
377
+ const sorted = [...paths].sort((left, right) => left.localeCompare(right));
378
+ if (paths.some((logicalPath, index) => logicalPath !== sorted[index])) {
379
+ throw new Error(`${location} must be sorted by logicalPath.`);
380
+ }
381
+ };
382
+
383
+ const assertSurfacePaths = (value: unknown, location: string): string[] => {
384
+ if (!Array.isArray(value) || value.length === 0) {
385
+ throw new Error(`${location} must contain at least one artifact path.`);
386
+ }
387
+ const paths = value.map((item, index) =>
388
+ assertNormalizedLogicalPath(item, `${location}[${index}]`),
389
+ );
390
+ const sorted = [...paths].sort((left, right) => left.localeCompare(right));
391
+ if (paths.some((logicalPath, index) => logicalPath !== sorted[index])) {
392
+ throw new Error(`${location} must be sorted by logicalPath.`);
393
+ }
394
+ if (new Set(paths).size !== paths.length) {
395
+ throw new Error(`${location} must not contain duplicate artifact paths.`);
396
+ }
397
+ return paths;
398
+ };
399
+
400
+ const assertSurfaces = (value: unknown): MicroVerticalReleaseSurfaces => {
401
+ const surfaces = assertRecord(value, 'surfaces');
402
+ assertExactKeys(
403
+ surfaces,
404
+ ['uiClient', 'ssr', 'apiBackend', 'backendFederation'],
405
+ 'surfaces',
406
+ );
407
+ const backendFederation = assertRecord(
408
+ surfaces.backendFederation,
409
+ 'surfaces.backendFederation',
410
+ );
411
+ assertExactKeys(
412
+ backendFederation,
413
+ ['manifest', 'container'],
414
+ 'surfaces.backendFederation',
415
+ );
416
+ return {
417
+ uiClient: assertSurfacePaths(surfaces.uiClient, 'surfaces.uiClient'),
418
+ ssr: assertSurfacePaths(surfaces.ssr, 'surfaces.ssr'),
419
+ apiBackend: assertSurfacePaths(surfaces.apiBackend, 'surfaces.apiBackend'),
420
+ backendFederation: {
421
+ manifest: assertNormalizedLogicalPath(
422
+ backendFederation.manifest,
423
+ 'surfaces.backendFederation.manifest',
424
+ ),
425
+ container: assertNormalizedLogicalPath(
426
+ backendFederation.container,
427
+ 'surfaces.backendFederation.container',
428
+ ),
429
+ },
430
+ };
431
+ };
432
+
433
+ const assertSurfaceReferences = (
434
+ artifacts: readonly MicroVerticalReleaseArtifactInput[],
435
+ surfaces: MicroVerticalReleaseSurfaces,
436
+ ) => {
437
+ const artifactPaths = new Set(
438
+ artifacts.map(artifact => artifact.logicalPath),
439
+ );
440
+ for (const [surface, paths] of Object.entries({
441
+ uiClient: surfaces.uiClient,
442
+ ssr: surfaces.ssr,
443
+ apiBackend: surfaces.apiBackend,
444
+ 'backendFederation.manifest': [surfaces.backendFederation.manifest],
445
+ 'backendFederation.container': [surfaces.backendFederation.container],
446
+ })) {
447
+ for (const logicalPath of paths) {
448
+ if (!artifactPaths.has(logicalPath)) {
449
+ throw new Error(
450
+ `surfaces.${surface} references unbound artifact "${logicalPath}".`,
451
+ );
452
+ }
453
+ }
454
+ }
455
+ };
456
+
457
+ const assertTargetSurfaceContract = (
458
+ target: MicroVerticalReleaseTarget,
459
+ artifacts: readonly MicroVerticalReleaseArtifactInput[],
460
+ surfaces: MicroVerticalReleaseSurfaces,
461
+ ) => {
462
+ const byPath = new Map(
463
+ artifacts.map(artifact => [artifact.logicalPath, artifact]),
464
+ );
465
+ const assertRuntime = (
466
+ paths: readonly string[],
467
+ expected: string,
468
+ surface: string,
469
+ ) => {
470
+ for (const logicalPath of paths) {
471
+ const artifact = byPath.get(logicalPath);
472
+ if (artifact?.runtime !== expected) {
473
+ throw new Error(
474
+ `${target} ${surface} artifact "${logicalPath}" must use runtime "${expected}"; received "${String(artifact?.runtime)}".`,
475
+ );
476
+ }
477
+ if ('kind' in artifact && artifact.kind !== 'file') {
478
+ throw new Error(
479
+ `${target} ${surface} artifact "${logicalPath}" must be a file.`,
480
+ );
481
+ }
482
+ }
483
+ };
484
+
485
+ assertRuntime(surfaces.uiClient, 'browser', 'UI/client');
486
+ assertRuntime(surfaces.ssr, target === 'node' ? 'nodejs' : 'workerd', 'SSR');
487
+ assertRuntime(
488
+ surfaces.apiBackend,
489
+ target === 'node' ? 'nodejs' : 'workerd-effect',
490
+ 'API/backend',
491
+ );
492
+ assertRuntime(
493
+ [surfaces.backendFederation.manifest],
494
+ 'module-federation-manifest',
495
+ 'backend federation manifest',
496
+ );
497
+ assertRuntime(
498
+ [surfaces.backendFederation.container],
499
+ target === 'node' ? 'nodejs' : 'commonjs-module',
500
+ 'backend federation container',
501
+ );
502
+ };
503
+
504
+ const deepFreeze = <T>(value: T): T => {
505
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
506
+ Object.freeze(value);
507
+ for (const child of Object.values(value)) {
508
+ deepFreeze(child);
509
+ }
510
+ }
511
+ return value;
512
+ };
513
+
514
+ const assertEnvelope = (value: unknown): MicroVerticalReleaseEnvelope => {
515
+ const envelope = assertRecord(value, 'envelope');
516
+ assertExactKeys(
517
+ envelope,
518
+ [
519
+ 'schemaVersion',
520
+ 'kind',
521
+ 'target',
522
+ 'identity',
523
+ 'artifacts',
524
+ 'surfaces',
525
+ 'envelopeDigest',
526
+ ],
527
+ 'envelope',
528
+ );
529
+ if (
530
+ envelope.schemaVersion !== MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION
531
+ ) {
532
+ throw new Error(
533
+ `envelope.schemaVersion must be ${MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION}.`,
534
+ );
535
+ }
536
+ if (envelope.kind !== MICROVERTICAL_RELEASE_ENVELOPE_KIND) {
537
+ throw new Error(
538
+ `envelope.kind must be "${MICROVERTICAL_RELEASE_ENVELOPE_KIND}".`,
539
+ );
540
+ }
541
+ if (!Array.isArray(envelope.artifacts) || envelope.artifacts.length === 0) {
542
+ throw new Error('envelope.artifacts must contain at least one artifact.');
543
+ }
544
+ const artifacts = envelope.artifacts.map((artifact, index) =>
545
+ assertReleaseArtifact(artifact, `envelope.artifacts[${index}]`),
546
+ );
547
+ assertUniqueSortedArtifacts(artifacts, 'envelope.artifacts');
548
+ const target = assertTarget(envelope.target, 'envelope.target');
549
+ const surfaces = assertSurfaces(envelope.surfaces);
550
+ assertSurfaceReferences(artifacts, surfaces);
551
+ assertTargetSurfaceContract(target, artifacts, surfaces);
552
+ const parsed: MicroVerticalReleaseEnvelope = {
553
+ schemaVersion: MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION,
554
+ kind: MICROVERTICAL_RELEASE_ENVELOPE_KIND,
555
+ target,
556
+ identity: assertReleaseIdentity(envelope.identity, 'envelope.identity'),
557
+ artifacts,
558
+ surfaces,
559
+ envelopeDigest: assertNonEmptyString(
560
+ envelope.envelopeDigest,
561
+ 'envelope.envelopeDigest',
562
+ ),
563
+ };
564
+ if (!SHA256_PATTERN.test(parsed.envelopeDigest)) {
565
+ throw new Error(
566
+ 'envelope.envelopeDigest must be a lowercase SHA-256 digest.',
567
+ );
568
+ }
569
+ const expectedDigest = digestMicroVerticalReleaseEnvelopePayload(
570
+ releaseEnvelopePayload(parsed),
571
+ );
572
+ if (parsed.envelopeDigest !== expectedDigest) {
573
+ throw new Error(
574
+ 'envelope.envelopeDigest does not match canonical payload.',
575
+ );
576
+ }
577
+ return parsed;
578
+ };
579
+
580
+ export const createMicroVerticalReleaseEnvelope = async (
581
+ input: CreateMicroVerticalReleaseEnvelopeInput,
582
+ ): Promise<MicroVerticalReleaseEnvelope> => {
583
+ const artifactRoot = await fs.realpath(path.resolve(input.artifactRoot));
584
+ const target = assertTarget(input.target);
585
+ const identity = assertReleaseIdentity(input.identity);
586
+ if (!Array.isArray(input.artifacts) || input.artifacts.length === 0) {
587
+ throw new Error('artifacts must contain at least one artifact.');
588
+ }
589
+ const inputs = input.artifacts.map((artifact, index) =>
590
+ assertArtifactInput(artifact, `artifacts[${index}]`),
591
+ );
592
+ const sortedInputs = [...inputs].sort((left, right) =>
593
+ left.logicalPath.localeCompare(right.logicalPath),
594
+ );
595
+ assertUniqueSortedArtifacts(sortedInputs, 'artifacts');
596
+ const surfaces = assertSurfaces(input.surfaces);
597
+ assertSurfaceReferences(sortedInputs, surfaces);
598
+ assertTargetSurfaceContract(target, sortedInputs, surfaces);
599
+ const artifacts = await Promise.all(
600
+ sortedInputs.map(artifact => readFinalArtifact(artifactRoot, artifact)),
601
+ );
602
+ assertTargetSurfaceContract(target, artifacts, surfaces);
603
+ const payload: MicroVerticalReleaseEnvelopePayload = {
604
+ schemaVersion: MICROVERTICAL_RELEASE_ENVELOPE_SCHEMA_VERSION,
605
+ kind: MICROVERTICAL_RELEASE_ENVELOPE_KIND,
606
+ target,
607
+ identity,
608
+ artifacts,
609
+ surfaces,
610
+ };
611
+ return deepFreeze({
612
+ ...payload,
613
+ envelopeDigest: digestMicroVerticalReleaseEnvelopePayload(payload),
614
+ });
615
+ };
616
+
617
+ export const verifyMicroVerticalReleaseEnvelope = async (
618
+ value: unknown,
619
+ options: VerifyMicroVerticalReleaseEnvelopeOptions,
620
+ ): Promise<MicroVerticalReleaseEnvelope> => {
621
+ const envelope = assertEnvelope(value);
622
+ if (
623
+ options.expectedTarget !== undefined &&
624
+ envelope.target !== options.expectedTarget
625
+ ) {
626
+ throw new Error(
627
+ `envelope.target must be "${options.expectedTarget}" for this staging target; received "${envelope.target}".`,
628
+ );
629
+ }
630
+ const artifactRoot = await fs.realpath(path.resolve(options.artifactRoot));
631
+ for (const artifact of envelope.artifacts) {
632
+ const finalArtifact = await readFinalArtifact(
633
+ artifactRoot,
634
+ artifact,
635
+ options.logicalPathForArtifact?.(artifact) ?? artifact.logicalPath,
636
+ );
637
+ const matches =
638
+ finalArtifact.kind === artifact.kind &&
639
+ (artifact.kind === 'file' && finalArtifact.kind === 'file'
640
+ ? finalArtifact.byteLength === artifact.byteLength &&
641
+ finalArtifact.sha256 === artifact.sha256
642
+ : artifact.kind === 'symbolic-link' &&
643
+ finalArtifact.kind === 'symbolic-link' &&
644
+ finalArtifact.linkTarget === artifact.linkTarget &&
645
+ finalArtifact.targetKind === artifact.targetKind &&
646
+ finalArtifact.targetLogicalPath === artifact.targetLogicalPath);
647
+ if (!matches) {
648
+ throw new Error(
649
+ artifact.kind === 'file' && finalArtifact.kind === 'file'
650
+ ? `Artifact "${artifact.logicalPath}" digest does not match final artifact bytes.`
651
+ : `Artifact "${artifact.logicalPath}" does not match its final filesystem binding.`,
652
+ );
653
+ }
654
+ }
655
+ return deepFreeze(envelope);
656
+ };