@specific.dev/spectest 0.98.0 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/replay-key.d.ts +27 -0
- package/dist/components/replay-key.js +96 -0
- package/dist/components/replayFake.d.ts +12 -17
- package/dist/components/replayFake.js +51 -50
- package/dist/components/supabase.d.ts +5 -1
- package/dist/components/supabase.js +3 -1
- package/dist/daemon.js +16 -22
- package/dist/harness/http-proxy.d.ts +9 -9
- package/dist/harness/http-proxy.js +14 -9
- package/dist/harness/ingress-table.d.ts +2 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +3 -0
- package/dist/ingress.d.ts +8 -0
- package/dist/ingress.js +9 -2
- package/package.json +1 -1
- package/src/components/index.ts +4 -1
- package/src/components/replay-key.ts +127 -0
- package/src/components/replayFake-record.test.ts +114 -0
- package/src/components/replayFake.test.ts +254 -0
- package/src/components/replayFake.ts +59 -65
- package/src/components/supabase.ts +8 -1
- package/src/daemon.ts +17 -25
- package/src/harness/http-proxy.test.ts +2 -1
- package/src/harness/http-proxy.ts +20 -9
- package/src/harness/ingress-cors.test.ts +184 -0
- package/src/harness/ingress-table.ts +2 -1
- package/src/index.ts +10 -1
- package/src/ingress.ts +18 -6
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* yet.
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
|
+
import type { CorsMode } from "../ingress.js";
|
|
45
46
|
import { certCovers, isWildcard, wildcardSuffix } from "./hostmatch";
|
|
46
47
|
|
|
47
48
|
/**
|
|
@@ -53,7 +54,7 @@ import { certCovers, isWildcard, wildcardSuffix } from "./hostmatch";
|
|
|
53
54
|
*/
|
|
54
55
|
export type Route<F = unknown> =
|
|
55
56
|
| { kind: "fake"; fake: F }
|
|
56
|
-
| { kind: "proxy"; service: string; port: number; http2?: boolean };
|
|
57
|
+
| { kind: "proxy"; service: string; port: number; http2?: boolean; cors?: CorsMode };
|
|
57
58
|
|
|
58
59
|
/** A PEM leaf + key, as handed to `Bun.serve`'s TLS config. */
|
|
59
60
|
export interface Leaf {
|
package/src/index.ts
CHANGED
|
@@ -190,6 +190,7 @@ export {
|
|
|
190
190
|
SELF_SERVICE_TOKEN,
|
|
191
191
|
} from "./ingress.js";
|
|
192
192
|
export type {
|
|
193
|
+
CorsMode,
|
|
193
194
|
CertificateDecl,
|
|
194
195
|
DnsDecl,
|
|
195
196
|
ProxyDecl,
|
|
@@ -197,7 +198,7 @@ export type {
|
|
|
197
198
|
DnsTarget,
|
|
198
199
|
LoweredIngress,
|
|
199
200
|
} from "./ingress.js";
|
|
200
|
-
import type
|
|
201
|
+
import { validateCorsMode, type CorsMode, type DnsTarget } from "./ingress.js";
|
|
201
202
|
import type {
|
|
202
203
|
InterceptHandler,
|
|
203
204
|
InterceptNext,
|
|
@@ -1164,6 +1165,9 @@ export interface ServiceTls {
|
|
|
1164
1165
|
/** Accept HTTP/2 over TLS and forward it as plaintext HTTP/2 (h2c).
|
|
1165
1166
|
* Required for gRPC. HTTP/1.1 continues to use the existing HTTP proxy. */
|
|
1166
1167
|
http2?: boolean;
|
|
1168
|
+
/** Preserve upstream CORS policy by default. "permissive" opts HTTP/1
|
|
1169
|
+
* ingress into answering preflights and adding credentialed CORS headers. */
|
|
1170
|
+
cors?: CorsMode;
|
|
1167
1171
|
}
|
|
1168
1172
|
|
|
1169
1173
|
/** @internal Normalized wire representation; legacy definitions remain accepted. */
|
|
@@ -1665,6 +1669,7 @@ function validateEnvironmentConfig<S extends ServicesMap>(
|
|
|
1665
1669
|
);
|
|
1666
1670
|
}
|
|
1667
1671
|
if (entry.http2 !== undefined && typeof entry.http2 !== "boolean") throw new Error(`service "${name}" tls http2 must be a boolean`);
|
|
1672
|
+
validateCorsMode(entry.cors);
|
|
1668
1673
|
const h = entry.hostname.toLowerCase();
|
|
1669
1674
|
if (!HOSTNAME_RE.test(hostPatternBody(h))) {
|
|
1670
1675
|
throw new Error(
|
|
@@ -2144,6 +2149,9 @@ export interface FakeDefinition<
|
|
|
2144
2149
|
hostnames: readonly string[];
|
|
2145
2150
|
/** TCP port the fake listens on. Default `80`. */
|
|
2146
2151
|
port?: number;
|
|
2152
|
+
/** Preserve handler/interceptor CORS policy by default. "permissive"
|
|
2153
|
+
* answers preflights and adds credentialed CORS headers at ingress. */
|
|
2154
|
+
cors?: CorsMode;
|
|
2147
2155
|
/**
|
|
2148
2156
|
* Build the fake's initial state. Called once when the project loads.
|
|
2149
2157
|
* The returned value lives in daemon memory for the rest of the
|
|
@@ -2227,6 +2235,7 @@ export function defineFake<
|
|
|
2227
2235
|
);
|
|
2228
2236
|
}
|
|
2229
2237
|
}
|
|
2238
|
+
validateCorsMode(opts.cors);
|
|
2230
2239
|
if (typeof opts.handler !== "function") {
|
|
2231
2240
|
throw new Error(`defineFake(${opts.name}): \`handler\` is required`);
|
|
2232
2241
|
}
|
package/src/ingress.ts
CHANGED
|
@@ -25,6 +25,17 @@ import type { FakeDefinition, Project, ServiceConfig } from "./index.js";
|
|
|
25
25
|
* live IP on spectest-net (a plain peer alias, no daemon hop). */
|
|
26
26
|
export type DnsTarget = { ingress: true } | { service: string };
|
|
27
27
|
|
|
28
|
+
/** Passthrough preserves application CORS policy (the default). Permissive
|
|
29
|
+
* answers preflights and adds credentialed CORS headers at HTTP/1 ingress. */
|
|
30
|
+
export type CorsMode = "passthrough" | "permissive";
|
|
31
|
+
|
|
32
|
+
/** @internal Validate JavaScript config as well as TypeScript callers. */
|
|
33
|
+
export function validateCorsMode(cors: unknown): void {
|
|
34
|
+
if (cors !== undefined && cors !== "passthrough" && cors !== "permissive") {
|
|
35
|
+
throw new Error('cors must be "passthrough" or "permissive"');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
export interface CertificateDecl {
|
|
29
40
|
kind: "certificate";
|
|
30
41
|
/** Hostnames the leaf cert's SANs cover. One cert is minted per decl. */
|
|
@@ -40,7 +51,7 @@ export interface DnsDecl {
|
|
|
40
51
|
export interface ProxyDecl {
|
|
41
52
|
kind: "proxy";
|
|
42
53
|
hostname: string;
|
|
43
|
-
upstream: { service: string; port: number; http2?: boolean };
|
|
54
|
+
upstream: { service: string; port: number; http2?: boolean; cors?: CorsMode };
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
export type IngressDecl = CertificateDecl | DnsDecl | ProxyDecl;
|
|
@@ -137,7 +148,7 @@ export function dnsName(hostname: string, target: DnsTarget): DnsDecl {
|
|
|
137
148
|
*/
|
|
138
149
|
export function proxy(
|
|
139
150
|
hostname: string,
|
|
140
|
-
upstream: { service: string; port: number; http2?: boolean },
|
|
151
|
+
upstream: { service: string; port: number; http2?: boolean; cors?: CorsMode },
|
|
141
152
|
): ProxyDecl {
|
|
142
153
|
assertNameOrWildcard(hostname, "proxy()");
|
|
143
154
|
if (!upstream || typeof upstream.service !== "string" || typeof upstream.port !== "number") {
|
|
@@ -146,6 +157,7 @@ export function proxy(
|
|
|
146
157
|
);
|
|
147
158
|
}
|
|
148
159
|
if (upstream.http2 !== undefined && typeof upstream.http2 !== "boolean") throw new Error("proxy(): http2 must be a boolean");
|
|
160
|
+
validateCorsMode(upstream.cors);
|
|
149
161
|
return { kind: "proxy", hostname: hostname.toLowerCase(), upstream };
|
|
150
162
|
}
|
|
151
163
|
|
|
@@ -198,7 +210,7 @@ export interface LoweredIngress {
|
|
|
198
210
|
/** One leaf cert per group; SANs = the group's hostnames. */
|
|
199
211
|
certificates: { hostnames: string[] }[];
|
|
200
212
|
/** Reverse-proxy routes: hostname → upstream service:port. */
|
|
201
|
-
proxies: { hostname: string; service: string; port: number; http2?: boolean }[];
|
|
213
|
+
proxies: { hostname: string; service: string; port: number; http2?: boolean; cors?: CorsMode }[];
|
|
202
214
|
/** Hostnames that resolve to the daemon ingress gateway (DNS registry
|
|
203
215
|
* + `--add-host` on every container). Covers fakes, TLS proxies, and
|
|
204
216
|
* any `dnsName(h, { ingress: true })`. */
|
|
@@ -227,7 +239,7 @@ function resolveSelf(service: string, selfKey: string): string {
|
|
|
227
239
|
*/
|
|
228
240
|
export function lowerIngress(project: Project): LoweredIngress {
|
|
229
241
|
const certificates: { hostnames: string[] }[] = [];
|
|
230
|
-
const proxies: { hostname: string; service: string; port: number; http2?: boolean }[] = [];
|
|
242
|
+
const proxies: { hostname: string; service: string; port: number; http2?: boolean; cors?: CorsMode }[] = [];
|
|
231
243
|
const ingressSet = new Set<string>();
|
|
232
244
|
const aliasesByService: Record<string, string[]> = {};
|
|
233
245
|
const wildcards: { pattern: string; target: DnsTarget }[] = [];
|
|
@@ -242,7 +254,7 @@ export function lowerIngress(project: Project): LoweredIngress {
|
|
|
242
254
|
break;
|
|
243
255
|
case "proxy": {
|
|
244
256
|
const service = resolveSelf(decl.upstream.service, selfKey);
|
|
245
|
-
proxies.push({ hostname: decl.hostname, service, port: decl.upstream.port, ...(decl.upstream.http2 ? { http2: true } : {}) });
|
|
257
|
+
proxies.push({ hostname: decl.hostname, service, port: decl.upstream.port, ...(decl.upstream.http2 ? { http2: true } : {}), ...(decl.upstream.cors ? { cors: decl.upstream.cors } : {}) });
|
|
246
258
|
// A proxied hostname must route to the daemon. A wildcard one can
|
|
247
259
|
// only be answered by the resolver (there is no `--add-host` for a
|
|
248
260
|
// pattern), so it goes in the wildcard table pointed at the ingress.
|
|
@@ -279,7 +291,7 @@ export function lowerIngress(project: Project): LoweredIngress {
|
|
|
279
291
|
// `tls: [{ hostname, port }]` → cert + TLS-terminated reverse proxy.
|
|
280
292
|
for (const entry of (svc as ServiceConfig).tls ?? []) {
|
|
281
293
|
applyDecl(certificate([entry.hostname]), name);
|
|
282
|
-
applyDecl(proxy(entry.hostname, { service: name, port: entry.port, ...(entry.http2 ? { http2: true } : {}) }), name);
|
|
294
|
+
applyDecl(proxy(entry.hostname, { service: name, port: entry.port, ...(entry.http2 ? { http2: true } : {}), ...(entry.cors ? { cors: entry.cors } : {}) }), name);
|
|
283
295
|
}
|
|
284
296
|
// `hostnames: [h]` → plain peer alias (no daemon hop).
|
|
285
297
|
for (const h of (svc as ServiceConfig).hostnames ?? []) {
|