@skippercorp/skipper 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,175 @@
1
+ import { createHash } from "node:crypto";
2
+ import { gunzipSync, gzipSync } from "node:zlib";
3
+ import {
4
+ createDefaultWorkerParameterValues,
5
+ listWorkerChunkParameterKeys,
6
+ WORKER_CHUNK_COUNT,
7
+ WORKER_CHUNK_SIZE,
8
+ WORKERS_CHUNK_COUNT_PARAM,
9
+ WORKERS_ENCODING,
10
+ WORKERS_ENCODING_PARAM,
11
+ WORKER_SCHEMA_VERSION,
12
+ WORKERS_SCHEMA_VERSION_PARAM,
13
+ WORKERS_SHA256_PARAM,
14
+ } from "./aws-params.js";
15
+ import { parseUnknownJson } from "../shared/validation/parse-json.js";
16
+ import { parseWorkerDefinition, type WorkerManifest } from "./contract.js";
17
+
18
+ export type SerializedWorkers = {
19
+ parameterValues: Record<string, string>;
20
+ byteLength: number;
21
+ workerCount: number;
22
+ };
23
+
24
+ /**
25
+ * Build worker parameter values from manifest.
26
+ *
27
+ * @since 1.0.0
28
+ * @category Shared
29
+ */
30
+ export function encodeWorkerManifest(manifest: WorkerManifest): SerializedWorkers {
31
+ if (manifest.workers.length === 0) {
32
+ return {
33
+ parameterValues: createDefaultWorkerParameterValues(),
34
+ byteLength: 0,
35
+ workerCount: 0,
36
+ };
37
+ }
38
+ const serializedJson = JSON.stringify(manifest);
39
+ const compressed = gzipSync(Buffer.from(serializedJson, "utf8"));
40
+ const encoded = compressed.toString("base64");
41
+ const chunks = chunkValue(encoded, WORKER_CHUNK_SIZE);
42
+ if (chunks.length > WORKER_CHUNK_COUNT) {
43
+ throw new Error(`worker manifest too large: ${chunks.length} chunks > ${WORKER_CHUNK_COUNT}`);
44
+ }
45
+ const parameterValues = createDefaultWorkerParameterValues();
46
+ parameterValues[WORKERS_ENCODING_PARAM] = WORKERS_ENCODING;
47
+ parameterValues[WORKERS_SCHEMA_VERSION_PARAM] = WORKER_SCHEMA_VERSION;
48
+ parameterValues[WORKERS_SHA256_PARAM] = sha256Hex(serializedJson);
49
+ parameterValues[WORKERS_CHUNK_COUNT_PARAM] = String(chunks.length);
50
+
51
+ const keys = listWorkerChunkParameterKeys();
52
+ for (let index = 0; index < chunks.length; index += 1) {
53
+ const key = keys[index];
54
+ const chunk = chunks[index];
55
+ if (!key) {
56
+ throw new Error(`missing worker chunk key for index ${index}`);
57
+ }
58
+ if (chunk === undefined) {
59
+ throw new Error(`missing worker chunk for index ${index}`);
60
+ }
61
+ parameterValues[key] = chunk;
62
+ }
63
+ return {
64
+ parameterValues,
65
+ byteLength: serializedJson.length,
66
+ workerCount: manifest.workers.length,
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Read worker manifest from CloudFormation parameter values.
72
+ *
73
+ * @since 1.0.0
74
+ * @category Shared
75
+ */
76
+ export function decodeWorkerManifest(
77
+ parameterValues: Record<string, string | undefined>,
78
+ ): WorkerManifest | undefined {
79
+ const chunkCount = readChunkCount(parameterValues[WORKERS_CHUNK_COUNT_PARAM]);
80
+ const sha = parameterValues[WORKERS_SHA256_PARAM]?.trim() ?? "";
81
+ if (chunkCount === 0 || sha.length === 0) {
82
+ return undefined;
83
+ }
84
+ const encoding = parameterValues[WORKERS_ENCODING_PARAM]?.trim();
85
+ if (encoding !== WORKERS_ENCODING) {
86
+ throw new Error(`unsupported workers encoding: ${encoding ?? ""}`);
87
+ }
88
+ const schemaVersion = parameterValues[WORKERS_SCHEMA_VERSION_PARAM]?.trim();
89
+ if (schemaVersion !== WORKER_SCHEMA_VERSION) {
90
+ throw new Error(`unsupported workers schema version: ${schemaVersion ?? ""}`);
91
+ }
92
+ const keys = listWorkerChunkParameterKeys();
93
+ const encoded = keys
94
+ .slice(0, chunkCount)
95
+ .map((key) => parameterValues[key]?.trim() ?? "")
96
+ .join("");
97
+ if (encoded.length === 0) {
98
+ throw new Error("worker chunks missing");
99
+ }
100
+ const compressed = Buffer.from(encoded, "base64");
101
+ const json = gunzipSync(compressed).toString("utf8");
102
+ const parsed = parseWorkerManifestJson(json);
103
+ const actualSha = sha256Hex(json);
104
+ if (actualSha !== sha) {
105
+ throw new Error("worker manifest checksum mismatch");
106
+ }
107
+ return parsed;
108
+ }
109
+
110
+ /**
111
+ * Parse serialized worker manifest JSON.
112
+ *
113
+ * @since 1.0.0
114
+ * @category Shared
115
+ */
116
+ function parseWorkerManifestJson(value: string): WorkerManifest {
117
+ const parsed = parseUnknownJson(value, "worker manifest");
118
+ if (!isRecord(parsed) || !Array.isArray(parsed.workers)) {
119
+ throw new Error("invalid worker manifest");
120
+ }
121
+ return {
122
+ workers: parsed.workers.map((worker, index) =>
123
+ parseWorkerDefinition(worker, `manifest worker[${index}]`),
124
+ ),
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Build sha256 hash as hex.
130
+ *
131
+ * @since 1.0.0
132
+ * @category Shared
133
+ */
134
+ function sha256Hex(value: string): string {
135
+ return createHash("sha256").update(value).digest("hex");
136
+ }
137
+
138
+ /**
139
+ * Split string into fixed-size chunks.
140
+ *
141
+ * @since 1.0.0
142
+ * @category Shared
143
+ */
144
+ function chunkValue(value: string, chunkSize: number): string[] {
145
+ const chunks: string[] = [];
146
+ for (let offset = 0; offset < value.length; offset += chunkSize) {
147
+ chunks.push(value.slice(offset, offset + chunkSize));
148
+ }
149
+ return chunks;
150
+ }
151
+
152
+ /**
153
+ * Parse worker chunk count value.
154
+ *
155
+ * @since 1.0.0
156
+ * @category Shared
157
+ */
158
+ function readChunkCount(value: string | undefined): number {
159
+ if (!value) return 0;
160
+ const parsed = Number.parseInt(value, 10);
161
+ if (Number.isNaN(parsed) || parsed < 0 || parsed > WORKER_CHUNK_COUNT) {
162
+ throw new Error("invalid worker chunk count");
163
+ }
164
+ return parsed;
165
+ }
166
+
167
+ /**
168
+ * Check plain object shape.
169
+ *
170
+ * @since 1.0.0
171
+ * @category Shared
172
+ */
173
+ function isRecord(value: unknown): value is Record<string, unknown> {
174
+ return typeof value === "object" && value !== null;
175
+ }