veryfront 0.1.1067 → 0.1.1069

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,394 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import { getRedisModule } from "../platform/adapters/redis/modules.js";
3
+ import { getEnv } from "../platform/compat/process.js";
4
+ const ROUTING_INVALIDATION_CHANNEL = "vf-proxy-routing-invalidations-v1";
5
+ const ROUTING_INVALIDATION_ACK_PREFIX = `${ROUTING_INVALIDATION_CHANNEL}:ack:`;
6
+ const DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS = 1_500;
7
+ const DEFAULT_CONNECT_TIMEOUT_MS = 3_000;
8
+ const MAX_RECONNECT_ATTEMPTS = 5;
9
+ const MAX_RECENT_EVENT_IDS = 1_000;
10
+ const MAX_SIGNED_ENVELOPE_BYTES = 24 * 1024;
11
+ const MAX_SIGNED_PAYLOAD_BYTES = 16 * 1024;
12
+ const DEFAULT_MAX_ENVELOPE_AGE_MS = 60_000;
13
+ const DEFAULT_MAX_ENVELOPE_FUTURE_MS = 5_000;
14
+ const INTEGRITY_SECRET_ENV_VAR = "VERYFRONT_PROXY_ROUTING_INVALIDATION_SECRET";
15
+ const EVENT_SIGNATURE_DOMAIN = "vf-proxy-routing-invalidation:event:v1";
16
+ const ACK_SIGNATURE_DOMAIN = "vf-proxy-routing-invalidation:ack:v1";
17
+ function positiveInteger(value, fallback) {
18
+ const parsed = Number(value);
19
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
20
+ }
21
+ function encodedByteLength(value) {
22
+ return new TextEncoder().encode(value).byteLength;
23
+ }
24
+ function parseSignedEnvelope(message) {
25
+ if (encodedByteLength(message) > MAX_SIGNED_ENVELOPE_BYTES)
26
+ return null;
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(message);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
35
+ return null;
36
+ const envelope = parsed;
37
+ if (envelope.version !== 1 ||
38
+ typeof envelope.issuedAtMs !== "number" ||
39
+ !Number.isInteger(envelope.issuedAtMs) ||
40
+ envelope.issuedAtMs <= 0 ||
41
+ typeof envelope.payload !== "string" ||
42
+ encodedByteLength(envelope.payload) > MAX_SIGNED_PAYLOAD_BYTES ||
43
+ typeof envelope.signature !== "string" ||
44
+ envelope.signature.length < 32 ||
45
+ envelope.signature.length > 128) {
46
+ return null;
47
+ }
48
+ return envelope;
49
+ }
50
+ function signatureDomainPrefix(domain) {
51
+ return domain === "event" ? EVENT_SIGNATURE_DOMAIN : ACK_SIGNATURE_DOMAIN;
52
+ }
53
+ function signatureInput(domain, issuedAtMs, payload) {
54
+ const encoded = new TextEncoder().encode(`${signatureDomainPrefix(domain)}\0${issuedAtMs}\0${payload}`);
55
+ return encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
56
+ }
57
+ function base64UrlEncode(bytes) {
58
+ const binary = Array.from(new Uint8Array(bytes), (byte) => String.fromCharCode(byte)).join("");
59
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
60
+ }
61
+ function base64UrlDecode(value) {
62
+ if (!/^[A-Za-z0-9_-]+$/u.test(value))
63
+ return null;
64
+ const padded = value.replaceAll("-", "+").replaceAll("_", "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
65
+ try {
66
+ return Uint8Array.from(atob(padded), (char) => char.charCodeAt(0)).buffer;
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ async function createHmacKey(secret) {
73
+ return dntShim.crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
74
+ }
75
+ async function signPayload(key, domain, issuedAtMs, payload) {
76
+ return base64UrlEncode(await dntShim.crypto.subtle.sign("HMAC", key, signatureInput(domain, issuedAtMs, payload)));
77
+ }
78
+ async function verifyPayloadSignature(key, domain, issuedAtMs, payload, signature) {
79
+ const signatureBytes = base64UrlDecode(signature);
80
+ if (!signatureBytes)
81
+ return false;
82
+ return await dntShim.crypto.subtle.verify("HMAC", key, signatureBytes, signatureInput(domain, issuedAtMs, payload));
83
+ }
84
+ async function serializeSignedEnvelope(key, domain, payload, now) {
85
+ if (encodedByteLength(payload) > MAX_SIGNED_PAYLOAD_BYTES) {
86
+ throw new Error("Proxy routing invalidation payload is too large");
87
+ }
88
+ const issuedAtMs = Math.trunc(now());
89
+ return JSON.stringify({
90
+ version: 1,
91
+ issuedAtMs,
92
+ payload,
93
+ signature: await signPayload(key, domain, issuedAtMs, payload),
94
+ });
95
+ }
96
+ async function verifySignedEnvelope(key, domain, message, now) {
97
+ const envelope = parseSignedEnvelope(message);
98
+ if (!envelope)
99
+ return null;
100
+ const currentTimeMs = now();
101
+ if (envelope.issuedAtMs < currentTimeMs - DEFAULT_MAX_ENVELOPE_AGE_MS ||
102
+ envelope.issuedAtMs > currentTimeMs + DEFAULT_MAX_ENVELOPE_FUTURE_MS) {
103
+ return null;
104
+ }
105
+ const verified = await verifyPayloadSignature(key, domain, envelope.issuedAtMs, envelope.payload, envelope.signature);
106
+ return verified ? envelope.payload : null;
107
+ }
108
+ function parseEvent(message) {
109
+ if (encodedByteLength(message) > MAX_SIGNED_PAYLOAD_BYTES)
110
+ return null;
111
+ let parsed;
112
+ try {
113
+ parsed = JSON.parse(message);
114
+ }
115
+ catch {
116
+ return null;
117
+ }
118
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
119
+ return null;
120
+ const event = parsed;
121
+ if (event.version !== 1 ||
122
+ typeof event.eventId !== "string" || !event.eventId ||
123
+ typeof event.projectId !== "string" || !event.projectId ||
124
+ typeof event.projectSlug !== "string" || !event.projectSlug ||
125
+ typeof event.deploymentId !== "string" || !event.deploymentId ||
126
+ typeof event.environmentId !== "string" || !event.environmentId ||
127
+ typeof event.environmentName !== "string" || !event.environmentName ||
128
+ typeof event.releaseId !== "string" || !event.releaseId) {
129
+ return null;
130
+ }
131
+ return event;
132
+ }
133
+ function parseAcknowledgement(message) {
134
+ if (encodedByteLength(message) > MAX_SIGNED_PAYLOAD_BYTES)
135
+ return null;
136
+ let parsed;
137
+ try {
138
+ parsed = JSON.parse(message);
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
144
+ return null;
145
+ const acknowledgement = parsed;
146
+ if (typeof acknowledgement.eventId !== "string" || !acknowledgement.eventId ||
147
+ typeof acknowledgement.replicaId !== "string" || !acknowledgement.replicaId) {
148
+ return null;
149
+ }
150
+ return acknowledgement;
151
+ }
152
+ async function createDefaultClient(redisUrl) {
153
+ const { NodeRedis } = await getRedisModule();
154
+ if (!NodeRedis)
155
+ throw new Error("Redis client module is unavailable");
156
+ const createClient = NodeRedis.createClient;
157
+ return createClient({
158
+ url: redisUrl,
159
+ socket: {
160
+ connectTimeout: DEFAULT_CONNECT_TIMEOUT_MS,
161
+ reconnectStrategy: (retries) => retries >= MAX_RECONNECT_ATTEMPTS
162
+ ? new Error("Routing invalidation Redis reconnect limit reached")
163
+ : Math.min(100 * 2 ** retries, 1_000),
164
+ },
165
+ });
166
+ }
167
+ async function closeClient(client, logger) {
168
+ try {
169
+ await client.close();
170
+ }
171
+ catch (error) {
172
+ client.destroy();
173
+ logger?.warn("Failed to close routing invalidation Redis client cleanly", {
174
+ error: error instanceof Error ? error.message : String(error),
175
+ });
176
+ }
177
+ }
178
+ export async function startProxyRoutingInvalidationBus(options) {
179
+ const redisUrl = options.redisUrl === undefined ? getEnv("REDIS_URL") : options.redisUrl;
180
+ if (!redisUrl)
181
+ return null;
182
+ const integritySecret = options.integritySecret ?? getEnv(INTEGRITY_SECRET_ENV_VAR) ?? "";
183
+ if (!integritySecret)
184
+ return null;
185
+ const expectedReplicas = positiveInteger(options.expectedReplicas ?? getEnv("VERYFRONT_PROXY_EXPECTED_REPLICAS"), 1);
186
+ const acknowledgementTimeoutMs = positiveInteger(options.acknowledgementTimeoutMs, DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS);
187
+ const replicaId = options.replicaId ?? getEnv("HOSTNAME") ?? dntShim.crypto.randomUUID();
188
+ const createClient = options.createClient ?? createDefaultClient;
189
+ const publishClient = await createClient(redisUrl);
190
+ const subscribeClient = await createClient(redisUrl);
191
+ const processedEventIds = new Set();
192
+ const eventProcessing = new Map();
193
+ const hmacKey = await createHmacKey(integritySecret);
194
+ const now = options.now ?? (() => Date.now());
195
+ const acknowledgementListeners = new Map();
196
+ const activeAcknowledgementChannels = new Set();
197
+ const closeWaiters = new Set();
198
+ let closed = false;
199
+ const waitForClose = () => {
200
+ if (closed)
201
+ return Promise.resolve();
202
+ return new Promise((resolve) => {
203
+ closeWaiters.add(resolve);
204
+ });
205
+ };
206
+ const resolveCloseWaiters = () => {
207
+ for (const resolve of closeWaiters)
208
+ resolve();
209
+ closeWaiters.clear();
210
+ };
211
+ const subscribeAcknowledgement = async (channel, listener) => {
212
+ let listeners = acknowledgementListeners.get(channel);
213
+ if (!listeners) {
214
+ listeners = new Set();
215
+ acknowledgementListeners.set(channel, listeners);
216
+ await subscribeClient.subscribe(channel, (message, receivedChannel) => {
217
+ if (receivedChannel !== channel)
218
+ return;
219
+ for (const acknowledgementListener of listeners ?? []) {
220
+ acknowledgementListener(message, receivedChannel);
221
+ }
222
+ });
223
+ }
224
+ listeners.add(listener);
225
+ activeAcknowledgementChannels.add(channel);
226
+ };
227
+ const unsubscribeAcknowledgement = async (channel, listener) => {
228
+ const listeners = acknowledgementListeners.get(channel);
229
+ if (!listeners)
230
+ return;
231
+ listeners.delete(listener);
232
+ if (listeners.size > 0)
233
+ return;
234
+ acknowledgementListeners.delete(channel);
235
+ activeAcknowledgementChannels.delete(channel);
236
+ if (!closed)
237
+ await subscribeClient.unsubscribe(channel);
238
+ };
239
+ const rememberProcessedEvent = (eventId) => {
240
+ processedEventIds.delete(eventId);
241
+ processedEventIds.add(eventId);
242
+ while (processedEventIds.size > MAX_RECENT_EVENT_IDS) {
243
+ const oldestEventId = processedEventIds.values().next().value;
244
+ if (!oldestEventId)
245
+ break;
246
+ processedEventIds.delete(oldestEventId);
247
+ }
248
+ };
249
+ const processEvent = (event) => {
250
+ if (processedEventIds.has(event.eventId))
251
+ return Promise.resolve();
252
+ const existing = eventProcessing.get(event.eventId);
253
+ if (existing)
254
+ return existing;
255
+ const processing = Promise.resolve()
256
+ .then(() => options.onInvalidate(event))
257
+ .then(() => {
258
+ rememberProcessedEvent(event.eventId);
259
+ eventProcessing.delete(event.eventId);
260
+ }, (error) => {
261
+ eventProcessing.delete(event.eventId);
262
+ throw error;
263
+ });
264
+ eventProcessing.set(event.eventId, processing);
265
+ return processing;
266
+ };
267
+ const logRedisError = (error) => {
268
+ options.logger?.error("Proxy routing invalidation Redis error", error instanceof Error ? error : new Error(String(error)));
269
+ };
270
+ publishClient.on?.("error", logRedisError);
271
+ subscribeClient.on?.("error", logRedisError);
272
+ try {
273
+ await Promise.all([publishClient.connect(), subscribeClient.connect()]);
274
+ await subscribeClient.subscribe(ROUTING_INVALIDATION_CHANNEL, (message, channel) => {
275
+ if (channel !== ROUTING_INVALIDATION_CHANNEL)
276
+ return;
277
+ void verifySignedEnvelope(hmacKey, "event", message, now)
278
+ .then((payload) => {
279
+ if (!payload || closed)
280
+ return null;
281
+ const event = parseEvent(payload);
282
+ if (!event)
283
+ return null;
284
+ return processEvent(event).then(async () => {
285
+ if (closed)
286
+ return;
287
+ const acknowledgementPayload = JSON.stringify({ eventId: event.eventId, replicaId });
288
+ await publishClient.publish(`${ROUTING_INVALIDATION_ACK_PREFIX}${event.eventId}`, await serializeSignedEnvelope(hmacKey, "ack", acknowledgementPayload, now));
289
+ });
290
+ })
291
+ .catch((error) => {
292
+ options.logger?.error("Failed to apply proxy routing invalidation", error instanceof Error ? error : new Error(String(error)));
293
+ });
294
+ });
295
+ }
296
+ catch (error) {
297
+ publishClient.destroy();
298
+ subscribeClient.destroy();
299
+ throw error;
300
+ }
301
+ options.logger?.info("Proxy routing invalidation bus connected", {
302
+ expectedReplicas,
303
+ replicaId,
304
+ });
305
+ return {
306
+ async publish(event) {
307
+ if (closed)
308
+ throw new Error("Proxy routing invalidation bus is closed");
309
+ const acknowledgementChannel = `${ROUTING_INVALIDATION_ACK_PREFIX}${event.eventId}`;
310
+ const acknowledgedReplicas = new Set();
311
+ let recipients = 0;
312
+ let resolveAcknowledged;
313
+ const acknowledgementReceived = new Promise((resolve) => {
314
+ resolveAcknowledged = resolve;
315
+ });
316
+ const acknowledgementListener = (message, channel) => {
317
+ if (channel !== acknowledgementChannel)
318
+ return;
319
+ void verifySignedEnvelope(hmacKey, "ack", message, now)
320
+ .then((payload) => {
321
+ if (!payload)
322
+ return;
323
+ const acknowledgement = parseAcknowledgement(payload);
324
+ if (!acknowledgement || acknowledgement.eventId !== event.eventId)
325
+ return;
326
+ acknowledgedReplicas.add(acknowledgement.replicaId);
327
+ if (recipients > 0 && acknowledgedReplicas.size >= recipients) {
328
+ resolveAcknowledged?.();
329
+ }
330
+ })
331
+ .catch((error) => {
332
+ options.logger?.error("Failed to verify proxy routing invalidation acknowledgement", error instanceof Error ? error : new Error(String(error)), { eventId: event.eventId });
333
+ });
334
+ };
335
+ await subscribeAcknowledgement(acknowledgementChannel, acknowledgementListener);
336
+ try {
337
+ const eventPayload = JSON.stringify(event);
338
+ try {
339
+ recipients = await publishClient.publish(ROUTING_INVALIDATION_CHANNEL, await serializeSignedEnvelope(hmacKey, "event", eventPayload, now));
340
+ }
341
+ catch (error) {
342
+ if (!closed)
343
+ throw error;
344
+ return {
345
+ acknowledged: acknowledgedReplicas.size,
346
+ converged: false,
347
+ recipients,
348
+ };
349
+ }
350
+ if (recipients > 0 && acknowledgedReplicas.size >= recipients)
351
+ resolveAcknowledged?.();
352
+ if (recipients > 0) {
353
+ let timeout;
354
+ await Promise.race([
355
+ acknowledgementReceived,
356
+ waitForClose(),
357
+ new Promise((resolve) => {
358
+ timeout = setTimeout(resolve, acknowledgementTimeoutMs);
359
+ }),
360
+ ]);
361
+ if (timeout !== undefined)
362
+ clearTimeout(timeout);
363
+ }
364
+ const acknowledged = acknowledgedReplicas.size;
365
+ return {
366
+ acknowledged,
367
+ converged: recipients >= expectedReplicas && acknowledged >= recipients,
368
+ recipients,
369
+ };
370
+ }
371
+ finally {
372
+ await unsubscribeAcknowledgement(acknowledgementChannel, acknowledgementListener);
373
+ }
374
+ },
375
+ async close() {
376
+ if (closed)
377
+ return;
378
+ closed = true;
379
+ resolveCloseWaiters();
380
+ try {
381
+ await Promise.allSettled([
382
+ subscribeClient.unsubscribe(ROUTING_INVALIDATION_CHANNEL),
383
+ ...[...activeAcknowledgementChannels].map((channel) => subscribeClient.unsubscribe(channel)),
384
+ ]);
385
+ }
386
+ finally {
387
+ await Promise.all([
388
+ closeClient(publishClient, options.logger),
389
+ closeClient(subscribeClient, options.logger),
390
+ ]);
391
+ }
392
+ },
393
+ };
394
+ }
@@ -0,0 +1,32 @@
1
+ export declare const PROXY_ROUTING_INVALIDATION_PATH = "/_proxy/internal/routing-invalidation";
2
+ export declare const PROXY_ROUTING_INVALIDATION_PLATFORM = "proxy-routing";
3
+ export declare const PROXY_ROUTING_INVALIDATION_SUBJECT = "deployment-routing-invalidation";
4
+ export interface ProxyRoutingInvalidationRequest {
5
+ version: 1;
6
+ projectId: string;
7
+ projectSlug: string;
8
+ deploymentId: string;
9
+ environmentId: string;
10
+ environmentName: string;
11
+ releaseId: string;
12
+ }
13
+ export interface ProxyRoutingInvalidationEvent extends ProxyRoutingInvalidationRequest {
14
+ eventId: string;
15
+ }
16
+ export interface ProxyRoutingInvalidationPublishResult {
17
+ acknowledged: number;
18
+ converged: boolean;
19
+ recipients: number;
20
+ }
21
+ export interface ProxyRoutingInvalidationPublisher {
22
+ publish(event: ProxyRoutingInvalidationEvent): Promise<ProxyRoutingInvalidationPublishResult>;
23
+ }
24
+ interface ProxyRoutingInvalidationHandlerOptions {
25
+ createEventId?: () => string;
26
+ publicKeyPem?: string;
27
+ publisher: ProxyRoutingInvalidationPublisher | null;
28
+ }
29
+ export declare function parseProxyRoutingInvalidationRequest(body: string): ProxyRoutingInvalidationRequest | null;
30
+ export declare function handleProxyRoutingInvalidationRequest(req: Request, options: ProxyRoutingInvalidationHandlerOptions): Promise<Response>;
31
+ export {};
32
+ //# sourceMappingURL=routing-invalidation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-invalidation.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/routing-invalidation.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,+BAA+B,0CAA0C,CAAC;AACvF,eAAO,MAAM,mCAAmC,kBAAkB,CAAC;AACnE,eAAO,MAAM,kCAAkC,oCAAoC,CAAC;AAOpF,MAAM,WAAW,+BAA+B;IAC9C,OAAO,EAAE,CAAC,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,6BAA8B,SAAQ,+BAA+B;IACpF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,qCAAqC;IACpD,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iCAAiC;IAChD,OAAO,CACL,KAAK,EAAE,6BAA6B,GACnC,OAAO,CAAC,qCAAqC,CAAC,CAAC;CACnD;AAED,UAAU,sCAAsC;IAC9C,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,iCAAiC,GAAG,IAAI,CAAC;CACrD;AAkDD,wBAAgB,oCAAoC,CAClD,IAAI,EAAE,MAAM,GACX,+BAA+B,GAAG,IAAI,CA+BxC;AAED,wBAAsB,qCAAqC,CACzD,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,sCAAsC,GAC9C,OAAO,CAAC,QAAQ,CAAC,CAqDnB"}
@@ -0,0 +1,135 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import { verifyDispatchJws } from "../channels/control-plane.js";
3
+ import { getHostEnv } from "../platform/compat/process.js";
4
+ export const PROXY_ROUTING_INVALIDATION_PATH = "/_proxy/internal/routing-invalidation";
5
+ export const PROXY_ROUTING_INVALIDATION_PLATFORM = "proxy-routing";
6
+ export const PROXY_ROUTING_INVALIDATION_SUBJECT = "deployment-routing-invalidation";
7
+ const DISPATCH_JWS_HEADER = "x-veryfront-dispatch-jws";
8
+ const PUBLIC_KEY_ENV_VAR = "CHANNEL_DISPATCH_SIGNING_PUBLIC_KEY";
9
+ const MAX_SIGNATURE_AGE_SECONDS = 60;
10
+ const MAX_REQUEST_BODY_BYTES = 16 * 1024;
11
+ function jsonResponse(status, body) {
12
+ return Response.json(body, {
13
+ status,
14
+ headers: { "cache-control": "no-store" },
15
+ });
16
+ }
17
+ function nonEmptyString(value) {
18
+ return typeof value === "string" && value.length > 0;
19
+ }
20
+ async function readBoundedRequestBody(req) {
21
+ if (!req.body)
22
+ return { body: "" };
23
+ const reader = req.body.getReader();
24
+ const chunks = [];
25
+ let totalBytes = 0;
26
+ try {
27
+ while (true) {
28
+ const { done, value } = await reader.read();
29
+ if (done)
30
+ break;
31
+ if (!value)
32
+ continue;
33
+ totalBytes += value.byteLength;
34
+ if (totalBytes > MAX_REQUEST_BODY_BYTES) {
35
+ await reader.cancel("Request body is too large").catch(() => undefined);
36
+ return { error: "too-large" };
37
+ }
38
+ chunks.push(value);
39
+ }
40
+ }
41
+ catch {
42
+ return { error: "unreadable" };
43
+ }
44
+ finally {
45
+ reader.releaseLock();
46
+ }
47
+ const bytes = new Uint8Array(totalBytes);
48
+ let offset = 0;
49
+ for (const chunk of chunks) {
50
+ bytes.set(chunk, offset);
51
+ offset += chunk.byteLength;
52
+ }
53
+ return { body: new TextDecoder().decode(bytes) };
54
+ }
55
+ export function parseProxyRoutingInvalidationRequest(body) {
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(body);
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
64
+ return null;
65
+ const input = parsed;
66
+ if (input.version !== 1 ||
67
+ !nonEmptyString(input.projectId) ||
68
+ !nonEmptyString(input.projectSlug) ||
69
+ !nonEmptyString(input.deploymentId) ||
70
+ !nonEmptyString(input.environmentId) ||
71
+ !nonEmptyString(input.environmentName) ||
72
+ !nonEmptyString(input.releaseId)) {
73
+ return null;
74
+ }
75
+ return {
76
+ version: 1,
77
+ projectId: input.projectId,
78
+ projectSlug: input.projectSlug,
79
+ deploymentId: input.deploymentId,
80
+ environmentId: input.environmentId,
81
+ environmentName: input.environmentName,
82
+ releaseId: input.releaseId,
83
+ };
84
+ }
85
+ export async function handleProxyRoutingInvalidationRequest(req, options) {
86
+ if (req.method !== "POST") {
87
+ return new Response(null, { status: 405, headers: { allow: "POST" } });
88
+ }
89
+ const publicKeyPem = options.publicKeyPem ?? getHostEnv(PUBLIC_KEY_ENV_VAR) ?? "";
90
+ if (!publicKeyPem || !options.publisher) {
91
+ return jsonResponse(503, { error: "Routing invalidation is unavailable" });
92
+ }
93
+ const declaredLength = Number(req.headers.get("content-length") ?? 0);
94
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_REQUEST_BODY_BYTES) {
95
+ return jsonResponse(413, { error: "Request body is too large" });
96
+ }
97
+ const bodyResult = await readBoundedRequestBody(req);
98
+ if ("error" in bodyResult && bodyResult.error === "too-large") {
99
+ return jsonResponse(413, { error: "Request body is too large" });
100
+ }
101
+ if ("error" in bodyResult) {
102
+ return jsonResponse(400, { error: "Invalid routing invalidation request" });
103
+ }
104
+ const { body } = bodyResult;
105
+ const input = parseProxyRoutingInvalidationRequest(body);
106
+ if (!input)
107
+ return jsonResponse(400, { error: "Invalid routing invalidation request" });
108
+ const jws = req.headers.get(DISPATCH_JWS_HEADER);
109
+ if (!jws)
110
+ return jsonResponse(401, { error: "Invalid routing invalidation signature" });
111
+ try {
112
+ await verifyDispatchJws(jws, body, {
113
+ audience: input.projectSlug,
114
+ expectedPlatform: PROXY_ROUTING_INVALIDATION_PLATFORM,
115
+ expectedProjectId: input.projectId,
116
+ expectedSubject: PROXY_ROUTING_INVALIDATION_SUBJECT,
117
+ maxAgeSeconds: MAX_SIGNATURE_AGE_SECONDS,
118
+ publicKeyPem,
119
+ });
120
+ }
121
+ catch {
122
+ return jsonResponse(401, { error: "Invalid routing invalidation signature" });
123
+ }
124
+ try {
125
+ const createEventId = options.createEventId ?? (() => dntShim.crypto.randomUUID());
126
+ const result = await options.publisher.publish({
127
+ eventId: createEventId(),
128
+ ...input,
129
+ });
130
+ return jsonResponse(result.converged ? 200 : 503, { ...result });
131
+ }
132
+ catch {
133
+ return jsonResponse(503, { error: "Routing invalidation did not converge" });
134
+ }
135
+ }
@@ -1,3 +1,3 @@
1
1
  /** Shared version value. */
2
- export declare const VERSION = "0.1.1067";
2
+ export declare const VERSION = "0.1.1069";
3
3
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,4 +1,4 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
3
  /** Shared version value. */
4
- export const VERSION = "0.1.1067";
4
+ export const VERSION = "0.1.1069";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.1067",
3
+ "version": "0.1.1069",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
@@ -328,10 +328,10 @@
328
328
  "@types/react": "19.2.14",
329
329
  "@types/react-dom": "19.2.3",
330
330
  "ws": "8.21.0",
331
- "@veryfront/ext-bundler-esbuild": "0.1.1067",
332
- "@veryfront/ext-content-mdx": "0.1.1067",
333
- "@veryfront/ext-css-tailwind": "0.1.1067",
334
- "@veryfront/ext-parser-babel": "0.1.1067"
331
+ "@veryfront/ext-bundler-esbuild": "0.1.1069",
332
+ "@veryfront/ext-content-mdx": "0.1.1069",
333
+ "@veryfront/ext-css-tailwind": "0.1.1069",
334
+ "@veryfront/ext-parser-babel": "0.1.1069"
335
335
  },
336
336
  "devDependencies": {
337
337
  "@types/node": "20.9.0"