@void/deploy-core 0.20.0
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/LICENSE +21 -0
- package/README.md +14 -0
- package/dist/index.d.mts +400 -0
- package/dist/index.mjs +473 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present, Cloudflare, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @void/deploy-core
|
|
2
|
+
|
|
3
|
+
Shared deployment contracts for the Void SDK and hosted Void platform.
|
|
4
|
+
|
|
5
|
+
This package owns the wire types exchanged by the `void` CLI and the managed
|
|
6
|
+
platform, including deployment manifests, progress events, assets, routing
|
|
7
|
+
rules, queues, append-only Durable Object migration planning and metadata, normalized upload artifacts, runtime
|
|
8
|
+
manifest validation, and provider-neutral resource planning. It has no
|
|
9
|
+
dependency on either deployment target and must remain usable from Node.js and
|
|
10
|
+
Cloudflare Workers.
|
|
11
|
+
|
|
12
|
+
The package also defines the adapter boundary implemented by deployment
|
|
13
|
+
targets. Target-specific orchestration belongs in `void` or `platform/`, not
|
|
14
|
+
here.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
//#region src/validate.d.ts
|
|
2
|
+
export declare const MANIFEST_REDIRECT_RULE_STATUSES: ReadonlySet<number>;
|
|
3
|
+
export declare const MAX_ROUTING_RULES = 2e3;
|
|
4
|
+
export declare const MAX_REVALIDATE_QUERY_ALLOWLIST_ENTRIES: number;
|
|
5
|
+
export declare function validateManifestShape(parsed: unknown): string | null;
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region src/resource-plan.d.ts
|
|
8
|
+
type DeployResource = {
|
|
9
|
+
type: "d1";
|
|
10
|
+
bindingName: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: "kv";
|
|
13
|
+
bindingName: string;
|
|
14
|
+
} | {
|
|
15
|
+
type: "r2";
|
|
16
|
+
bindingName: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: "queue";
|
|
19
|
+
bindingName: string;
|
|
20
|
+
resourceName: string;
|
|
21
|
+
consumer: QueueMeta;
|
|
22
|
+
} | {
|
|
23
|
+
type: "hyperdrive";
|
|
24
|
+
bindingName: string;
|
|
25
|
+
};
|
|
26
|
+
type DeployResourcePlan = {
|
|
27
|
+
resources: Array<DeployResource>;
|
|
28
|
+
runtime: {
|
|
29
|
+
ai: boolean;
|
|
30
|
+
schedules: NonNullable<DeployManifest["schedules"]>;
|
|
31
|
+
websocket: DeployManifest["websocket"] | null;
|
|
32
|
+
sandbox: DeployManifest["sandbox"] | null;
|
|
33
|
+
live: DeployManifest["live"] | null;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Convert a deployment manifest into provider-neutral resource intent.
|
|
38
|
+
*
|
|
39
|
+
* Resource names, remote ids, and injected platform bindings belong to the
|
|
40
|
+
* target adapter and are deliberately absent from this plan.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createDeployResourcePlan(manifest: DeployManifest): DeployResourcePlan;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/target.d.ts
|
|
45
|
+
/** A concrete destination that can execute a Void deployment. */
|
|
46
|
+
type DeployTarget = "cloudflare" | "void";
|
|
47
|
+
type DeployAppType = "void" | "spa" | "static" | "framework";
|
|
48
|
+
type DeployCapabilitySupport = "supported" | "partial" | "unsupported";
|
|
49
|
+
/**
|
|
50
|
+
* Cross-target capabilities used by CLI planning and user-facing diagnostics.
|
|
51
|
+
*
|
|
52
|
+
* A target reports what it can execute today. Capability declarations must not
|
|
53
|
+
* be aspirational: feature gates are removed only when the corresponding
|
|
54
|
+
* adapter and end-to-end coverage exist.
|
|
55
|
+
*/
|
|
56
|
+
type DeployTargetCapabilities = {
|
|
57
|
+
managedProjects: boolean;
|
|
58
|
+
skipBuild: boolean;
|
|
59
|
+
appTypes: Readonly<Record<DeployAppType, DeployCapabilitySupport>>;
|
|
60
|
+
features: Readonly<{
|
|
61
|
+
auth: DeployCapabilitySupport;
|
|
62
|
+
cron: DeployCapabilitySupport;
|
|
63
|
+
queues: DeployCapabilitySupport;
|
|
64
|
+
durableState: DeployCapabilitySupport;
|
|
65
|
+
webSockets: DeployCapabilitySupport;
|
|
66
|
+
live: DeployCapabilitySupport;
|
|
67
|
+
sandbox: DeployCapabilitySupport;
|
|
68
|
+
isr: DeployCapabilitySupport;
|
|
69
|
+
remoteDevelopment: DeployCapabilitySupport;
|
|
70
|
+
deploymentHistory: DeployCapabilitySupport;
|
|
71
|
+
rollback: DeployCapabilitySupport;
|
|
72
|
+
secrets: DeployCapabilitySupport;
|
|
73
|
+
domains: DeployCapabilitySupport;
|
|
74
|
+
logs: DeployCapabilitySupport;
|
|
75
|
+
}>;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Boundary implemented by deployment targets.
|
|
79
|
+
*
|
|
80
|
+
* Build classification and packaging stay outside the adapter; provider
|
|
81
|
+
* credentials, resource names, remote ids, and execution stay inside it.
|
|
82
|
+
*/
|
|
83
|
+
type DeployTargetAdapter<TOptions, TContext> = {
|
|
84
|
+
readonly target: DeployTarget;
|
|
85
|
+
readonly capabilities: DeployTargetCapabilities;
|
|
86
|
+
deploy(root: string, options: TOptions, context: TContext): Promise<void>;
|
|
87
|
+
};
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/intent.d.ts
|
|
90
|
+
type DeployRuntimeFeatures = Readonly<{
|
|
91
|
+
assets: boolean;
|
|
92
|
+
auth: boolean;
|
|
93
|
+
cron: boolean;
|
|
94
|
+
queues: boolean;
|
|
95
|
+
migrations: boolean;
|
|
96
|
+
ai: boolean;
|
|
97
|
+
durableObjects: boolean;
|
|
98
|
+
webSockets: boolean;
|
|
99
|
+
live: boolean;
|
|
100
|
+
sandbox: boolean;
|
|
101
|
+
isr: boolean;
|
|
102
|
+
prerender: boolean;
|
|
103
|
+
}>;
|
|
104
|
+
/** Canonical, target-neutral interpretation of a deployment manifest. */
|
|
105
|
+
type DeployIntent = Readonly<{
|
|
106
|
+
appType: DeployAppType;
|
|
107
|
+
hasWorker: boolean;
|
|
108
|
+
manifest: DeployManifest;
|
|
109
|
+
resourcePlan: DeployResourcePlan;
|
|
110
|
+
features: DeployRuntimeFeatures;
|
|
111
|
+
}>;
|
|
112
|
+
/**
|
|
113
|
+
* Interpret a manifest once before a target assigns provider names and ids.
|
|
114
|
+
* This function performs no I/O and does not mutate the manifest.
|
|
115
|
+
*/
|
|
116
|
+
export declare function createDeployIntent(manifest: DeployManifest): DeployIntent;
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/artifact.d.ts
|
|
119
|
+
export declare const DEPLOY_ARTIFACT_VERSION: 1;
|
|
120
|
+
/** Content-addressed metadata for one static asset in a deployment artifact. */
|
|
121
|
+
type DeployAssetManifestEntry = {
|
|
122
|
+
blake3: string;
|
|
123
|
+
md5: string;
|
|
124
|
+
size: number;
|
|
125
|
+
/** @deprecated v1 alias of `blake3`. */
|
|
126
|
+
hash: string;
|
|
127
|
+
};
|
|
128
|
+
/** Target-neutral output of packaging a Void application for deployment. */
|
|
129
|
+
type DeployArtifact = Readonly<{
|
|
130
|
+
artifactVersion: typeof DEPLOY_ARTIFACT_VERSION;
|
|
131
|
+
manifest: Readonly<DeployManifest>;
|
|
132
|
+
assetManifest: Readonly<Record<string, Readonly<DeployAssetManifestEntry>>>;
|
|
133
|
+
/** Worker module bytes keyed by relative path, base64-encoded for transport. */
|
|
134
|
+
workerFiles?: Readonly<Record<string, string>>;
|
|
135
|
+
}>;
|
|
136
|
+
type DeploySourceMetadata = {
|
|
137
|
+
source: string | null;
|
|
138
|
+
commit: string | null;
|
|
139
|
+
repositoryUrl: string | null;
|
|
140
|
+
commitUrl: string | null;
|
|
141
|
+
gitRef: string | null;
|
|
142
|
+
};
|
|
143
|
+
/** JSON payload accepted by the managed deployment API. */
|
|
144
|
+
type DeployUploadPayload = {
|
|
145
|
+
manifest: DeployManifest;
|
|
146
|
+
assetManifest: Record<string, DeployAssetManifestEntry>;
|
|
147
|
+
workerFiles?: Record<string, string>;
|
|
148
|
+
} & DeploySourceMetadata & {
|
|
149
|
+
preflight: "true" | null;
|
|
150
|
+
};
|
|
151
|
+
/** Asset declaration accepted by the direct-upload URL endpoint. */
|
|
152
|
+
type DeployAssetUploadRequest = {
|
|
153
|
+
blake3: string;
|
|
154
|
+
md5: string;
|
|
155
|
+
size: number;
|
|
156
|
+
contentType: string;
|
|
157
|
+
};
|
|
158
|
+
/** Build one target-neutral artifact before either target adds provider state. */
|
|
159
|
+
export declare function createDeployArtifact(input: {
|
|
160
|
+
manifest: DeployManifest;
|
|
161
|
+
assetManifest: Record<string, DeployAssetManifestEntry>;
|
|
162
|
+
workerFiles?: Record<string, string>;
|
|
163
|
+
}): DeployArtifact;
|
|
164
|
+
/** Convert the canonical artifact into the stable hosted-platform wire shape. */
|
|
165
|
+
export declare function createDeployUploadPayload(artifact: DeployArtifact, metadata: DeploySourceMetadata & {
|
|
166
|
+
preflight: "true" | null;
|
|
167
|
+
}): DeployUploadPayload;
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/index.d.ts
|
|
170
|
+
export declare const SUPPORTED_MANIFEST_VERSIONS: ReadonlyArray<number>;
|
|
171
|
+
export type MigrationMeta = {
|
|
172
|
+
name: string;
|
|
173
|
+
destructive: boolean;
|
|
174
|
+
};
|
|
175
|
+
export type ScheduleMeta = {
|
|
176
|
+
name: string;
|
|
177
|
+
cron: string;
|
|
178
|
+
};
|
|
179
|
+
export type QueueMeta = {
|
|
180
|
+
name: string;
|
|
181
|
+
bindingName: string;
|
|
182
|
+
maxBatchSize?: number;
|
|
183
|
+
maxBatchTimeout?: number;
|
|
184
|
+
maxRetries?: number;
|
|
185
|
+
retryDelay?: number;
|
|
186
|
+
};
|
|
187
|
+
export type HeaderRule = {
|
|
188
|
+
pattern: string;
|
|
189
|
+
headers: Array<[string, string]>;
|
|
190
|
+
};
|
|
191
|
+
/** Public wire shape of a redirect, rewrite, or fallback rule. */
|
|
192
|
+
export type RedirectRule = {
|
|
193
|
+
source: string;
|
|
194
|
+
destination: string;
|
|
195
|
+
status: number;
|
|
196
|
+
/** Lowercase literal hostname. Path-only rules omit this field. */
|
|
197
|
+
host?: string;
|
|
198
|
+
};
|
|
199
|
+
export type AssetConfig = {
|
|
200
|
+
not_found_handling?: "none" | "single-page-application" | "404-page";
|
|
201
|
+
html_handling?: "auto-trailing-slash" | "force-trailing-slash" | "drop-trailing-slash" | "none";
|
|
202
|
+
/** Native Cloudflare Workers Assets routing; ignored by managed dispatch. */
|
|
203
|
+
run_worker_first?: boolean | Array<string>;
|
|
204
|
+
};
|
|
205
|
+
export type WebSocketRouteMeta = {
|
|
206
|
+
pattern: string;
|
|
207
|
+
filePath: string;
|
|
208
|
+
className: string;
|
|
209
|
+
bindingName?: string;
|
|
210
|
+
mode: "room" | "socket";
|
|
211
|
+
hasAuthGate: boolean;
|
|
212
|
+
};
|
|
213
|
+
export type WebSocketMigrationMeta = {
|
|
214
|
+
tag: string;
|
|
215
|
+
newClasses?: ReadonlyArray<string>;
|
|
216
|
+
newSqliteClasses?: ReadonlyArray<string>;
|
|
217
|
+
deletedClasses?: ReadonlyArray<string>;
|
|
218
|
+
};
|
|
219
|
+
export type DurableObjectMigrationStep = {
|
|
220
|
+
tag?: string;
|
|
221
|
+
new_classes?: ReadonlyArray<string>;
|
|
222
|
+
new_sqlite_classes?: ReadonlyArray<string>;
|
|
223
|
+
deleted_classes?: ReadonlyArray<string>;
|
|
224
|
+
renamed_classes?: ReadonlyArray<{
|
|
225
|
+
from: string;
|
|
226
|
+
to: string;
|
|
227
|
+
}>;
|
|
228
|
+
transferred_classes?: ReadonlyArray<{
|
|
229
|
+
from: string;
|
|
230
|
+
from_script: string;
|
|
231
|
+
to: string;
|
|
232
|
+
}>;
|
|
233
|
+
};
|
|
234
|
+
export type PlannedWebSocketMigrations = {
|
|
235
|
+
classes: ReadonlyArray<string>;
|
|
236
|
+
migrations: ReadonlyArray<WebSocketMigrationMeta>;
|
|
237
|
+
changed: boolean;
|
|
238
|
+
nextVersion: number;
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Preserve an existing WebSocket class history while assigning every genuinely
|
|
242
|
+
* new class to SQLite-backed Durable Object storage.
|
|
243
|
+
*/
|
|
244
|
+
export declare function planWebSocketMigrations(input: {
|
|
245
|
+
nextClasses: ReadonlyArray<string> | undefined;
|
|
246
|
+
previousClasses: ReadonlyArray<string> | undefined;
|
|
247
|
+
previousMigrations: ReadonlyArray<WebSocketMigrationMeta> | undefined;
|
|
248
|
+
currentVersion: number;
|
|
249
|
+
tagPrefix?: string;
|
|
250
|
+
}): PlannedWebSocketMigrations;
|
|
251
|
+
/** Append one SQLite class step without reclassifying an existing namespace. */
|
|
252
|
+
export declare function appendSqliteClassMigration(migrations: ReadonlyArray<DurableObjectMigrationStep>, classes: ReadonlyArray<string>, baseTag: string, knownExistingClasses?: ReadonlyArray<string>): {
|
|
253
|
+
migrations: ReadonlyArray<DurableObjectMigrationStep>;
|
|
254
|
+
added: boolean;
|
|
255
|
+
};
|
|
256
|
+
export type WebSocketManifestMeta = {
|
|
257
|
+
routes: ReadonlyArray<WebSocketRouteMeta>;
|
|
258
|
+
classes: ReadonlyArray<string>;
|
|
259
|
+
migrations?: ReadonlyArray<WebSocketMigrationMeta>;
|
|
260
|
+
};
|
|
261
|
+
export type SandboxMigrationMeta = {
|
|
262
|
+
tag: string;
|
|
263
|
+
newSqliteClasses?: ReadonlyArray<string>;
|
|
264
|
+
deletedClasses?: ReadonlyArray<string>;
|
|
265
|
+
};
|
|
266
|
+
export type SandboxManifestMeta = {
|
|
267
|
+
bindingName: string;
|
|
268
|
+
className: string;
|
|
269
|
+
containerName: string;
|
|
270
|
+
image: string;
|
|
271
|
+
instanceType?: string;
|
|
272
|
+
maxInstances?: number;
|
|
273
|
+
migrations?: ReadonlyArray<SandboxMigrationMeta>;
|
|
274
|
+
};
|
|
275
|
+
export type LiveMigrationMeta = {
|
|
276
|
+
tag: string;
|
|
277
|
+
newSqliteClasses?: ReadonlyArray<string>;
|
|
278
|
+
};
|
|
279
|
+
export type LiveManifestMeta = {
|
|
280
|
+
bindingName: string;
|
|
281
|
+
className: string;
|
|
282
|
+
storage: "sqlite";
|
|
283
|
+
classes: ReadonlyArray<string>;
|
|
284
|
+
migrations?: ReadonlyArray<LiveMigrationMeta>;
|
|
285
|
+
};
|
|
286
|
+
export type DeployManifest = {
|
|
287
|
+
version: number;
|
|
288
|
+
assetManifestVersion?: 1 | 2;
|
|
289
|
+
auth?: boolean;
|
|
290
|
+
bindings: {
|
|
291
|
+
d1?: Array<string>;
|
|
292
|
+
kv?: Array<string>;
|
|
293
|
+
r2?: Array<string>;
|
|
294
|
+
ai?: boolean;
|
|
295
|
+
hyperdrive?: Array<string>;
|
|
296
|
+
};
|
|
297
|
+
migrations?: {
|
|
298
|
+
dialect: "sqlite" | "postgresql" | "mysql";
|
|
299
|
+
pending: number;
|
|
300
|
+
protocolVersion?: 2;
|
|
301
|
+
classifierVersion?: number;
|
|
302
|
+
migrationListHash?: string;
|
|
303
|
+
stability?: "stable" | "legacy";
|
|
304
|
+
};
|
|
305
|
+
schedules?: Array<ScheduleMeta>;
|
|
306
|
+
queues?: Array<QueueMeta>;
|
|
307
|
+
ssr?: boolean;
|
|
308
|
+
framework?: string;
|
|
309
|
+
type?: "void" | "spa" | "static" | "framework";
|
|
310
|
+
/** @deprecated Legacy v1 static deployment discriminator. */
|
|
311
|
+
static?: "spa" | "ssg";
|
|
312
|
+
revalidate?: number | Record<string, number>;
|
|
313
|
+
revalidateQueryAllowlist?: Record<string, Array<string>>;
|
|
314
|
+
pages?: Array<{
|
|
315
|
+
pattern: string;
|
|
316
|
+
revalidate?: number;
|
|
317
|
+
}>;
|
|
318
|
+
pathRevalidate?: Record<string, number>;
|
|
319
|
+
prerender?: Array<string>;
|
|
320
|
+
assetConfig?: AssetConfig;
|
|
321
|
+
vars?: Record<string, string>;
|
|
322
|
+
workerMain?: string;
|
|
323
|
+
compatibilityDate?: string;
|
|
324
|
+
compatibilityFlags?: Array<string>;
|
|
325
|
+
limits?: {
|
|
326
|
+
cpuMs?: number;
|
|
327
|
+
};
|
|
328
|
+
hashedAssetsPrefix?: string;
|
|
329
|
+
headerRules?: Array<HeaderRule>;
|
|
330
|
+
redirectRules?: Array<RedirectRule>;
|
|
331
|
+
fallbackRules?: Array<RedirectRule>;
|
|
332
|
+
websocket?: WebSocketManifestMeta;
|
|
333
|
+
sandbox?: SandboxManifestMeta;
|
|
334
|
+
live?: LiveManifestMeta;
|
|
335
|
+
};
|
|
336
|
+
export type DeployEvent = {
|
|
337
|
+
event: "start";
|
|
338
|
+
deploymentId: string;
|
|
339
|
+
} | {
|
|
340
|
+
event: "heartbeat";
|
|
341
|
+
deploymentId?: string;
|
|
342
|
+
step: string;
|
|
343
|
+
} | {
|
|
344
|
+
event: "asset_upload";
|
|
345
|
+
total: number;
|
|
346
|
+
new: number;
|
|
347
|
+
cached: number;
|
|
348
|
+
} | {
|
|
349
|
+
event: "asset_upload_done";
|
|
350
|
+
} | {
|
|
351
|
+
event: "provisioning";
|
|
352
|
+
bindings: Array<string>;
|
|
353
|
+
} | {
|
|
354
|
+
event: "provisioning_done";
|
|
355
|
+
} | {
|
|
356
|
+
event: "worker_readiness";
|
|
357
|
+
} | {
|
|
358
|
+
event: "migration_start";
|
|
359
|
+
pending: number;
|
|
360
|
+
} | {
|
|
361
|
+
event: "migration";
|
|
362
|
+
name: string;
|
|
363
|
+
durationMs: number;
|
|
364
|
+
stability?: "stable" | "legacy";
|
|
365
|
+
} | {
|
|
366
|
+
event: "migration_warning";
|
|
367
|
+
message: string;
|
|
368
|
+
} | {
|
|
369
|
+
event: "worker_upload";
|
|
370
|
+
modules: number;
|
|
371
|
+
} | {
|
|
372
|
+
event: "worker_upload_done";
|
|
373
|
+
} | {
|
|
374
|
+
event: "queue_setup";
|
|
375
|
+
queues: Array<string>;
|
|
376
|
+
} | {
|
|
377
|
+
event: "queue_consumer_conflict";
|
|
378
|
+
queue: string;
|
|
379
|
+
ownerScript: string;
|
|
380
|
+
} | {
|
|
381
|
+
event: "prerender";
|
|
382
|
+
pages: number;
|
|
383
|
+
} | {
|
|
384
|
+
event: "prerender_done";
|
|
385
|
+
} | {
|
|
386
|
+
event: "done";
|
|
387
|
+
deployment: string;
|
|
388
|
+
url: string;
|
|
389
|
+
urls: Array<string>;
|
|
390
|
+
assets: number;
|
|
391
|
+
workers: number;
|
|
392
|
+
migrations?: number;
|
|
393
|
+
published?: boolean;
|
|
394
|
+
} | {
|
|
395
|
+
event: "error";
|
|
396
|
+
message: string;
|
|
397
|
+
step: string;
|
|
398
|
+
};
|
|
399
|
+
//#endregion
|
|
400
|
+
export type { DeployAppType, DeployArtifact, DeployAssetManifestEntry, DeployAssetUploadRequest, DeployCapabilitySupport, DeployIntent, DeployResource, DeployResourcePlan, DeployRuntimeFeatures, DeploySourceMetadata, DeployTarget, DeployTargetAdapter, DeployTargetCapabilities, DeployUploadPayload };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
//#region src/validate.ts
|
|
2
|
+
const MANIFEST_REDIRECT_RULE_STATUSES = /* @__PURE__ */ new Set([
|
|
3
|
+
200,
|
|
4
|
+
301,
|
|
5
|
+
302,
|
|
6
|
+
303,
|
|
7
|
+
307,
|
|
8
|
+
308
|
|
9
|
+
]);
|
|
10
|
+
const MAX_ROUTING_RULES = 2e3;
|
|
11
|
+
const MAX_REVALIDATE_QUERY_ALLOWLIST_ENTRIES = MAX_ROUTING_RULES;
|
|
12
|
+
const MAX_PAGES = 5e3;
|
|
13
|
+
/**
|
|
14
|
+
* Strict literal-hostname regex used to validate `redirectRules[].host` /
|
|
15
|
+
* `fallbackRules[].host` on the deploy manifest. Mirrors the regex in
|
|
16
|
+
* `routes/domains.ts` (custom-domain create endpoint) — the source of truth
|
|
17
|
+
* for what counts as a valid project-owned hostname. Rejects scheme prefixes,
|
|
18
|
+
* ports, paths, whitespace, control chars, wildcards, single-label hosts,
|
|
19
|
+
* IPv4 literals, and userinfo. Hostnames are lowercased before testing so
|
|
20
|
+
* the same byte-exact form is what dispatch compares against `url.hostname`
|
|
21
|
+
* (also lowercase per WHATWG).
|
|
22
|
+
*/
|
|
23
|
+
const LITERAL_HOSTNAME_REGEX = /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/;
|
|
24
|
+
/**
|
|
25
|
+
* Validate and lowercase-normalize an optional `host` field on a redirect or
|
|
26
|
+
* fallback rule. Mutates `ruleObject.host` in place so the persisted manifest carries
|
|
27
|
+
* the canonical form. Returns an error message string on failure, or `null`
|
|
28
|
+
* on success (including the absent-field case).
|
|
29
|
+
*/
|
|
30
|
+
function validateRuleHost(ruleObject, ruleKind, sourceRef) {
|
|
31
|
+
if (ruleObject.host === void 0) return null;
|
|
32
|
+
if (typeof ruleObject.host !== "string") return `invalid manifest shape: ${ruleKind} entry ${sourceRef} host must be a string`;
|
|
33
|
+
const normalized = ruleObject.host.toLowerCase();
|
|
34
|
+
if (!LITERAL_HOSTNAME_REGEX.test(normalized)) return `invalid manifest shape: ${ruleKind} entry ${sourceRef} host must be a literal hostname (e.g. "www.example.com"; no scheme, port, path, or wildcards): ${JSON.stringify(ruleObject.host)}`;
|
|
35
|
+
ruleObject.host = normalized;
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function validateManifestShape(parsed) {
|
|
39
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) return "invalid manifest shape: expected a JSON object";
|
|
40
|
+
const manifest = parsed;
|
|
41
|
+
if (manifest.bindings == null || typeof manifest.bindings !== "object" || Array.isArray(manifest.bindings)) return "invalid manifest shape: bindings must be an object";
|
|
42
|
+
const bindings = manifest.bindings;
|
|
43
|
+
for (const key of [
|
|
44
|
+
"d1",
|
|
45
|
+
"kv",
|
|
46
|
+
"r2",
|
|
47
|
+
"hyperdrive"
|
|
48
|
+
]) {
|
|
49
|
+
const bindingValue = bindings[key];
|
|
50
|
+
if (bindingValue === void 0) continue;
|
|
51
|
+
if (!Array.isArray(bindingValue) || !bindingValue.every((value) => typeof value === "string")) return `invalid manifest shape: bindings.${key} must be an array of strings`;
|
|
52
|
+
}
|
|
53
|
+
if (manifest.queues !== void 0) {
|
|
54
|
+
if (!Array.isArray(manifest.queues)) return "invalid manifest shape: queues must be an array";
|
|
55
|
+
for (const queue of manifest.queues) {
|
|
56
|
+
if (queue == null || typeof queue !== "object" || Array.isArray(queue)) return "invalid manifest shape: queues entries must be objects";
|
|
57
|
+
const queueObject = queue;
|
|
58
|
+
if (typeof queueObject.name !== "string" || typeof queueObject.bindingName !== "string") return "invalid manifest shape: queues entries must have string name and bindingName";
|
|
59
|
+
for (const tuning of [
|
|
60
|
+
"maxBatchSize",
|
|
61
|
+
"maxBatchTimeout",
|
|
62
|
+
"maxRetries",
|
|
63
|
+
"retryDelay"
|
|
64
|
+
]) {
|
|
65
|
+
const tuningValue = queueObject[tuning];
|
|
66
|
+
if (tuningValue === void 0) continue;
|
|
67
|
+
if (typeof tuningValue !== "number" || !Number.isFinite(tuningValue)) return `invalid manifest shape: queues entry ${tuning} must be a finite number`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (manifest.schedules !== void 0) {
|
|
72
|
+
if (!Array.isArray(manifest.schedules)) return "invalid manifest shape: schedules must be an array";
|
|
73
|
+
for (const schedule of manifest.schedules) {
|
|
74
|
+
if (schedule == null || typeof schedule !== "object" || Array.isArray(schedule)) return "invalid manifest shape: schedules entries must be objects";
|
|
75
|
+
const scheduleObject = schedule;
|
|
76
|
+
if (typeof scheduleObject.name !== "string" || typeof scheduleObject.cron !== "string") return "invalid manifest shape: schedules entries must have string name and cron";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (manifest.prerender !== void 0) {
|
|
80
|
+
if (!Array.isArray(manifest.prerender) || !manifest.prerender.every((x) => typeof x === "string")) return "invalid manifest shape: prerender must be an array of strings";
|
|
81
|
+
}
|
|
82
|
+
if (manifest.vars !== void 0) {
|
|
83
|
+
if (manifest.vars == null || typeof manifest.vars !== "object" || Array.isArray(manifest.vars)) return "invalid manifest shape: vars must be an object";
|
|
84
|
+
for (const [k, v] of Object.entries(manifest.vars)) if (typeof v !== "string") return `invalid manifest shape: vars.${k} must be a string`;
|
|
85
|
+
}
|
|
86
|
+
if (manifest.migrations !== void 0) {
|
|
87
|
+
if (manifest.migrations == null || typeof manifest.migrations !== "object" || Array.isArray(manifest.migrations)) return "invalid manifest shape: migrations must be an object";
|
|
88
|
+
const mig = manifest.migrations;
|
|
89
|
+
if (mig.dialect !== void 0) {
|
|
90
|
+
const allowed = [
|
|
91
|
+
"sqlite",
|
|
92
|
+
"postgresql",
|
|
93
|
+
"mysql"
|
|
94
|
+
];
|
|
95
|
+
if (typeof mig.dialect !== "string" || !allowed.includes(mig.dialect)) return `invalid manifest shape: migrations.dialect must be one of ${allowed.join(", ")}`;
|
|
96
|
+
}
|
|
97
|
+
if (mig.pending !== void 0) {
|
|
98
|
+
if (typeof mig.pending !== "number" || !Number.isInteger(mig.pending) || mig.pending < 0) return "invalid manifest shape: migrations.pending must be a non-negative integer";
|
|
99
|
+
}
|
|
100
|
+
if (mig.protocolVersion !== void 0 && mig.protocolVersion !== 2) return "invalid manifest shape: migrations.protocolVersion must be 2";
|
|
101
|
+
if (mig.classifierVersion !== void 0 && (typeof mig.classifierVersion !== "number" || !Number.isInteger(mig.classifierVersion))) return "invalid manifest shape: migrations.classifierVersion must be an integer";
|
|
102
|
+
if (mig.migrationListHash !== void 0 && typeof mig.migrationListHash !== "string") return "invalid manifest shape: migrations.migrationListHash must be a string";
|
|
103
|
+
if (mig.stability !== void 0 && mig.stability !== "stable" && mig.stability !== "legacy") return "invalid manifest shape: migrations.stability must be stable or legacy";
|
|
104
|
+
}
|
|
105
|
+
if (manifest.auth !== void 0 && typeof manifest.auth !== "boolean") return "invalid manifest shape: auth must be a boolean";
|
|
106
|
+
if (manifest.ssr !== void 0 && typeof manifest.ssr !== "boolean") return "invalid manifest shape: ssr must be a boolean";
|
|
107
|
+
for (const key of [
|
|
108
|
+
"workerMain",
|
|
109
|
+
"compatibilityDate",
|
|
110
|
+
"hashedAssetsPrefix",
|
|
111
|
+
"framework"
|
|
112
|
+
]) if (manifest[key] !== void 0 && typeof manifest[key] !== "string") return `invalid manifest shape: ${key} must be a string`;
|
|
113
|
+
if (manifest.assetManifestVersion !== void 0) {
|
|
114
|
+
if (manifest.assetManifestVersion !== 1 && manifest.assetManifestVersion !== 2) return "invalid manifest shape: assetManifestVersion must be 1 or 2";
|
|
115
|
+
}
|
|
116
|
+
if (manifest.compatibilityFlags !== void 0) {
|
|
117
|
+
if (!Array.isArray(manifest.compatibilityFlags) || !manifest.compatibilityFlags.every((x) => typeof x === "string")) return "invalid manifest shape: compatibilityFlags must be an array of strings";
|
|
118
|
+
}
|
|
119
|
+
if (manifest.limits !== void 0) {
|
|
120
|
+
if (manifest.limits == null || typeof manifest.limits !== "object" || Array.isArray(manifest.limits)) return "invalid manifest shape: limits must be an object";
|
|
121
|
+
const limits = manifest.limits;
|
|
122
|
+
if (limits.cpuMs !== void 0) {
|
|
123
|
+
if (typeof limits.cpuMs !== "number" || !Number.isSafeInteger(limits.cpuMs) || limits.cpuMs < 1) return "invalid manifest shape: limits.cpuMs must be a positive integer";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (manifest.type !== void 0) {
|
|
127
|
+
const allowed = [
|
|
128
|
+
"void",
|
|
129
|
+
"spa",
|
|
130
|
+
"static",
|
|
131
|
+
"framework"
|
|
132
|
+
];
|
|
133
|
+
if (typeof manifest.type !== "string" || !allowed.includes(manifest.type)) return `invalid manifest shape: type must be one of ${allowed.join(", ")}`;
|
|
134
|
+
}
|
|
135
|
+
if (manifest.assetConfig !== void 0) {
|
|
136
|
+
if (manifest.assetConfig == null || typeof manifest.assetConfig !== "object" || Array.isArray(manifest.assetConfig)) return "invalid manifest shape: assetConfig must be an object";
|
|
137
|
+
const assetConfig = manifest.assetConfig;
|
|
138
|
+
if (assetConfig.not_found_handling !== void 0 && ![
|
|
139
|
+
"none",
|
|
140
|
+
"single-page-application",
|
|
141
|
+
"404-page"
|
|
142
|
+
].includes(assetConfig.not_found_handling)) return "invalid manifest shape: assetConfig.not_found_handling must be one of none, single-page-application, 404-page";
|
|
143
|
+
if (assetConfig.html_handling !== void 0 && ![
|
|
144
|
+
"auto-trailing-slash",
|
|
145
|
+
"force-trailing-slash",
|
|
146
|
+
"drop-trailing-slash",
|
|
147
|
+
"none"
|
|
148
|
+
].includes(assetConfig.html_handling)) return "invalid manifest shape: assetConfig.html_handling must be one of auto-trailing-slash, force-trailing-slash, drop-trailing-slash, none";
|
|
149
|
+
}
|
|
150
|
+
if (manifest.revalidate !== void 0) {
|
|
151
|
+
if (typeof manifest.revalidate === "number") {} else if (manifest.revalidate == null || typeof manifest.revalidate !== "object" || Array.isArray(manifest.revalidate)) return "invalid manifest shape: revalidate must be a number or object";
|
|
152
|
+
else for (const [pathname, value] of Object.entries(manifest.revalidate)) if (typeof value !== "number") return `invalid manifest shape: revalidate.${pathname} must be a number`;
|
|
153
|
+
}
|
|
154
|
+
if (manifest.headerRules !== void 0) {
|
|
155
|
+
if (!Array.isArray(manifest.headerRules)) return "invalid manifest shape: headerRules must be an array";
|
|
156
|
+
for (const rule of manifest.headerRules) {
|
|
157
|
+
if (rule == null || typeof rule !== "object" || Array.isArray(rule)) return "invalid manifest shape: headerRules entries must be objects";
|
|
158
|
+
const ruleObject = rule;
|
|
159
|
+
if (typeof ruleObject.pattern !== "string") return "invalid manifest shape: headerRules entries must have a string pattern";
|
|
160
|
+
if (!Array.isArray(ruleObject.headers)) return "invalid manifest shape: headerRules entries must have a headers array";
|
|
161
|
+
for (const pair of ruleObject.headers) if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string" || typeof pair[1] !== "string") return "invalid manifest shape: headerRules headers entries must be [name, value] string pairs";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (manifest.redirectRules !== void 0) {
|
|
165
|
+
if (!Array.isArray(manifest.redirectRules)) return "invalid manifest shape: redirectRules must be an array";
|
|
166
|
+
if (manifest.redirectRules.length > 2e3) return `invalid manifest shape: redirectRules has ${manifest.redirectRules.length} entries, exceeds the ${MAX_ROUTING_RULES} per-list limit`;
|
|
167
|
+
for (const rule of manifest.redirectRules) {
|
|
168
|
+
if (rule == null || typeof rule !== "object" || Array.isArray(rule)) return "invalid manifest shape: redirectRules entries must be objects";
|
|
169
|
+
const ruleObject = rule;
|
|
170
|
+
if (typeof ruleObject.source !== "string" || typeof ruleObject.destination !== "string" || typeof ruleObject.status !== "number") return "invalid manifest shape: redirectRules entries must have string source, string destination, and numeric status";
|
|
171
|
+
const sourceRef = JSON.stringify(ruleObject.source);
|
|
172
|
+
if (!MANIFEST_REDIRECT_RULE_STATUSES.has(ruleObject.status)) return `invalid manifest shape: redirectRules entry ${sourceRef} status must be one of ${[...MANIFEST_REDIRECT_RULE_STATUSES].join(", ")} (200 = rewrite, 3xx = redirect)`;
|
|
173
|
+
const destination = ruleObject.destination;
|
|
174
|
+
const isRewrite = ruleObject.status === 200;
|
|
175
|
+
if (isRewrite && destination.includes("?")) return `invalid manifest shape: redirectRules entry ${sourceRef} destination must not contain a query string when status is 200`;
|
|
176
|
+
if (isRewrite && !destination.startsWith("/")) return `invalid manifest shape: redirectRules entry ${sourceRef} destination must be path-absolute (start with "/") when status is 200`;
|
|
177
|
+
if (isRewrite && destination.startsWith("//")) return `invalid manifest shape: redirectRules entry ${sourceRef} destination must not start with "//" (protocol-relative URLs parse cross-origin; use a single-slash path-absolute destination)`;
|
|
178
|
+
const hostError = validateRuleHost(ruleObject, "redirectRules", sourceRef);
|
|
179
|
+
if (hostError) return hostError;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (manifest.revalidateQueryAllowlist !== void 0) {
|
|
183
|
+
if (manifest.revalidateQueryAllowlist == null || typeof manifest.revalidateQueryAllowlist !== "object" || Array.isArray(manifest.revalidateQueryAllowlist)) return "invalid manifest shape: revalidateQueryAllowlist must be an object";
|
|
184
|
+
const entries = Object.entries(manifest.revalidateQueryAllowlist);
|
|
185
|
+
if (entries.length > 2e3) return `invalid manifest shape: revalidateQueryAllowlist has ${entries.length} entries, exceeds the ${MAX_REVALIDATE_QUERY_ALLOWLIST_ENTRIES} entry limit`;
|
|
186
|
+
for (const [pattern, value] of entries) {
|
|
187
|
+
if (pattern === "") return "invalid manifest shape: revalidateQueryAllowlist must not include an empty pattern";
|
|
188
|
+
if (!Array.isArray(value)) return `invalid manifest shape: revalidateQueryAllowlist entry ${JSON.stringify(pattern)} must be an array of strings`;
|
|
189
|
+
for (const param of value) if (typeof param !== "string") return `invalid manifest shape: revalidateQueryAllowlist entry ${JSON.stringify(pattern)} must contain only strings`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (manifest.pages !== void 0) {
|
|
193
|
+
if (!Array.isArray(manifest.pages)) return "invalid manifest shape: pages must be an array";
|
|
194
|
+
if (manifest.pages.length > MAX_PAGES) return `invalid manifest shape: pages has ${manifest.pages.length} entries, exceeds the ${MAX_PAGES} entry limit`;
|
|
195
|
+
for (const entry of manifest.pages) {
|
|
196
|
+
if (entry == null || typeof entry !== "object" || Array.isArray(entry)) return "invalid manifest shape: each pages entry must be an object";
|
|
197
|
+
const e = entry;
|
|
198
|
+
if (typeof e.pattern !== "string" || e.pattern === "") return "invalid manifest shape: pages entry pattern must be a string";
|
|
199
|
+
if (e.revalidate !== void 0 && typeof e.revalidate !== "number") return "invalid manifest shape: pages entry revalidate must be a number";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (manifest.pathRevalidate !== void 0) {
|
|
203
|
+
if (manifest.pathRevalidate == null || typeof manifest.pathRevalidate !== "object" || Array.isArray(manifest.pathRevalidate)) return "invalid manifest shape: pathRevalidate must be an object";
|
|
204
|
+
for (const [pathname, value] of Object.entries(manifest.pathRevalidate)) if (typeof value !== "number") return `invalid manifest shape: pathRevalidate.${pathname} must be a number`;
|
|
205
|
+
}
|
|
206
|
+
if (manifest.fallbackRules !== void 0) {
|
|
207
|
+
if (!Array.isArray(manifest.fallbackRules)) return "invalid manifest shape: fallbackRules must be an array";
|
|
208
|
+
if (manifest.fallbackRules.length > 2e3) return `invalid manifest shape: fallbackRules has ${manifest.fallbackRules.length} entries, exceeds the ${MAX_ROUTING_RULES} per-list limit`;
|
|
209
|
+
for (const rule of manifest.fallbackRules) {
|
|
210
|
+
if (rule == null || typeof rule !== "object" || Array.isArray(rule)) return "invalid manifest shape: fallbackRules entries must be objects";
|
|
211
|
+
const ruleObject = rule;
|
|
212
|
+
if (typeof ruleObject.source !== "string" || typeof ruleObject.destination !== "string" || typeof ruleObject.status !== "number") return "invalid manifest shape: fallbackRules entries must have string source, string destination, and numeric status";
|
|
213
|
+
const sourceRef = JSON.stringify(ruleObject.source);
|
|
214
|
+
if (ruleObject.status !== 200) return `invalid manifest shape: fallbackRules entry ${sourceRef} status must be 200 (fallbacks are rewrites only)`;
|
|
215
|
+
const destination = ruleObject.destination;
|
|
216
|
+
if (destination.includes("?")) return `invalid manifest shape: fallbackRules entry ${sourceRef} destination must not contain a query string`;
|
|
217
|
+
if (!destination.startsWith("/")) return `invalid manifest shape: fallbackRules entry ${sourceRef} destination must be path-absolute (start with "/")`;
|
|
218
|
+
if (destination.startsWith("//")) return `invalid manifest shape: fallbackRules entry ${sourceRef} destination must not start with "//" (protocol-relative URLs parse cross-origin; use a single-slash path-absolute destination)`;
|
|
219
|
+
const hostError = validateRuleHost(ruleObject, "fallbackRules", sourceRef);
|
|
220
|
+
if (hostError) return hostError;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (manifest.websocket !== void 0) {
|
|
224
|
+
if (manifest.websocket == null || typeof manifest.websocket !== "object" || Array.isArray(manifest.websocket)) return "invalid manifest shape: websocket must be an object";
|
|
225
|
+
const websocket = manifest.websocket;
|
|
226
|
+
if (!Array.isArray(websocket.routes)) return "invalid manifest shape: websocket.routes must be an array";
|
|
227
|
+
for (const route of websocket.routes) {
|
|
228
|
+
if (route == null || typeof route !== "object" || Array.isArray(route)) return "invalid manifest shape: websocket.routes entries must be objects";
|
|
229
|
+
const routeObject = route;
|
|
230
|
+
if (typeof routeObject.className !== "string") return "invalid manifest shape: websocket.routes entries must have a string className";
|
|
231
|
+
if (routeObject.bindingName !== void 0 && typeof routeObject.bindingName !== "string") return "invalid manifest shape: websocket.routes entry bindingName must be a string";
|
|
232
|
+
}
|
|
233
|
+
if (!Array.isArray(websocket.classes) || !websocket.classes.every((x) => typeof x === "string")) return "invalid manifest shape: websocket.classes must be an array of strings";
|
|
234
|
+
if (websocket.migrations !== void 0) {
|
|
235
|
+
if (!Array.isArray(websocket.migrations)) return "invalid manifest shape: websocket.migrations must be an array";
|
|
236
|
+
for (const migration of websocket.migrations) {
|
|
237
|
+
if (migration == null || typeof migration !== "object" || Array.isArray(migration)) return "invalid manifest shape: websocket.migrations entries must be objects";
|
|
238
|
+
const migrationObject = migration;
|
|
239
|
+
if (typeof migrationObject.tag !== "string" || migrationObject.tag.length === 0) return "invalid manifest shape: websocket.migrations entries must have a non-empty string tag";
|
|
240
|
+
for (const field of [
|
|
241
|
+
"newClasses",
|
|
242
|
+
"newSqliteClasses",
|
|
243
|
+
"deletedClasses"
|
|
244
|
+
]) {
|
|
245
|
+
const value = migrationObject[field];
|
|
246
|
+
if (value !== void 0 && (!Array.isArray(value) || !value.every((className) => typeof className === "string"))) return `invalid manifest shape: websocket.migrations ${field} must be an array of strings`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (manifest.sandbox !== void 0) {
|
|
252
|
+
if (manifest.type === "static" || manifest.type === "spa") return "sandbox is not supported for static/spa deployments";
|
|
253
|
+
if (manifest.sandbox == null || typeof manifest.sandbox !== "object" || Array.isArray(manifest.sandbox)) return "invalid manifest shape: sandbox must be an object";
|
|
254
|
+
const sandbox = manifest.sandbox;
|
|
255
|
+
for (const key of [
|
|
256
|
+
"bindingName",
|
|
257
|
+
"className",
|
|
258
|
+
"containerName",
|
|
259
|
+
"image"
|
|
260
|
+
]) if (typeof sandbox[key] !== "string" || sandbox[key].length === 0) return `invalid manifest shape: sandbox.${key} must be a non-empty string`;
|
|
261
|
+
if (isLocalImageReference(sandbox.image)) return "invalid manifest shape: sandbox.image must be a registry image reference";
|
|
262
|
+
if (sandbox.instanceType !== void 0 && typeof sandbox.instanceType !== "string") return "invalid manifest shape: sandbox.instanceType must be a string";
|
|
263
|
+
if (sandbox.maxInstances !== void 0 && (typeof sandbox.maxInstances !== "number" || !Number.isInteger(sandbox.maxInstances) || sandbox.maxInstances < 1)) return "invalid manifest shape: sandbox.maxInstances must be a positive integer";
|
|
264
|
+
if (sandbox.migrations !== void 0) {
|
|
265
|
+
if (!Array.isArray(sandbox.migrations)) return "invalid manifest shape: sandbox.migrations must be an array";
|
|
266
|
+
for (const migration of sandbox.migrations) {
|
|
267
|
+
if (migration == null || typeof migration !== "object" || Array.isArray(migration)) return "invalid manifest shape: sandbox.migrations entries must be objects";
|
|
268
|
+
const migrationObject = migration;
|
|
269
|
+
if (typeof migrationObject.tag !== "string") return "invalid manifest shape: sandbox.migrations entries must have a string tag";
|
|
270
|
+
if (migrationObject.newSqliteClasses !== void 0 && (!Array.isArray(migrationObject.newSqliteClasses) || !migrationObject.newSqliteClasses.every((value) => typeof value === "string"))) return "invalid manifest shape: sandbox.migrations newSqliteClasses must be an array of strings";
|
|
271
|
+
if (migrationObject.deletedClasses !== void 0 && (!Array.isArray(migrationObject.deletedClasses) || !migrationObject.deletedClasses.every((value) => typeof value === "string"))) return "invalid manifest shape: sandbox.migrations deletedClasses must be an array of strings";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (manifest.live !== void 0) {
|
|
276
|
+
if (manifest.live == null || typeof manifest.live !== "object" || Array.isArray(manifest.live)) return "invalid manifest shape: live must be an object";
|
|
277
|
+
const live = manifest.live;
|
|
278
|
+
for (const key of ["bindingName", "className"]) if (typeof live[key] !== "string" || live[key].length === 0) return `invalid manifest shape: live.${key} must be a non-empty string`;
|
|
279
|
+
if (live.storage !== "sqlite") return "invalid manifest shape: live.storage must be \"sqlite\"";
|
|
280
|
+
if (!Array.isArray(live.classes) || !live.classes.every((x) => typeof x === "string")) return "invalid manifest shape: live.classes must be an array of strings";
|
|
281
|
+
if (live.migrations !== void 0) {
|
|
282
|
+
if (!Array.isArray(live.migrations)) return "invalid manifest shape: live.migrations must be an array";
|
|
283
|
+
for (const migration of live.migrations) {
|
|
284
|
+
if (migration == null || typeof migration !== "object" || Array.isArray(migration)) return "invalid manifest shape: live.migrations entries must be objects";
|
|
285
|
+
const migrationObject = migration;
|
|
286
|
+
if (typeof migrationObject.tag !== "string") return "invalid manifest shape: live.migrations entries must have a string tag";
|
|
287
|
+
if (migrationObject.newSqliteClasses !== void 0 && (!Array.isArray(migrationObject.newSqliteClasses) || !migrationObject.newSqliteClasses.every((value) => typeof value === "string"))) return "invalid manifest shape: live.migrations newSqliteClasses must be an array of strings";
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function isLocalImageReference(image) {
|
|
294
|
+
const lowerImage = image.toLowerCase();
|
|
295
|
+
return image.startsWith(".") || image.startsWith("/") || lowerImage === "dockerfile" || lowerImage.includes("dockerfile") || lowerImage.endsWith("/dockerfile") || lowerImage.endsWith("\\dockerfile");
|
|
296
|
+
}
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/resource-plan.ts
|
|
299
|
+
/**
|
|
300
|
+
* Convert a deployment manifest into provider-neutral resource intent.
|
|
301
|
+
*
|
|
302
|
+
* Resource names, remote ids, and injected platform bindings belong to the
|
|
303
|
+
* target adapter and are deliberately absent from this plan.
|
|
304
|
+
*/
|
|
305
|
+
function createDeployResourcePlan(manifest) {
|
|
306
|
+
const resources = [];
|
|
307
|
+
for (const bindingName of manifest.bindings.d1 ?? []) resources.push({
|
|
308
|
+
type: "d1",
|
|
309
|
+
bindingName
|
|
310
|
+
});
|
|
311
|
+
for (const bindingName of manifest.bindings.kv ?? []) resources.push({
|
|
312
|
+
type: "kv",
|
|
313
|
+
bindingName
|
|
314
|
+
});
|
|
315
|
+
for (const bindingName of manifest.bindings.r2 ?? []) resources.push({
|
|
316
|
+
type: "r2",
|
|
317
|
+
bindingName
|
|
318
|
+
});
|
|
319
|
+
for (const consumer of manifest.queues ?? []) resources.push({
|
|
320
|
+
type: "queue",
|
|
321
|
+
bindingName: consumer.bindingName,
|
|
322
|
+
resourceName: consumer.name,
|
|
323
|
+
consumer
|
|
324
|
+
});
|
|
325
|
+
for (const bindingName of manifest.bindings.hyperdrive ?? []) resources.push({
|
|
326
|
+
type: "hyperdrive",
|
|
327
|
+
bindingName
|
|
328
|
+
});
|
|
329
|
+
return {
|
|
330
|
+
resources,
|
|
331
|
+
runtime: {
|
|
332
|
+
ai: manifest.bindings.ai === true,
|
|
333
|
+
schedules: manifest.schedules ?? [],
|
|
334
|
+
websocket: manifest.websocket ?? null,
|
|
335
|
+
sandbox: manifest.sandbox ?? null,
|
|
336
|
+
live: manifest.live ?? null
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/intent.ts
|
|
342
|
+
/**
|
|
343
|
+
* Interpret a manifest once before a target assigns provider names and ids.
|
|
344
|
+
* This function performs no I/O and does not mutate the manifest.
|
|
345
|
+
*/
|
|
346
|
+
function createDeployIntent(manifest) {
|
|
347
|
+
const appType = manifest.type ?? (manifest.framework ? "framework" : "void");
|
|
348
|
+
const webSockets = (manifest.websocket?.classes.length ?? 0) > 0;
|
|
349
|
+
const live = manifest.live !== void 0;
|
|
350
|
+
const sandbox = manifest.sandbox !== void 0;
|
|
351
|
+
return {
|
|
352
|
+
appType,
|
|
353
|
+
hasWorker: appType === "void" || appType === "framework",
|
|
354
|
+
manifest,
|
|
355
|
+
resourcePlan: createDeployResourcePlan(manifest),
|
|
356
|
+
features: {
|
|
357
|
+
assets: manifest.assetConfig !== void 0,
|
|
358
|
+
auth: manifest.auth === true,
|
|
359
|
+
cron: (manifest.schedules?.length ?? 0) > 0,
|
|
360
|
+
queues: (manifest.queues?.length ?? 0) > 0,
|
|
361
|
+
migrations: (manifest.migrations?.pending ?? 0) > 0,
|
|
362
|
+
ai: manifest.bindings.ai === true,
|
|
363
|
+
durableObjects: webSockets || live || sandbox,
|
|
364
|
+
webSockets,
|
|
365
|
+
live,
|
|
366
|
+
sandbox,
|
|
367
|
+
isr: manifest.revalidate !== void 0 || manifest.pathRevalidate !== void 0 || (manifest.pages?.some((page) => page.revalidate !== void 0) ?? false),
|
|
368
|
+
prerender: (manifest.prerender?.length ?? 0) > 0
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/artifact.ts
|
|
374
|
+
const DEPLOY_ARTIFACT_VERSION = 1;
|
|
375
|
+
/** Build one target-neutral artifact before either target adds provider state. */
|
|
376
|
+
function createDeployArtifact(input) {
|
|
377
|
+
return {
|
|
378
|
+
artifactVersion: 1,
|
|
379
|
+
manifest: input.manifest,
|
|
380
|
+
assetManifest: input.assetManifest,
|
|
381
|
+
...input.workerFiles && Object.keys(input.workerFiles).length > 0 ? { workerFiles: input.workerFiles } : {}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/** Convert the canonical artifact into the stable hosted-platform wire shape. */
|
|
385
|
+
function createDeployUploadPayload(artifact, metadata) {
|
|
386
|
+
return {
|
|
387
|
+
manifest: {
|
|
388
|
+
...artifact.manifest,
|
|
389
|
+
bindings: { ...artifact.manifest.bindings }
|
|
390
|
+
},
|
|
391
|
+
assetManifest: { ...artifact.assetManifest },
|
|
392
|
+
...artifact.workerFiles ? { workerFiles: { ...artifact.workerFiles } } : {},
|
|
393
|
+
...metadata
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/index.ts
|
|
398
|
+
const SUPPORTED_MANIFEST_VERSIONS = [3];
|
|
399
|
+
/**
|
|
400
|
+
* Preserve an existing WebSocket class history while assigning every genuinely
|
|
401
|
+
* new class to SQLite-backed Durable Object storage.
|
|
402
|
+
*/
|
|
403
|
+
function planWebSocketMigrations(input) {
|
|
404
|
+
const nextClasses = sortUniqueStrings(input.nextClasses);
|
|
405
|
+
const previousClasses = sortUniqueStrings(input.previousClasses);
|
|
406
|
+
const previousMigrations = input.previousMigrations ?? [];
|
|
407
|
+
if (!!sameStringSet(nextClasses, previousClasses)) return {
|
|
408
|
+
classes: nextClasses,
|
|
409
|
+
migrations: previousMigrations,
|
|
410
|
+
changed: false,
|
|
411
|
+
nextVersion: input.currentVersion
|
|
412
|
+
};
|
|
413
|
+
const existingClasses = new Set(previousClasses);
|
|
414
|
+
for (const migration of previousMigrations) {
|
|
415
|
+
for (const name of migration.newClasses ?? []) existingClasses.add(name);
|
|
416
|
+
for (const name of migration.newSqliteClasses ?? []) existingClasses.add(name);
|
|
417
|
+
for (const name of migration.deletedClasses ?? []) existingClasses.delete(name);
|
|
418
|
+
}
|
|
419
|
+
const additions = nextClasses.filter((name) => !existingClasses.has(name));
|
|
420
|
+
if (additions.length === 0) return {
|
|
421
|
+
classes: nextClasses,
|
|
422
|
+
migrations: previousMigrations,
|
|
423
|
+
changed: true,
|
|
424
|
+
nextVersion: input.currentVersion
|
|
425
|
+
};
|
|
426
|
+
const nextVersion = input.currentVersion + 1;
|
|
427
|
+
return {
|
|
428
|
+
classes: nextClasses,
|
|
429
|
+
migrations: [...previousMigrations, {
|
|
430
|
+
tag: `${input.tagPrefix ?? "ws"}-v${nextVersion}`,
|
|
431
|
+
newSqliteClasses: additions
|
|
432
|
+
}],
|
|
433
|
+
changed: true,
|
|
434
|
+
nextVersion
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/** Append one SQLite class step without reclassifying an existing namespace. */
|
|
438
|
+
function appendSqliteClassMigration(migrations, classes, baseTag, knownExistingClasses = []) {
|
|
439
|
+
const existingClasses = new Set(knownExistingClasses);
|
|
440
|
+
for (const migration of migrations) {
|
|
441
|
+
for (const name of migration.new_classes ?? []) existingClasses.add(name);
|
|
442
|
+
for (const name of migration.new_sqlite_classes ?? []) existingClasses.add(name);
|
|
443
|
+
for (const rename of migration.renamed_classes ?? []) {
|
|
444
|
+
existingClasses.delete(rename.from);
|
|
445
|
+
existingClasses.add(rename.to);
|
|
446
|
+
}
|
|
447
|
+
for (const transfer of migration.transferred_classes ?? []) existingClasses.add(transfer.to);
|
|
448
|
+
for (const name of migration.deleted_classes ?? []) existingClasses.delete(name);
|
|
449
|
+
}
|
|
450
|
+
const additions = sortUniqueStrings(classes).filter((name) => !existingClasses.has(name));
|
|
451
|
+
if (additions.length === 0) return {
|
|
452
|
+
migrations,
|
|
453
|
+
added: false
|
|
454
|
+
};
|
|
455
|
+
const tags = new Set(migrations.map((migration) => migration.tag).filter(Boolean));
|
|
456
|
+
let tag = baseTag;
|
|
457
|
+
for (let suffix = 2; tags.has(tag); suffix++) tag = `${baseTag}-${suffix}`;
|
|
458
|
+
return {
|
|
459
|
+
migrations: [...migrations, {
|
|
460
|
+
tag,
|
|
461
|
+
new_sqlite_classes: additions
|
|
462
|
+
}],
|
|
463
|
+
added: true
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function sortUniqueStrings(values) {
|
|
467
|
+
return [...new Set(values ?? [])].sort((a, b) => a.localeCompare(b));
|
|
468
|
+
}
|
|
469
|
+
function sameStringSet(a, b) {
|
|
470
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
471
|
+
}
|
|
472
|
+
//#endregion
|
|
473
|
+
export { DEPLOY_ARTIFACT_VERSION, MANIFEST_REDIRECT_RULE_STATUSES, MAX_REVALIDATE_QUERY_ALLOWLIST_ENTRIES, MAX_ROUTING_RULES, SUPPORTED_MANIFEST_VERSIONS, appendSqliteClassMigration, createDeployArtifact, createDeployIntent, createDeployResourcePlan, createDeployUploadPayload, planWebSocketMigrations, validateManifestShape };
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@void/deploy-core",
|
|
3
|
+
"version": "0.20.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/voidzero-dev/void.git",
|
|
7
|
+
"directory": "packages/deploy-core"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.mts",
|
|
16
|
+
"import": "./dist/index.mjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "7.0.2",
|
|
24
|
+
"vite-plus": "0.3.1"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "vp pack",
|
|
28
|
+
"dev": "vp pack --watch",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
}
|
|
31
|
+
}
|