veryfront 0.1.1067 → 0.1.1068
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/esm/cli/commands/deploy/command.d.ts +12 -1
- package/esm/cli/commands/deploy/command.d.ts.map +1 -1
- package/esm/cli/commands/deploy/command.js +28 -1
- package/esm/cli/commands/deploy/index.d.ts +2 -2
- package/esm/cli/commands/deploy/index.d.ts.map +1 -1
- package/esm/cli/commands/deploy/index.js +1 -1
- package/esm/deno.js +1 -1
- package/esm/src/proxy/handler.d.ts +19 -0
- package/esm/src/proxy/handler.d.ts.map +1 -1
- package/esm/src/proxy/handler.js +160 -13
- package/esm/src/proxy/main.js +45 -6
- package/esm/src/proxy/routing-invalidation-redis.d.ts +33 -0
- package/esm/src/proxy/routing-invalidation-redis.d.ts.map +1 -0
- package/esm/src/proxy/routing-invalidation-redis.js +394 -0
- package/esm/src/proxy/routing-invalidation.d.ts +32 -0
- package/esm/src/proxy/routing-invalidation.d.ts.map +1 -0
- package/esm/src/proxy/routing-invalidation.js +135 -0
- package/esm/src/utils/version-constant.d.ts +1 -1
- package/esm/src/utils/version-constant.js +1 -1
- package/package.json +5 -5
|
@@ -82,10 +82,20 @@ interface Release {
|
|
|
82
82
|
build_status: string;
|
|
83
83
|
deploy_status: string;
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
export type DeploymentRoutingConvergence = {
|
|
86
|
+
status: "converged";
|
|
87
|
+
acknowledged: number;
|
|
88
|
+
recipients: number;
|
|
89
|
+
} | {
|
|
90
|
+
status: "pending";
|
|
91
|
+
} | {
|
|
92
|
+
status: "skipped";
|
|
93
|
+
};
|
|
94
|
+
export interface Deployment {
|
|
86
95
|
id: string;
|
|
87
96
|
release_id: string;
|
|
88
97
|
environment_id: string;
|
|
98
|
+
routing_convergence?: DeploymentRoutingConvergence;
|
|
89
99
|
}
|
|
90
100
|
export interface DeploymentVerification {
|
|
91
101
|
projectId: string;
|
|
@@ -128,6 +138,7 @@ interface DeploymentVerificationOptions {
|
|
|
128
138
|
delayMs?: number;
|
|
129
139
|
verifiedRelease?: ReleaseSourceVerification;
|
|
130
140
|
}
|
|
141
|
+
export declare function getDeploymentRoutingConvergenceWarning(deployment: Deployment): string | null;
|
|
131
142
|
export declare function assertProjectOwnership(resourceType: "Environment" | "Release", resource: {
|
|
132
143
|
id: string;
|
|
133
144
|
project_id?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/command.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAE3E,OAAO,EAAE,KAAK,SAAS,EAA0C,MAAM,wBAAwB,CAAC;AAchG;;GAEG;AACH,eAAO,MAAM,mBAAmB;;;;;;IAO5B,oDAAoD;;GAGvD,CAAC;AAEF,eAAO,MAAM,gBAAgB;;;;;;IALzB,oDAAoD;;GAKO,CAAC;AAEhE;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC,CAAC;AAEhF;;GAEG;AACH,eAAO,MAAM,eAAe;;;;;;IAfxB,oDAAoD;;GAsBtD,CAAC;AAEH;;GAEG;AACH,UAAU,qBAAqB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE;QACP,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;KACrB,GAAG,IAAI,CAAC;CACV;AAED,UAAU,WAAW;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;CAC3C;AAED;;GAEG;AACH,UAAU,OAAO;IACf,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/command.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,7 +10,7 @@ import { defineSchema, lazySchema } from "../../../src/schemas/index.js";
|
|
|
10
10
|
import { cwd } from "../../../src/platform/index.js";
|
|
11
11
|
import { createApiClient, resolveConfigWithAuth } from "../../shared/config.js";
|
|
12
12
|
import { CommonArgs, createArgParser } from "../../shared/args.js";
|
|
13
|
-
import { confirmPrompt, logInfo, logSuccess } from "../../utils/index.js";
|
|
13
|
+
import { confirmPrompt, logInfo, logSuccess, logWarning } from "../../utils/index.js";
|
|
14
14
|
import { createNoopSpinner, createSpinner, muted } from "../../ui/index.js";
|
|
15
15
|
import { isJsonMode, streamJsonLine } from "../../shared/json-output.js";
|
|
16
16
|
import { computeSourceDigest, getProjectTarget, normalizeControlPlane, readPushReceipt, resolveGitSource, validatePushReceipt, } from "../../shared/deployment-provenance.js";
|
|
@@ -60,8 +60,20 @@ function normalizeDeployment(deployment) {
|
|
|
60
60
|
id: deployment.id,
|
|
61
61
|
release_id: releaseId,
|
|
62
62
|
environment_id: environmentId,
|
|
63
|
+
routing_convergence: deployment.routing_convergence,
|
|
63
64
|
};
|
|
64
65
|
}
|
|
66
|
+
export function getDeploymentRoutingConvergenceWarning(deployment) {
|
|
67
|
+
const convergence = deployment.routing_convergence;
|
|
68
|
+
if (!convergence)
|
|
69
|
+
return null;
|
|
70
|
+
if (convergence.status === "converged" &&
|
|
71
|
+
convergence.recipients > 0 &&
|
|
72
|
+
convergence.acknowledged >= convergence.recipients) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return `Deployment ${deployment.id} committed, but data-plane routing convergence was not confirmed; bounded cache expiry remains the recovery path`;
|
|
76
|
+
}
|
|
65
77
|
export function assertProjectOwnership(resourceType, resource, projectId) {
|
|
66
78
|
if (resource.project_id && resource.project_id !== projectId) {
|
|
67
79
|
throw new Error(`${resourceType} ${resource.id} does not belong to resolved project ${projectId}`);
|
|
@@ -348,6 +360,12 @@ export async function deployCommand(options) {
|
|
|
348
360
|
logInfo(` Environment: ${env} (${verification.environmentId})`);
|
|
349
361
|
logInfo(` Release: ${release.name} (${verification.releaseVersion}, ${verification.releaseId})`);
|
|
350
362
|
logInfo(` Deployment: ${verification.deploymentId}`);
|
|
363
|
+
if (deployment.routing_convergence?.status === "converged") {
|
|
364
|
+
logInfo(` Data plane: ${deployment.routing_convergence.acknowledged}/${deployment.routing_convergence.recipients} proxy replicas converged`);
|
|
365
|
+
}
|
|
366
|
+
const routingConvergenceWarning = getDeploymentRoutingConvergenceWarning(deployment);
|
|
367
|
+
if (routingConvergenceWarning)
|
|
368
|
+
logWarning(routingConvergenceWarning);
|
|
351
369
|
logInfo(` Commit: ${verification.commitSha}`);
|
|
352
370
|
logInfo(` Source digest: ${verification.sourceDigest}`);
|
|
353
371
|
logInfo(` Control plane: ${normalizeControlPlane(config.apiUrl)}`);
|
|
@@ -448,6 +466,14 @@ async function deployCommandJson(options) {
|
|
|
448
466
|
sourceDigest: source.sourceDigest,
|
|
449
467
|
}, { verifiedRelease });
|
|
450
468
|
streamJsonLine({ type: "step", name: "verify-deployment", status: "completed" });
|
|
469
|
+
const routingConvergenceWarning = getDeploymentRoutingConvergenceWarning(deployment);
|
|
470
|
+
if (routingConvergenceWarning) {
|
|
471
|
+
streamJsonLine({
|
|
472
|
+
type: "warning",
|
|
473
|
+
code: "routing-convergence-unconfirmed",
|
|
474
|
+
message: routingConvergenceWarning,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
451
477
|
streamJsonLine({
|
|
452
478
|
type: "result",
|
|
453
479
|
success: true,
|
|
@@ -462,6 +488,7 @@ async function deployCommandJson(options) {
|
|
|
462
488
|
environment: env,
|
|
463
489
|
environmentId: verification.environmentId,
|
|
464
490
|
deploymentId: verification.deploymentId,
|
|
491
|
+
routingConvergence: deployment.routing_convergence ?? null,
|
|
465
492
|
commitSha: verification.commitSha,
|
|
466
493
|
sourceDigest: verification.sourceDigest,
|
|
467
494
|
controlPlane: normalizeControlPlane(config.apiUrl),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deploy command - Create and deploy releases
|
|
3
3
|
*/
|
|
4
|
-
export { assertProjectOwnership, createDeployment, createRelease, DeployArgsSchema, deployCommand, getDeployment, getEnvironmentByName, getProject, getRelease, getReleaseSourceDigest, parseDeployArgs, resolvePushedSource, verifyDeployment, verifyReleaseSource, } from "./command.js";
|
|
5
|
-
export type { DeploymentVerification, DeployOptions, ReleaseSourceVerification, } from "./command.js";
|
|
4
|
+
export { assertProjectOwnership, createDeployment, createRelease, DeployArgsSchema, deployCommand, getDeployment, getDeploymentRoutingConvergenceWarning, getEnvironmentByName, getProject, getRelease, getReleaseSourceDigest, parseDeployArgs, resolvePushedSource, verifyDeployment, verifyReleaseSource, } from "./command.js";
|
|
5
|
+
export type { Deployment, DeploymentRoutingConvergence, DeploymentVerification, DeployOptions, ReleaseSourceVerification, } from "./command.js";
|
|
6
6
|
export { handleDeployCommand } from "./handler.js";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,oBAAoB,EACpB,UAAU,EACV,UAAU,EACV,sBAAsB,EACtB,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,sBAAsB,EACtB,aAAa,EACb,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/deploy/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,sCAAsC,EACtC,oBAAoB,EACpB,UAAU,EACV,UAAU,EACV,sBAAsB,EACtB,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,UAAU,EACV,4BAA4B,EAC5B,sBAAsB,EACtB,aAAa,EACb,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deploy command - Create and deploy releases
|
|
3
3
|
*/
|
|
4
|
-
export { assertProjectOwnership, createDeployment, createRelease, DeployArgsSchema, deployCommand, getDeployment, getEnvironmentByName, getProject, getRelease, getReleaseSourceDigest, parseDeployArgs, resolvePushedSource, verifyDeployment, verifyReleaseSource, } from "./command.js";
|
|
4
|
+
export { assertProjectOwnership, createDeployment, createRelease, DeployArgsSchema, deployCommand, getDeployment, getDeploymentRoutingConvergenceWarning, getEnvironmentByName, getProject, getRelease, getReleaseSourceDigest, parseDeployArgs, resolvePushedSource, verifyDeployment, verifyReleaseSource, } from "./command.js";
|
|
5
5
|
export { handleDeployCommand } from "./handler.js";
|
package/esm/deno.js
CHANGED
|
@@ -48,6 +48,20 @@ export interface ProxyRequestOptions {
|
|
|
48
48
|
url?: URL;
|
|
49
49
|
timing?: ProxyServerTiming;
|
|
50
50
|
}
|
|
51
|
+
export interface ProxyRoutingInvalidation {
|
|
52
|
+
projectId: string;
|
|
53
|
+
projectSlug?: string;
|
|
54
|
+
deploymentId?: string;
|
|
55
|
+
environmentId?: string;
|
|
56
|
+
environmentName?: string;
|
|
57
|
+
releaseId?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface ConfirmedProxyRoutingInvalidation extends ProxyRoutingInvalidation {
|
|
60
|
+
projectSlug: string;
|
|
61
|
+
environmentId: string;
|
|
62
|
+
environmentName: string;
|
|
63
|
+
releaseId: string;
|
|
64
|
+
}
|
|
51
65
|
export declare function createProxyHandler(options: ProxyHandlerOptions): {
|
|
52
66
|
processRequest: (req: Request, options?: ProxyRequestOptions) => Promise<ProxyContext>;
|
|
53
67
|
getTokenForApi: (req: Request, options?: ProxyRequestOptions) => Promise<string | undefined>;
|
|
@@ -59,6 +73,11 @@ export declare function createProxyHandler(options: ProxyHandlerOptions): {
|
|
|
59
73
|
}>;
|
|
60
74
|
close: () => Promise<void>;
|
|
61
75
|
validateConfig: () => string[];
|
|
76
|
+
invalidateRoutingLookup: (input: ProxyRoutingInvalidation) => {
|
|
77
|
+
evictedEntries: number;
|
|
78
|
+
generation: number;
|
|
79
|
+
};
|
|
80
|
+
invalidateAndConfirmRoutingLookup: (input: ConfirmedProxyRoutingInvalidation) => Promise<void>;
|
|
62
81
|
localProjects: Record<string, string>;
|
|
63
82
|
};
|
|
64
83
|
export type ProxyHandler = ReturnType<typeof createProxyHandler>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/handler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,YAAY,EAAsB,MAAM,kCAAkC,CAAC;AACzF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAenD,OAAO,EAAiC,KAAK,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG3F,OAAO,EAAE,iCAAiC,EAAE,MAAM,2BAA2B,CAAC;AAE9E,eAAO,MAAM,sBAAsB,0MAYzB,CAAC;
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/handler.ts"],"names":[],"mappings":"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"}
|
package/esm/src/proxy/handler.js
CHANGED
|
@@ -26,6 +26,7 @@ export const INTERNAL_PROXY_HEADERS = [
|
|
|
26
26
|
];
|
|
27
27
|
const DEFAULT_PROXY_ROUTING_CACHE_TTL_MS = 60_000;
|
|
28
28
|
const DEFAULT_PROXY_ROUTING_CACHE_MAX_ENTRIES = 1_000;
|
|
29
|
+
const MAX_ROUTING_LOOKUP_INVALIDATION_RETRIES = 2;
|
|
29
30
|
function readNonNegativeIntegerEnv(name, fallback) {
|
|
30
31
|
const raw = getEnv(name);
|
|
31
32
|
if (!raw)
|
|
@@ -46,6 +47,12 @@ class ProxyLookupAuthError extends Error {
|
|
|
46
47
|
this.name = "ProxyLookupAuthError";
|
|
47
48
|
}
|
|
48
49
|
}
|
|
50
|
+
class ProxyRoutingInvalidationRaceError extends Error {
|
|
51
|
+
constructor() {
|
|
52
|
+
super("Project routing changed during request; retry");
|
|
53
|
+
this.name = "ProxyRoutingInvalidationRaceError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
49
56
|
function isProxyLookupAuthError(error) {
|
|
50
57
|
return error instanceof ProxyLookupAuthError;
|
|
51
58
|
}
|
|
@@ -225,6 +232,11 @@ export function createProxyHandler(options) {
|
|
|
225
232
|
const routingCacheMaxEntries = readNonNegativeIntegerEnv("VERYFRONT_PROXY_ROUTING_CACHE_MAX_ENTRIES", DEFAULT_PROXY_ROUTING_CACHE_MAX_ENTRIES);
|
|
226
233
|
const routingLookupCache = new Map();
|
|
227
234
|
const routingLookupInflight = new Map();
|
|
235
|
+
const projectInvalidationGenerations = new Map();
|
|
236
|
+
const lookupKeyInvalidationGenerations = new Map();
|
|
237
|
+
const activeRoutingLookupGenerations = new Map();
|
|
238
|
+
const maxTrackedInvalidationGenerations = Math.max(routingCacheMaxEntries, DEFAULT_PROXY_ROUTING_CACHE_MAX_ENTRIES);
|
|
239
|
+
let routingLookupGeneration = 0;
|
|
228
240
|
async function resolveProjectLookup(lookupKey, token, timing) {
|
|
229
241
|
return await profileProxyServerTimingPhase(timing ?? { enabled: false, startedAt: 0, phases: new Map() }, "proxy.project_lookup", () => lookupProjectByDomain(lookupKey, config.apiBaseUrl, token, logger));
|
|
230
242
|
}
|
|
@@ -261,6 +273,71 @@ export function createProxyHandler(options) {
|
|
|
261
273
|
expiresAt: Date.now() + routingCacheTtlMs,
|
|
262
274
|
});
|
|
263
275
|
}
|
|
276
|
+
function pruneInvalidationGenerations(generations) {
|
|
277
|
+
const oldestActiveGeneration = activeRoutingLookupGenerations.size > 0
|
|
278
|
+
? Math.min(...activeRoutingLookupGenerations.keys())
|
|
279
|
+
: Number.POSITIVE_INFINITY;
|
|
280
|
+
while (generations.size > maxTrackedInvalidationGenerations) {
|
|
281
|
+
const oldestEntry = generations.entries().next().value;
|
|
282
|
+
if (!oldestEntry || oldestEntry[1] > oldestActiveGeneration)
|
|
283
|
+
break;
|
|
284
|
+
generations.delete(oldestEntry[0]);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function rememberInvalidationGeneration(generations, key, generation) {
|
|
288
|
+
generations.delete(key);
|
|
289
|
+
generations.set(key, generation);
|
|
290
|
+
pruneInvalidationGenerations(generations);
|
|
291
|
+
}
|
|
292
|
+
function beginRoutingLookup(generation) {
|
|
293
|
+
activeRoutingLookupGenerations.set(generation, (activeRoutingLookupGenerations.get(generation) ?? 0) + 1);
|
|
294
|
+
}
|
|
295
|
+
function endRoutingLookup(generation) {
|
|
296
|
+
const activeCount = activeRoutingLookupGenerations.get(generation) ?? 0;
|
|
297
|
+
if (activeCount <= 1)
|
|
298
|
+
activeRoutingLookupGenerations.delete(generation);
|
|
299
|
+
else
|
|
300
|
+
activeRoutingLookupGenerations.set(generation, activeCount - 1);
|
|
301
|
+
pruneInvalidationGenerations(projectInvalidationGenerations);
|
|
302
|
+
pruneInvalidationGenerations(lookupKeyInvalidationGenerations);
|
|
303
|
+
}
|
|
304
|
+
function wasRoutingLookupInvalidated(cacheKey, result, startedAtGeneration) {
|
|
305
|
+
const keyGeneration = lookupKeyInvalidationGenerations.get(cacheKey) ?? 0;
|
|
306
|
+
if (keyGeneration > startedAtGeneration)
|
|
307
|
+
return true;
|
|
308
|
+
if (!result)
|
|
309
|
+
return false;
|
|
310
|
+
return (projectInvalidationGenerations.get(result.id) ?? 0) > startedAtGeneration;
|
|
311
|
+
}
|
|
312
|
+
function invalidateRoutingLookup(input) {
|
|
313
|
+
const generation = ++routingLookupGeneration;
|
|
314
|
+
rememberInvalidationGeneration(projectInvalidationGenerations, input.projectId, generation);
|
|
315
|
+
const normalizedProjectSlug = input.projectSlug
|
|
316
|
+
? normalizeProjectLookupKey(input.projectSlug)
|
|
317
|
+
: undefined;
|
|
318
|
+
if (normalizedProjectSlug) {
|
|
319
|
+
rememberInvalidationGeneration(lookupKeyInvalidationGenerations, normalizedProjectSlug, generation);
|
|
320
|
+
}
|
|
321
|
+
let evictedEntries = 0;
|
|
322
|
+
for (const [cacheKey, entry] of routingLookupCache) {
|
|
323
|
+
if (entry.value.id !== input.projectId && cacheKey !== normalizedProjectSlug)
|
|
324
|
+
continue;
|
|
325
|
+
routingLookupCache.delete(cacheKey);
|
|
326
|
+
rememberInvalidationGeneration(lookupKeyInvalidationGenerations, cacheKey, generation);
|
|
327
|
+
evictedEntries++;
|
|
328
|
+
}
|
|
329
|
+
logger?.info("Proxy routing metadata invalidated after deployment activation", {
|
|
330
|
+
projectId: input.projectId,
|
|
331
|
+
projectSlug: input.projectSlug,
|
|
332
|
+
deploymentId: input.deploymentId,
|
|
333
|
+
environmentId: input.environmentId,
|
|
334
|
+
environmentName: input.environmentName,
|
|
335
|
+
releaseId: input.releaseId,
|
|
336
|
+
generation,
|
|
337
|
+
evictedEntries,
|
|
338
|
+
});
|
|
339
|
+
return { evictedEntries, generation };
|
|
340
|
+
}
|
|
264
341
|
async function resolveProjectRoutingLookup(lookupKey, token, timing) {
|
|
265
342
|
const cacheKey = normalizeProjectLookupKey(lookupKey);
|
|
266
343
|
return await profileProxyServerTimingPhase(timing ?? { enabled: false, startedAt: 0, phases: new Map() }, "proxy.routing_lookup", async () => {
|
|
@@ -270,27 +347,89 @@ export function createProxyHandler(options) {
|
|
|
270
347
|
return cached;
|
|
271
348
|
}
|
|
272
349
|
const existingLookup = routingLookupInflight.get(cacheKey);
|
|
273
|
-
if (existingLookup) {
|
|
350
|
+
if (existingLookup?.generation === routingLookupGeneration) {
|
|
274
351
|
logger?.debug("Proxy routing metadata lookup joined in-flight request", { lookupKey });
|
|
275
|
-
return await existingLookup;
|
|
352
|
+
return await existingLookup.promise;
|
|
276
353
|
}
|
|
277
|
-
const lookupPromise =
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
354
|
+
const lookupPromise = (async () => {
|
|
355
|
+
for (let attempt = 0; attempt <= MAX_ROUTING_LOOKUP_INVALIDATION_RETRIES; attempt++) {
|
|
356
|
+
const startedAtGeneration = routingLookupGeneration;
|
|
357
|
+
beginRoutingLookup(startedAtGeneration);
|
|
358
|
+
try {
|
|
359
|
+
const result = await lookupProjectRoutingMetadata(lookupKey, config.apiBaseUrl, token, logger);
|
|
360
|
+
if (!wasRoutingLookupInvalidated(cacheKey, result, startedAtGeneration)) {
|
|
361
|
+
if (result)
|
|
362
|
+
setCachedRoutingLookup(cacheKey, result);
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
logger?.info("Retrying proxy routing metadata lookup after invalidation race", {
|
|
366
|
+
lookupKey,
|
|
367
|
+
attempt: attempt + 1,
|
|
368
|
+
generation: routingLookupGeneration,
|
|
369
|
+
});
|
|
370
|
+
if (attempt === MAX_ROUTING_LOOKUP_INVALIDATION_RETRIES) {
|
|
371
|
+
logger?.warn("Proxy routing metadata changed repeatedly during lookup; failing request closed", {
|
|
372
|
+
lookupKey,
|
|
373
|
+
attempts: attempt + 1,
|
|
374
|
+
});
|
|
375
|
+
throw new ProxyRoutingInvalidationRaceError();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
endRoutingLookup(startedAtGeneration);
|
|
380
|
+
}
|
|
281
381
|
}
|
|
282
|
-
return
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
382
|
+
return null;
|
|
383
|
+
})();
|
|
384
|
+
const inflightEntry = {
|
|
385
|
+
generation: routingLookupGeneration,
|
|
386
|
+
promise: lookupPromise,
|
|
387
|
+
};
|
|
388
|
+
routingLookupInflight.set(cacheKey, inflightEntry);
|
|
389
|
+
try {
|
|
390
|
+
return await lookupPromise;
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
if (routingLookupInflight.get(cacheKey) === inflightEntry) {
|
|
394
|
+
routingLookupInflight.delete(cacheKey);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
289
397
|
});
|
|
290
398
|
}
|
|
291
399
|
async function resolveProjectAccessLookup(lookupKey, token, includeUsers, timing) {
|
|
292
400
|
return await profileProxyServerTimingPhase(timing ?? { enabled: false, startedAt: 0, phases: new Map() }, "proxy.access_lookup", () => lookupProjectAccessMetadata(lookupKey, config.apiBaseUrl, token, includeUsers, logger));
|
|
293
401
|
}
|
|
402
|
+
async function invalidateAndConfirmRoutingLookup(input) {
|
|
403
|
+
invalidateRoutingLookup(input);
|
|
404
|
+
const scope = getScope(input.environmentName.toLowerCase());
|
|
405
|
+
const resolveWithToken = async (token) => {
|
|
406
|
+
const result = await resolveProjectRoutingLookup(input.projectSlug, token);
|
|
407
|
+
const environment = result?.environments?.find((candidate) => candidate.id === input.environmentId);
|
|
408
|
+
if (result?.id !== input.projectId ||
|
|
409
|
+
environment?.active_release_id !== input.releaseId) {
|
|
410
|
+
throw new Error(`Proxy routing metadata did not converge for project ${input.projectId} environment ${input.environmentId}`);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
let token = await tokenManager.getToken(scope, input.projectSlug);
|
|
414
|
+
try {
|
|
415
|
+
await resolveWithToken(token);
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
if (!isProxyLookupAuthError(error))
|
|
419
|
+
throw error;
|
|
420
|
+
await tokenManager.invalidateToken(scope, input.projectSlug);
|
|
421
|
+
token = await tokenManager.getToken(scope, input.projectSlug);
|
|
422
|
+
await resolveWithToken(token);
|
|
423
|
+
}
|
|
424
|
+
logger?.info("Proxy routing metadata converged after deployment activation", {
|
|
425
|
+
projectId: input.projectId,
|
|
426
|
+
projectSlug: input.projectSlug,
|
|
427
|
+
deploymentId: input.deploymentId,
|
|
428
|
+
environmentId: input.environmentId,
|
|
429
|
+
environmentName: input.environmentName,
|
|
430
|
+
releaseId: input.releaseId,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
294
433
|
function validateConfig() {
|
|
295
434
|
const missing = [];
|
|
296
435
|
if (!config.apiClientId)
|
|
@@ -410,6 +549,9 @@ export function createProxyHandler(options) {
|
|
|
410
549
|
return await resolveWithCurrentToken();
|
|
411
550
|
}
|
|
412
551
|
catch (error) {
|
|
552
|
+
if (error instanceof ProxyRoutingInvalidationRaceError) {
|
|
553
|
+
return { error: { status: 503, message: error.message } };
|
|
554
|
+
}
|
|
413
555
|
if (!isProxyLookupAuthError(error))
|
|
414
556
|
throw error;
|
|
415
557
|
const projectKey = tokenIdentity.projectSlug ?? tokenIdentity.customDomain;
|
|
@@ -444,6 +586,9 @@ export function createProxyHandler(options) {
|
|
|
444
586
|
return await resolveWithCurrentToken();
|
|
445
587
|
}
|
|
446
588
|
catch (retryError) {
|
|
589
|
+
if (retryError instanceof ProxyRoutingInvalidationRaceError) {
|
|
590
|
+
return { error: { status: 503, message: retryError.message } };
|
|
591
|
+
}
|
|
447
592
|
if (!isProxyLookupAuthError(retryError))
|
|
448
593
|
throw retryError;
|
|
449
594
|
logger?.error("Proxy API token rejected after refresh", retryError, {
|
|
@@ -689,6 +834,8 @@ export function createProxyHandler(options) {
|
|
|
689
834
|
getStats,
|
|
690
835
|
close,
|
|
691
836
|
validateConfig,
|
|
837
|
+
invalidateRoutingLookup,
|
|
838
|
+
invalidateAndConfirmRoutingLookup,
|
|
692
839
|
localProjects,
|
|
693
840
|
};
|
|
694
841
|
}
|
package/esm/src/proxy/main.js
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* - LOCAL_PROJECTS: JSON map of slug → filesystem path (for dev)
|
|
16
16
|
* - CACHE_TYPE: "memory" (default) or "redis"
|
|
17
17
|
* - REDIS_URL: Redis connection URL (required if CACHE_TYPE=redis)
|
|
18
|
+
* - VERYFRONT_PROXY_EXPECTED_REPLICAS: Minimum proxy replicas required to acknowledge routing changes
|
|
19
|
+
* - VERYFRONT_PROXY_ROUTING_INVALIDATION_SECRET: HMAC secret for Redis routing events and acknowledgements
|
|
18
20
|
* - VERYFRONT_API_INTERNAL_URL: API URL for internal endpoints (falls back to VERYFRONT_PROXY_API_BASE_URL)
|
|
19
21
|
* - VERYFRONT_API_INTERNAL_USER: Basic auth user for internal API
|
|
20
22
|
* - VERYFRONT_API_INTERNAL_PASS: Basic auth pass for internal API
|
|
@@ -42,6 +44,8 @@ import { createUpstreamFailureResponse, createUpstreamTimeoutResponse, UPSTREAM_
|
|
|
42
44
|
import { createProxyServerTiming, markProxyServerTimingPhase, profileProxyServerTimingPhase, withProxyServerTimingHeader, } from "./server-timing.js";
|
|
43
45
|
import { removeStickyCookieFromPublicCacheableResponse } from "./response-headers.js";
|
|
44
46
|
import { closeProxyServerWithin, createProxyDrainingResponse, parseProxyDrainTimeoutMs, ProxyRequestDrainTracker, } from "./request-drain.js";
|
|
47
|
+
import { handleProxyRoutingInvalidationRequest, PROXY_ROUTING_INVALIDATION_PATH, } from "./routing-invalidation.js";
|
|
48
|
+
import { startProxyRoutingInvalidationBus } from "./routing-invalidation-redis.js";
|
|
45
49
|
function getLocalProjects() {
|
|
46
50
|
const raw = getEnv("LOCAL_PROJECTS");
|
|
47
51
|
return raw ? JSON.parse(raw) : {};
|
|
@@ -98,6 +102,17 @@ const DEFAULT_SERVER_RETRY_DELAY_MS = 100;
|
|
|
98
102
|
const VERYFRONT_SERVER_RETRY_COUNT = parseInt(getEnv("VERYFRONT_SERVER_RETRY_COUNT") || String(DEFAULT_SERVER_RETRY_COUNT));
|
|
99
103
|
const VERYFRONT_SERVER_RETRY_DELAY_MS = parseInt(getEnv("VERYFRONT_SERVER_RETRY_DELAY_MS") || String(DEFAULT_SERVER_RETRY_DELAY_MS));
|
|
100
104
|
const SHUTDOWN_DRAIN_TIMEOUT_MS = parseProxyDrainTimeoutMs(getEnv("SHUTDOWN_DRAIN_TIMEOUT_MS"), DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS);
|
|
105
|
+
const routingInvalidationSecret = getEnv("VERYFRONT_PROXY_ROUTING_INVALIDATION_SECRET") ?? "";
|
|
106
|
+
const routingInvalidationSecretBytes = new TextEncoder().encode(routingInvalidationSecret).byteLength;
|
|
107
|
+
const expectedReplicasRaw = getEnv("VERYFRONT_PROXY_EXPECTED_REPLICAS");
|
|
108
|
+
const expectedReplicas = Number(expectedReplicasRaw);
|
|
109
|
+
const hasValidExpectedReplicas = Number.isInteger(expectedReplicas) && expectedReplicas > 0;
|
|
110
|
+
if (isProduction() && !hasValidExpectedReplicas) {
|
|
111
|
+
throw new Error("VERYFRONT_PROXY_EXPECTED_REPLICAS must be a positive integer in production");
|
|
112
|
+
}
|
|
113
|
+
if (isProduction() && routingInvalidationSecretBytes < 32) {
|
|
114
|
+
throw new Error("VERYFRONT_PROXY_ROUTING_INVALIDATION_SECRET must contain at least 32 bytes in production");
|
|
115
|
+
}
|
|
101
116
|
const proxyRequestDrainTracker = new ProxyRequestDrainTracker();
|
|
102
117
|
let shuttingDown = false;
|
|
103
118
|
const { createAuthProvider } = await importFirstPartyExtensionModule("ext-auth-jwt", "@veryfront/ext-auth-jwt").catch((error) => {
|
|
@@ -106,16 +121,32 @@ const { createAuthProvider } = await importFirstPartyExtensionModule("ext-auth-j
|
|
|
106
121
|
register("AuthProvider", createAuthProvider({}));
|
|
107
122
|
// Initialize cache and proxy handler
|
|
108
123
|
const cache = await createCacheFromEnv();
|
|
124
|
+
const routingInvalidationLogger = {
|
|
125
|
+
debug: (msg, extra) => proxyLogger.debug(msg, extra),
|
|
126
|
+
info: (msg, extra) => proxyLogger.info(msg, extra),
|
|
127
|
+
warn: (msg, extra) => proxyLogger.warn(msg, extra),
|
|
128
|
+
error: (msg, error, extra) => proxyLogger.error(msg, extra ?? {}, error),
|
|
129
|
+
};
|
|
109
130
|
const proxyHandler = createProxyHandler({
|
|
110
131
|
config,
|
|
111
132
|
cache,
|
|
112
|
-
logger:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
logger: routingInvalidationLogger,
|
|
134
|
+
});
|
|
135
|
+
const routingInvalidationBus = await startProxyRoutingInvalidationBus({
|
|
136
|
+
expectedReplicas: hasValidExpectedReplicas ? expectedReplicas : undefined,
|
|
137
|
+
integritySecret: routingInvalidationSecret,
|
|
138
|
+
logger: routingInvalidationLogger,
|
|
139
|
+
onInvalidate: proxyHandler.invalidateAndConfirmRoutingLookup,
|
|
140
|
+
}).catch((error) => {
|
|
141
|
+
if (isProduction()) {
|
|
142
|
+
throw new Error("Proxy routing invalidation bus failed to start", { cause: error });
|
|
143
|
+
}
|
|
144
|
+
proxyLogger.error("Proxy routing invalidation bus failed; TTL recovery remains active", {}, error instanceof Error ? error : new Error(String(error)));
|
|
145
|
+
return null;
|
|
118
146
|
});
|
|
147
|
+
if (isProduction() && !routingInvalidationBus) {
|
|
148
|
+
throw new Error("Proxy routing invalidation bus requires REDIS_URL and a valid VERYFRONT_PROXY_ROUTING_INVALIDATION_SECRET in production");
|
|
149
|
+
}
|
|
119
150
|
// Validate configuration on startup
|
|
120
151
|
const missingCredentials = proxyHandler.validateConfig();
|
|
121
152
|
if (missingCredentials.length > 0) {
|
|
@@ -499,6 +530,11 @@ async function router(req) {
|
|
|
499
530
|
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
500
531
|
response = await handleWebSocketUpgrade(req, url);
|
|
501
532
|
}
|
|
533
|
+
else if (url.pathname === PROXY_ROUTING_INVALIDATION_PATH) {
|
|
534
|
+
response = await handleProxyRoutingInvalidationRequest(req, {
|
|
535
|
+
publisher: routingInvalidationBus,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
502
538
|
else if (url.pathname === "/_proxy/stats") {
|
|
503
539
|
response = Object.keys(proxyHandler.localProjects).length === 0
|
|
504
540
|
? new Response("Forbidden", { status: 403 })
|
|
@@ -533,6 +569,8 @@ async function shutdown(signal) {
|
|
|
533
569
|
drainTimeoutMs: SHUTDOWN_DRAIN_TIMEOUT_MS,
|
|
534
570
|
});
|
|
535
571
|
try {
|
|
572
|
+
// New requests receive the draining response after shuttingDown is set.
|
|
573
|
+
// Keep this replica subscribed while already-started responses finish.
|
|
536
574
|
const drained = await proxyRequestDrainTracker.waitForDrain(SHUTDOWN_DRAIN_TIMEOUT_MS);
|
|
537
575
|
if (!drained) {
|
|
538
576
|
const now = performance.now();
|
|
@@ -545,6 +583,7 @@ async function shutdown(signal) {
|
|
|
545
583
|
})),
|
|
546
584
|
});
|
|
547
585
|
}
|
|
586
|
+
await routingInvalidationBus?.close();
|
|
548
587
|
const closed = await closeProxyServerWithin(() => server.close(), PROXY_SERVER_CLOSE_TIMEOUT_MS);
|
|
549
588
|
if (!closed) {
|
|
550
589
|
proxyLogger.warn("Proxy server close timed out; process exit will close remaining connections", {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ProxyRoutingInvalidationEvent, ProxyRoutingInvalidationPublisher } from "./routing-invalidation.js";
|
|
2
|
+
type RedisListener = (message: string, channel: string) => void;
|
|
3
|
+
export interface RoutingInvalidationRedisClient {
|
|
4
|
+
connect(): Promise<void>;
|
|
5
|
+
publish(channel: string, message: string): Promise<number>;
|
|
6
|
+
subscribe(channel: string, listener: RedisListener): Promise<number>;
|
|
7
|
+
unsubscribe(channel: string): Promise<number>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
destroy(): void;
|
|
10
|
+
on?(event: "error", listener: (error: unknown) => void): unknown;
|
|
11
|
+
}
|
|
12
|
+
interface RoutingInvalidationLogger {
|
|
13
|
+
info(message: string, extra?: Record<string, unknown>): void;
|
|
14
|
+
warn(message: string, extra?: Record<string, unknown>): void;
|
|
15
|
+
error(message: string, error?: Error, extra?: Record<string, unknown>): void;
|
|
16
|
+
}
|
|
17
|
+
export interface ProxyRoutingInvalidationBus extends ProxyRoutingInvalidationPublisher {
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
interface StartProxyRoutingInvalidationBusOptions {
|
|
21
|
+
acknowledgementTimeoutMs?: number;
|
|
22
|
+
createClient?: (redisUrl: string) => RoutingInvalidationRedisClient | Promise<RoutingInvalidationRedisClient>;
|
|
23
|
+
expectedReplicas?: number;
|
|
24
|
+
now?: () => number;
|
|
25
|
+
logger?: RoutingInvalidationLogger;
|
|
26
|
+
onInvalidate: (event: ProxyRoutingInvalidationEvent) => unknown | Promise<unknown>;
|
|
27
|
+
integritySecret?: string;
|
|
28
|
+
redisUrl?: string;
|
|
29
|
+
replicaId?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function startProxyRoutingInvalidationBus(options: StartProxyRoutingInvalidationBusOptions): Promise<ProxyRoutingInvalidationBus | null>;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=routing-invalidation-redis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing-invalidation-redis.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/routing-invalidation-redis.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,6BAA6B,EAC7B,iCAAiC,EAElC,MAAM,2BAA2B,CAAC;AAgBnC,KAAK,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAGhE,MAAM,WAAW,8BAA8B;IAC7C,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,OAAO,IAAI,IAAI,CAAC;IAChB,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC;CAClE;AAED,UAAU,yBAAyB;IACjC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC7D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9E;AAED,MAAM,WAAW,2BAA4B,SAAQ,iCAAiC;IACpF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,UAAU,uCAAuC;IAC/C,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,YAAY,CAAC,EAAE,CACb,QAAQ,EAAE,MAAM,KACb,8BAA8B,GAAG,OAAO,CAAC,8BAA8B,CAAC,CAAC;IAC9E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,yBAAyB,CAAC;IACnC,YAAY,EAAE,CAAC,KAAK,EAAE,6BAA6B,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAmPD,wBAAsB,gCAAgC,CACpD,OAAO,EAAE,uCAAuC,GAC/C,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC,CAgP7C"}
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "veryfront",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1068",
|
|
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.
|
|
332
|
-
"@veryfront/ext-content-mdx": "0.1.
|
|
333
|
-
"@veryfront/ext-css-tailwind": "0.1.
|
|
334
|
-
"@veryfront/ext-parser-babel": "0.1.
|
|
331
|
+
"@veryfront/ext-bundler-esbuild": "0.1.1068",
|
|
332
|
+
"@veryfront/ext-content-mdx": "0.1.1068",
|
|
333
|
+
"@veryfront/ext-css-tailwind": "0.1.1068",
|
|
334
|
+
"@veryfront/ext-parser-babel": "0.1.1068"
|
|
335
335
|
},
|
|
336
336
|
"devDependencies": {
|
|
337
337
|
"@types/node": "20.9.0"
|