@abloh/core 0.1.2 → 1.0.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/dist/index.d.ts +41404 -2367
- package/dist/index.js +35724 -3455
- package/dist/playground-admission.d.ts +142 -0
- package/dist/playground-admission.js +166 -0
- package/dist/playground-ingress.d.ts +18 -0
- package/dist/playground-ingress.js +62 -0
- package/dist/source-analysis/index.d.ts +714 -0
- package/dist/source-analysis/index.js +1945 -0
- package/package.json +19 -2
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE EXECUTABLE SCHEMA KERNEL: one declaration per wire boundary, everything else derived.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. The fact-divergence audit found the same shape declared twice on twenty-one
|
|
5
|
+
* boundaries: a producer writes the object, a consumer re-types it, and neither reads the other.
|
|
6
|
+
* They agree in one checkout and drift the moment one side changes - and because a customer runs a
|
|
7
|
+
* PINNED Action, a PINNED CLI or a PINNED GitHub App against a service that moved, the drift is not
|
|
8
|
+
* a same-build risk that a test comparing the two copies could catch. Two of these already cost
|
|
9
|
+
* real runs: a runner emitting a valid new state made the service discard every Angular and Deno
|
|
10
|
+
* upload, and an added field made a pinned GitHub App throw away a whole check.
|
|
11
|
+
*
|
|
12
|
+
* WHAT A CONTRACT DECLARES ONCE, and what is derived from it rather than written beside it:
|
|
13
|
+
*
|
|
14
|
+
* the TypeScript type `Infer<typeof C>` - the type IS the schema, not a copy of it
|
|
15
|
+
* the consumer parser `C.parse(value)` - one refusal grammar, one path syntax
|
|
16
|
+
* the producer projection `project(C, value)` - drops what the contract does not carry
|
|
17
|
+
* every discriminator value `C.fields.x.values` - the enum tuple, readable at runtime
|
|
18
|
+
* every limit `C.fields.x.max` - the cap, readable by producer and consumer
|
|
19
|
+
*
|
|
20
|
+
* THE UNKNOWN-FIELD POSTURE IS DECLARED, NOT ASSUMED, and it is the single most important field on
|
|
21
|
+
* an object node. Two different boundaries want opposite answers and both are right:
|
|
22
|
+
*
|
|
23
|
+
* `unknownFields: "refuse"` is an INGEST DOOR. The service stores what it accepts and serves it
|
|
24
|
+
* to a customer surface, so a key it does not know is a producer attaching something the boundary
|
|
25
|
+
* never agreed to carry - a source slice on a row that is supposed to be a path. Refusing is the
|
|
26
|
+
* privacy posture and it must stay.
|
|
27
|
+
*
|
|
28
|
+
* `unknownFields: "ignore"` is a PINNED CONSUMER reading a NEWER PRODUCER. The GitHub App parsing
|
|
29
|
+
* the service's evidence object cannot refuse a field that did not exist when it was released;
|
|
30
|
+
* that exact refusal discarded a whole check. Additive change is the normal direction of travel
|
|
31
|
+
* on these boundaries, and a consumer that cannot survive it is a released-version outage.
|
|
32
|
+
*
|
|
33
|
+
* FAILURE IS A PATH AND A CAUSE, never a boolean. `runResult.fixLoop.summaries[3].verdict` tells a
|
|
34
|
+
* maintainer which field of which row, which is what the hand-written validators were already
|
|
35
|
+
* doing one string concatenation at a time.
|
|
36
|
+
*
|
|
37
|
+
* NOT A VALIDATION LIBRARY. It has exactly the node kinds these boundaries use. A boundary that
|
|
38
|
+
* needs something else adds the node here, once, rather than hand-writing its own validator again.
|
|
39
|
+
*/
|
|
40
|
+
/** Where in the document a value sat, in the syntax a maintainer reads in a refusal. */
|
|
41
|
+
type WirePath = string;
|
|
42
|
+
type WireResult<T> = {
|
|
43
|
+
ok: true;
|
|
44
|
+
value: T;
|
|
45
|
+
} | {
|
|
46
|
+
ok: false;
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* A node in a wire contract.
|
|
51
|
+
*
|
|
52
|
+
* `kind` and the per-kind reflection fields are what make a contract READABLE rather than only
|
|
53
|
+
* runnable: the limits mechanism (F27, F55, F60) needs the cap as a number, and the vocabulary
|
|
54
|
+
* mechanism (F11, F23, F54, F85) needs the enum as a tuple. A validator that only validated would
|
|
55
|
+
* leave those facts owned somewhere else again.
|
|
56
|
+
*/
|
|
57
|
+
interface WireNode<T> {
|
|
58
|
+
readonly kind: string;
|
|
59
|
+
/** Parse an unknown value from the wire. `path` is where it sat, for the refusal sentence. */
|
|
60
|
+
parse(value: unknown, path?: WirePath): WireResult<T>;
|
|
61
|
+
}
|
|
62
|
+
type Infer<N> = N extends WireNode<infer T> ? T : never;
|
|
63
|
+
/**
|
|
64
|
+
* A field of an object contract: its node, and whether the wire may leave it out.
|
|
65
|
+
*
|
|
66
|
+
* `Optional` IS A LITERAL TYPE PARAMETER rather than a `boolean` property, because the inferred
|
|
67
|
+
* TypeScript type is derived from it. Widened to `boolean`, every field reads as required and the
|
|
68
|
+
* generated type stops matching the parser beside it - which would put the two back out of step in
|
|
69
|
+
* the one place this kernel exists to keep them together.
|
|
70
|
+
*/
|
|
71
|
+
interface WireField<T, Optional extends boolean = boolean> {
|
|
72
|
+
readonly node: WireNode<T>;
|
|
73
|
+
readonly optional: Optional;
|
|
74
|
+
}
|
|
75
|
+
type WireFields = Record<string, WireField<unknown, boolean>>;
|
|
76
|
+
type RequiredKeys<F extends WireFields> = {
|
|
77
|
+
[K in keyof F]: F[K]["optional"] extends true ? never : K;
|
|
78
|
+
}[keyof F];
|
|
79
|
+
type OptionalKeys<F extends WireFields> = {
|
|
80
|
+
[K in keyof F]: F[K]["optional"] extends true ? K : never;
|
|
81
|
+
}[keyof F];
|
|
82
|
+
type InferFields<F extends WireFields> = {
|
|
83
|
+
[K in RequiredKeys<F>]: Infer<F[K]["node"]>;
|
|
84
|
+
} & {
|
|
85
|
+
[K in OptionalKeys<F>]?: Infer<F[K]["node"]>;
|
|
86
|
+
};
|
|
87
|
+
/** What a consumer does with a key the contract does not declare. See the file header. */
|
|
88
|
+
type UnknownFieldPosture = "refuse" | "ignore";
|
|
89
|
+
interface WireObject<F extends WireFields> extends WireNode<InferFields<F>> {
|
|
90
|
+
readonly kind: "object";
|
|
91
|
+
readonly fields: F;
|
|
92
|
+
readonly unknownFields: UnknownFieldPosture;
|
|
93
|
+
/** The declared key names, in declaration order - the field allowlist, derived not re-typed. */
|
|
94
|
+
readonly keys: readonly (keyof F & string)[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
declare const PLAYGROUND_HOUR_MS = 3600000;
|
|
98
|
+
declare const PLAYGROUND_DAY_MS: number;
|
|
99
|
+
declare const PLAYGROUND_BROWSER_ACTIVE = 1;
|
|
100
|
+
declare const PLAYGROUND_BROWSER_HOURLY = 3;
|
|
101
|
+
declare const PLAYGROUND_BROWSER_DAILY = 10;
|
|
102
|
+
declare const PLAYGROUND_IP_HOURLY = 30;
|
|
103
|
+
declare const PLAYGROUND_IP_DAILY = 100;
|
|
104
|
+
declare const PLAYGROUND_ADMISSION_ENDINGS: {
|
|
105
|
+
readonly PLAYGROUND_ACTIVE_RUN_LIMIT: "playground-browser-active-limit";
|
|
106
|
+
readonly PLAYGROUND_HOURLY_LIMIT: "playground-hourly-limit";
|
|
107
|
+
readonly PLAYGROUND_DAILY_LIMIT: "playground-browser-daily-limit";
|
|
108
|
+
readonly PLAYGROUND_IP_HOURLY_LIMIT: "playground-ip-hourly-limit";
|
|
109
|
+
readonly PLAYGROUND_IP_DAILY_LIMIT: "playground-ip-daily-limit";
|
|
110
|
+
readonly PLAYGROUND_BUSY: "playground-capacity";
|
|
111
|
+
readonly PLAYGROUND_GLOBAL_HOURLY_LIMIT: "playground-global-hourly-limit";
|
|
112
|
+
readonly PLAYGROUND_VERIFICATION_REQUIRED: "playground-verification-required";
|
|
113
|
+
readonly PLAYGROUND_VERIFICATION_UNAVAILABLE: "playground-verification-unavailable";
|
|
114
|
+
readonly PLAYGROUND_INGRESS_UNAVAILABLE: "playground-ingress-unavailable";
|
|
115
|
+
readonly PLAYGROUND_ADMISSION_UNAVAILABLE: "playground-admission-unavailable";
|
|
116
|
+
};
|
|
117
|
+
type PlaygroundAdmissionCode = keyof typeof PLAYGROUND_ADMISSION_ENDINGS;
|
|
118
|
+
declare const PLAYGROUND_MODEL_LIMITS: Readonly<{
|
|
119
|
+
requests: 8;
|
|
120
|
+
runUsd: 1;
|
|
121
|
+
hourUsd: 1.5;
|
|
122
|
+
dayUsd: 12.5;
|
|
123
|
+
}>;
|
|
124
|
+
declare const playgroundAdmissionErrorContract: WireObject<{
|
|
125
|
+
readonly code: WireField<"PLAYGROUND_ACTIVE_RUN_LIMIT" | "PLAYGROUND_HOURLY_LIMIT" | "PLAYGROUND_DAILY_LIMIT" | "PLAYGROUND_IP_HOURLY_LIMIT" | "PLAYGROUND_IP_DAILY_LIMIT" | "PLAYGROUND_BUSY" | "PLAYGROUND_GLOBAL_HOURLY_LIMIT" | "PLAYGROUND_VERIFICATION_REQUIRED" | "PLAYGROUND_VERIFICATION_UNAVAILABLE" | "PLAYGROUND_INGRESS_UNAVAILABLE" | "PLAYGROUND_ADMISSION_UNAVAILABLE", false>;
|
|
126
|
+
readonly message: WireField<string, false>;
|
|
127
|
+
readonly ending: WireField<"playground-browser-active-limit" | "playground-hourly-limit" | "playground-browser-daily-limit" | "playground-ip-hourly-limit" | "playground-ip-daily-limit" | "playground-capacity" | "playground-global-hourly-limit" | "playground-verification-required" | "playground-verification-unavailable" | "playground-ingress-unavailable" | "playground-admission-unavailable", false>;
|
|
128
|
+
readonly retryAt: WireField<string | null, false>;
|
|
129
|
+
}>;
|
|
130
|
+
type PlaygroundAdmissionFailure = Infer<typeof playgroundAdmissionErrorContract>;
|
|
131
|
+
declare function parsePlaygroundAdmissionFailure(value: unknown): PlaygroundAdmissionFailure | null;
|
|
132
|
+
/** Bump when the worker/public contract required to execute a visitor job changes. */
|
|
133
|
+
declare const PLAYGROUND_WORKER_CONTRACT = "playground-visitor-jobs/v1";
|
|
134
|
+
declare const PLAYGROUND_WORKER_HEARTBEAT_MS = 5000;
|
|
135
|
+
declare const PLAYGROUND_WORKER_LEASE_MS = 20000;
|
|
136
|
+
declare const playgroundReadinessContract: WireObject<{
|
|
137
|
+
readonly contract: WireField<string, false>;
|
|
138
|
+
readonly ready: WireField<boolean, false>;
|
|
139
|
+
}>;
|
|
140
|
+
declare function parsePlaygroundReadiness(value: unknown): boolean;
|
|
141
|
+
|
|
142
|
+
export { PLAYGROUND_ADMISSION_ENDINGS, PLAYGROUND_BROWSER_ACTIVE, PLAYGROUND_BROWSER_DAILY, PLAYGROUND_BROWSER_HOURLY, PLAYGROUND_DAY_MS, PLAYGROUND_HOUR_MS, PLAYGROUND_IP_DAILY, PLAYGROUND_IP_HOURLY, PLAYGROUND_MODEL_LIMITS, PLAYGROUND_WORKER_CONTRACT, PLAYGROUND_WORKER_HEARTBEAT_MS, PLAYGROUND_WORKER_LEASE_MS, type PlaygroundAdmissionCode, type PlaygroundAdmissionFailure, parsePlaygroundAdmissionFailure, parsePlaygroundReadiness, playgroundAdmissionErrorContract, playgroundReadinessContract };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/wire/schema-kernel.ts
|
|
2
|
+
function fail(path, what) {
|
|
3
|
+
return { ok: false, error: path === "" ? what : `${path} ${what}` };
|
|
4
|
+
}
|
|
5
|
+
function child(path, key) {
|
|
6
|
+
return path === "" ? key : `${path}.${key}`;
|
|
7
|
+
}
|
|
8
|
+
function wireEnum(values) {
|
|
9
|
+
const set = new Set(values);
|
|
10
|
+
const has = (value) => set.has(value);
|
|
11
|
+
return {
|
|
12
|
+
kind: "enum",
|
|
13
|
+
values,
|
|
14
|
+
has,
|
|
15
|
+
parse(value, path = "") {
|
|
16
|
+
if (typeof value !== "string") return fail(path, `must be one of ${values.join(", ")}`);
|
|
17
|
+
if (!set.has(value)) return fail(path, `is ${JSON.stringify(value)}, which is not one of ${values.join(", ")}`);
|
|
18
|
+
return { ok: true, value };
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function wireString(options = {}) {
|
|
23
|
+
const min = options.min ?? 0;
|
|
24
|
+
const bound = options.maxBytes !== void 0 ? { unit: "bytes", max: options.maxBytes } : options.max !== void 0 ? { unit: "characters", max: options.max } : void 0;
|
|
25
|
+
return {
|
|
26
|
+
kind: "string",
|
|
27
|
+
...options.max === void 0 ? {} : { max: options.max },
|
|
28
|
+
...options.maxBytes === void 0 ? {} : { maxBytes: options.maxBytes },
|
|
29
|
+
...bound === void 0 ? {} : { bound },
|
|
30
|
+
...options.pattern === void 0 ? {} : { pattern: options.pattern },
|
|
31
|
+
min,
|
|
32
|
+
parse(value, path = "") {
|
|
33
|
+
if (typeof value !== "string") return fail(path, "must be a string");
|
|
34
|
+
if (value.length < min) return fail(path, min === 1 ? "must not be empty" : `must be at least ${min} characters`);
|
|
35
|
+
if (options.max !== void 0 && value.length > options.max) {
|
|
36
|
+
return fail(path, `must be at most ${options.max} characters, and is ${value.length}`);
|
|
37
|
+
}
|
|
38
|
+
if (options.maxBytes !== void 0 && new TextEncoder().encode(value).length > options.maxBytes) {
|
|
39
|
+
return fail(path, `must be at most ${options.maxBytes} bytes, and is ${new TextEncoder().encode(value).length}`);
|
|
40
|
+
}
|
|
41
|
+
if (options.pattern !== void 0 && !options.pattern.test(value)) {
|
|
42
|
+
return fail(path, `must be ${options.patternIs ?? `written as ${String(options.pattern)}`}`);
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, value };
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function wireBoolean() {
|
|
49
|
+
return {
|
|
50
|
+
kind: "boolean",
|
|
51
|
+
parse(value, path = "") {
|
|
52
|
+
if (typeof value !== "boolean") return fail(path, "must be a boolean");
|
|
53
|
+
return { ok: true, value };
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function wireNullable(inner) {
|
|
58
|
+
return {
|
|
59
|
+
kind: "nullable",
|
|
60
|
+
inner,
|
|
61
|
+
parse(value, path = "") {
|
|
62
|
+
if (value === null) return { ok: true, value: null };
|
|
63
|
+
return inner.parse(value, path);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function required(node) {
|
|
68
|
+
return { node, optional: false };
|
|
69
|
+
}
|
|
70
|
+
function wireObject(fields, options) {
|
|
71
|
+
const keys = Object.keys(fields);
|
|
72
|
+
return {
|
|
73
|
+
kind: "object",
|
|
74
|
+
fields,
|
|
75
|
+
unknownFields: options.unknownFields,
|
|
76
|
+
keys,
|
|
77
|
+
parse(value, path = "") {
|
|
78
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return fail(path, "must be an object");
|
|
79
|
+
const record = value;
|
|
80
|
+
if (options.unknownFields === "refuse") {
|
|
81
|
+
for (const key of Object.keys(record)) {
|
|
82
|
+
if (!Object.prototype.hasOwnProperty.call(fields, key)) return fail(child(path, key), "is not a field of this contract");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const out = {};
|
|
86
|
+
for (const key of keys) {
|
|
87
|
+
const field = fields[key];
|
|
88
|
+
const raw = record[key];
|
|
89
|
+
if (raw === void 0) {
|
|
90
|
+
if (!field.optional) return fail(child(path, key), "is required");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const parsed = field.node.parse(raw, child(path, key));
|
|
94
|
+
if (!parsed.ok) return parsed;
|
|
95
|
+
if (parsed.value !== void 0) out[key] = parsed.value;
|
|
96
|
+
}
|
|
97
|
+
return { ok: true, value: out };
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/wire/playground-admission.ts
|
|
103
|
+
var PLAYGROUND_HOUR_MS = 36e5;
|
|
104
|
+
var PLAYGROUND_DAY_MS = 24 * PLAYGROUND_HOUR_MS;
|
|
105
|
+
var PLAYGROUND_BROWSER_ACTIVE = 1;
|
|
106
|
+
var PLAYGROUND_BROWSER_HOURLY = 3;
|
|
107
|
+
var PLAYGROUND_BROWSER_DAILY = 10;
|
|
108
|
+
var PLAYGROUND_IP_HOURLY = 30;
|
|
109
|
+
var PLAYGROUND_IP_DAILY = 100;
|
|
110
|
+
var PLAYGROUND_ADMISSION_ENDINGS = {
|
|
111
|
+
PLAYGROUND_ACTIVE_RUN_LIMIT: "playground-browser-active-limit",
|
|
112
|
+
PLAYGROUND_HOURLY_LIMIT: "playground-hourly-limit",
|
|
113
|
+
PLAYGROUND_DAILY_LIMIT: "playground-browser-daily-limit",
|
|
114
|
+
PLAYGROUND_IP_HOURLY_LIMIT: "playground-ip-hourly-limit",
|
|
115
|
+
PLAYGROUND_IP_DAILY_LIMIT: "playground-ip-daily-limit",
|
|
116
|
+
PLAYGROUND_BUSY: "playground-capacity",
|
|
117
|
+
PLAYGROUND_GLOBAL_HOURLY_LIMIT: "playground-global-hourly-limit",
|
|
118
|
+
PLAYGROUND_VERIFICATION_REQUIRED: "playground-verification-required",
|
|
119
|
+
PLAYGROUND_VERIFICATION_UNAVAILABLE: "playground-verification-unavailable",
|
|
120
|
+
PLAYGROUND_INGRESS_UNAVAILABLE: "playground-ingress-unavailable",
|
|
121
|
+
PLAYGROUND_ADMISSION_UNAVAILABLE: "playground-admission-unavailable"
|
|
122
|
+
};
|
|
123
|
+
var PLAYGROUND_MODEL_LIMITS = Object.freeze({ requests: 8, runUsd: 1, hourUsd: 1.5, dayUsd: 12.5 });
|
|
124
|
+
var playgroundAdmissionErrorContract = wireObject({
|
|
125
|
+
code: required(wireEnum(Object.keys(PLAYGROUND_ADMISSION_ENDINGS))),
|
|
126
|
+
message: required(wireString({ min: 1, max: 1024 })),
|
|
127
|
+
ending: required(wireEnum(Object.values(PLAYGROUND_ADMISSION_ENDINGS))),
|
|
128
|
+
retryAt: required(wireNullable(wireString({ min: 24, max: 24 })))
|
|
129
|
+
}, { unknownFields: "refuse" });
|
|
130
|
+
function parsePlaygroundAdmissionFailure(value) {
|
|
131
|
+
const parsed = playgroundAdmissionErrorContract.parse(value);
|
|
132
|
+
if (!parsed.ok) return null;
|
|
133
|
+
const { code, ending, retryAt } = parsed.value;
|
|
134
|
+
if (PLAYGROUND_ADMISSION_ENDINGS[code] !== ending) return null;
|
|
135
|
+
if (retryAt !== null && (!Number.isFinite(Date.parse(retryAt)) || new Date(retryAt).toISOString() !== retryAt)) return null;
|
|
136
|
+
return parsed.value;
|
|
137
|
+
}
|
|
138
|
+
var PLAYGROUND_WORKER_CONTRACT = "playground-visitor-jobs/v1";
|
|
139
|
+
var PLAYGROUND_WORKER_HEARTBEAT_MS = 5e3;
|
|
140
|
+
var PLAYGROUND_WORKER_LEASE_MS = 2e4;
|
|
141
|
+
var playgroundReadinessContract = wireObject({
|
|
142
|
+
contract: required(wireString({ min: 1, max: 100 })),
|
|
143
|
+
ready: required(wireBoolean())
|
|
144
|
+
}, { unknownFields: "refuse" });
|
|
145
|
+
function parsePlaygroundReadiness(value) {
|
|
146
|
+
const parsed = playgroundReadinessContract.parse(value);
|
|
147
|
+
return parsed.ok && parsed.value.contract === PLAYGROUND_WORKER_CONTRACT && parsed.value.ready;
|
|
148
|
+
}
|
|
149
|
+
export {
|
|
150
|
+
PLAYGROUND_ADMISSION_ENDINGS,
|
|
151
|
+
PLAYGROUND_BROWSER_ACTIVE,
|
|
152
|
+
PLAYGROUND_BROWSER_DAILY,
|
|
153
|
+
PLAYGROUND_BROWSER_HOURLY,
|
|
154
|
+
PLAYGROUND_DAY_MS,
|
|
155
|
+
PLAYGROUND_HOUR_MS,
|
|
156
|
+
PLAYGROUND_IP_DAILY,
|
|
157
|
+
PLAYGROUND_IP_HOURLY,
|
|
158
|
+
PLAYGROUND_MODEL_LIMITS,
|
|
159
|
+
PLAYGROUND_WORKER_CONTRACT,
|
|
160
|
+
PLAYGROUND_WORKER_HEARTBEAT_MS,
|
|
161
|
+
PLAYGROUND_WORKER_LEASE_MS,
|
|
162
|
+
parsePlaygroundAdmissionFailure,
|
|
163
|
+
parsePlaygroundReadiness,
|
|
164
|
+
playgroundAdmissionErrorContract,
|
|
165
|
+
playgroundReadinessContract
|
|
166
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare const PLAYGROUND_INGRESS_HEADER = "x-playground-ingress";
|
|
2
|
+
declare const PLAYGROUND_INGRESS_SECRET_KEY = "ABLOH_PLAYGROUND_INGRESS_SECRET";
|
|
3
|
+
declare const PLAYGROUND_CHALLENGE_HEADER = "x-playground-challenge";
|
|
4
|
+
declare const PLAYGROUND_CHALLENGE_ACTION = "playground-run";
|
|
5
|
+
declare function canonicalPlaygroundIp(value: string | null | undefined): string | null;
|
|
6
|
+
/** Only Vercel's overwritten header is authoritative in this deployment, never generic XFF. */
|
|
7
|
+
declare function playgroundProxyIp(headers: {
|
|
8
|
+
get(name: string): string | null;
|
|
9
|
+
}, environment: {
|
|
10
|
+
VERCEL?: string;
|
|
11
|
+
}): string | null;
|
|
12
|
+
declare function playgroundRequestDigest(body: unknown, visitor: string, key: string): string;
|
|
13
|
+
declare function signPlaygroundIngress(ipValue: string, digest: string, secret: string, now?: number): string | null;
|
|
14
|
+
declare function verifyPlaygroundIngress(token: string | undefined, digest: string, secret: string, now?: number): string | null;
|
|
15
|
+
/** Store only today's digest; query today and yesterday across the rolling 24-hour window. */
|
|
16
|
+
declare function playgroundIpKeys(ip: string, secret: string, now: number): readonly [string, string];
|
|
17
|
+
|
|
18
|
+
export { PLAYGROUND_CHALLENGE_ACTION, PLAYGROUND_CHALLENGE_HEADER, PLAYGROUND_INGRESS_HEADER, PLAYGROUND_INGRESS_SECRET_KEY, canonicalPlaygroundIp, playgroundIpKeys, playgroundProxyIp, playgroundRequestDigest, signPlaygroundIngress, verifyPlaygroundIngress };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/playground-ingress.ts
|
|
2
|
+
import { createHash, createHmac, timingSafeEqual } from "crypto";
|
|
3
|
+
import { isIP } from "net";
|
|
4
|
+
var PLAYGROUND_INGRESS_HEADER = "x-playground-ingress";
|
|
5
|
+
var PLAYGROUND_INGRESS_SECRET_KEY = "ABLOH_PLAYGROUND_INGRESS_SECRET";
|
|
6
|
+
var PLAYGROUND_CHALLENGE_HEADER = "x-playground-challenge";
|
|
7
|
+
var PLAYGROUND_CHALLENGE_ACTION = "playground-run";
|
|
8
|
+
var DAY_MS = 864e5;
|
|
9
|
+
var MAX_AGE_MS = 6e4;
|
|
10
|
+
function canonicalPlaygroundIp(value) {
|
|
11
|
+
if (!value || !isIP(value) || value.includes("%")) return null;
|
|
12
|
+
if (isIP(value) === 4) return value;
|
|
13
|
+
const canonical = new URL(`http://[${value}]/`).hostname.slice(1, -1);
|
|
14
|
+
const mapped = /^::ffff:([0-9a-f]+):([0-9a-f]+)$/u.exec(canonical);
|
|
15
|
+
if (!mapped) return canonical;
|
|
16
|
+
const high = parseInt(mapped[1], 16), low = parseInt(mapped[2], 16);
|
|
17
|
+
return `${high >>> 8}.${high & 255}.${low >>> 8}.${low & 255}`;
|
|
18
|
+
}
|
|
19
|
+
function playgroundProxyIp(headers, environment) {
|
|
20
|
+
return environment.VERCEL === "1" ? canonicalPlaygroundIp(headers.get("x-vercel-forwarded-for")) : null;
|
|
21
|
+
}
|
|
22
|
+
function playgroundRequestDigest(body, visitor, key) {
|
|
23
|
+
return createHash("sha256").update(JSON.stringify(["playground-new-run-v1", visitor, key, body])).digest("hex");
|
|
24
|
+
}
|
|
25
|
+
var mac = (key, text) => createHmac("sha256", key).update(text).digest("base64url");
|
|
26
|
+
function signPlaygroundIngress(ipValue, digest, secret, now = Date.now()) {
|
|
27
|
+
const ip = canonicalPlaygroundIp(ipValue);
|
|
28
|
+
if (!ip || secret.length < 32) return null;
|
|
29
|
+
const payload = Buffer.from(JSON.stringify({ ip, digest, at: now })).toString("base64url");
|
|
30
|
+
return `${payload}.${mac(secret, payload)}`;
|
|
31
|
+
}
|
|
32
|
+
function verifyPlaygroundIngress(token, digest, secret, now = Date.now()) {
|
|
33
|
+
if (!token || token.length > 1024 || secret.length < 32) return null;
|
|
34
|
+
const [payload, signature, extra] = token.split(".");
|
|
35
|
+
if (!payload || !signature || extra !== void 0) return null;
|
|
36
|
+
const expected = Buffer.from(mac(secret, payload)), actual = Buffer.from(signature);
|
|
37
|
+
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) return null;
|
|
38
|
+
try {
|
|
39
|
+
const value = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
40
|
+
if (value.digest !== digest || !Number.isSafeInteger(value.at) || value.at > now + 5e3 || now - value.at > MAX_AGE_MS) return null;
|
|
41
|
+
return canonicalPlaygroundIp(value.ip);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function playgroundIpKeys(ip, secret, now) {
|
|
47
|
+
const epoch = Math.floor(now / DAY_MS);
|
|
48
|
+
const key = (day) => mac(mac(secret, `playground-ip-day:${day}`), ip);
|
|
49
|
+
return [key(epoch), key(epoch - 1)];
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
PLAYGROUND_CHALLENGE_ACTION,
|
|
53
|
+
PLAYGROUND_CHALLENGE_HEADER,
|
|
54
|
+
PLAYGROUND_INGRESS_HEADER,
|
|
55
|
+
PLAYGROUND_INGRESS_SECRET_KEY,
|
|
56
|
+
canonicalPlaygroundIp,
|
|
57
|
+
playgroundIpKeys,
|
|
58
|
+
playgroundProxyIp,
|
|
59
|
+
playgroundRequestDigest,
|
|
60
|
+
signPlaygroundIngress,
|
|
61
|
+
verifyPlaygroundIngress
|
|
62
|
+
};
|