@greatwait/contracts 0.3.0 → 0.3.2
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/attempt-diagnostics.d.ts +1 -1
- package/dist/attempt-diagnostics.js +13 -1
- package/dist/attempt-search.d.ts +2 -0
- package/dist/attempt-search.js +22 -19
- package/dist/authority.js +9 -9
- package/dist/billing.d.ts +46 -0
- package/dist/billing.js +29 -0
- package/dist/callback-safety.js +5 -1
- package/dist/configuration.d.ts +6 -2
- package/dist/configuration.js +93 -52
- package/dist/delivery.d.ts +13 -1
- package/dist/delivery.js +2 -0
- package/dist/effective-limits.d.ts +4 -4
- package/dist/endpoints.d.ts +29 -0
- package/dist/endpoints.js +57 -26
- package/dist/forecast.d.ts +2 -2
- package/dist/http-request.d.ts +12 -2
- package/dist/http-request.js +56 -24
- package/dist/identifiers.js +20 -20
- package/dist/index.d.ts +8 -7
- package/dist/index.js +8 -7
- package/dist/mcp.js +1 -0
- package/dist/operational-config.d.ts +174 -22
- package/dist/operational-config.js +213 -79
- package/dist/operations.d.ts +334 -52
- package/dist/operations.js +195 -34
- package/dist/project-notifications.d.ts +35 -9
- package/dist/project-notifications.js +43 -12
- package/dist/protocol.d.ts +28 -0
- package/dist/protocol.js +56 -33
- package/dist/schedule-selection.js +2 -2
- package/dist/scheduling-records.d.ts +106 -16
- package/dist/scheduling-records.js +36 -12
- package/dist/scheduling.d.ts +200 -43
- package/dist/scheduling.js +280 -131
- package/dist/service-mode.d.ts +88 -0
- package/dist/service-mode.js +130 -0
- package/dist/signatures.d.ts +6 -6
- package/dist/signatures.js +24 -24
- package/dist/stored-http.d.ts +18 -0
- package/dist/stored-http.js +106 -0
- package/dist/tenancy.d.ts +54 -30
- package/dist/tenancy.js +35 -17
- package/dist/time.js +7 -7
- package/dist/timing.d.ts +39 -0
- package/dist/timing.js +51 -0
- package/dist/zod-values.d.ts +1 -1
- package/dist/zod-values.js +1 -1
- package/dist/zod.d.ts +1 -1
- package/dist/zod.js +1 -1
- package/generated/v1/examples/billing.noticesdismiss.request.json +3 -0
- package/generated/v1/examples/billing.noticesdismiss.response.json +6 -0
- package/generated/v1/examples/billing.reconcile.response.json +12 -0
- package/generated/v1/examples/billing.summary.response.json +12 -0
- package/generated/v1/examples/executions.list.response.json +1 -0
- package/generated/v1/examples/limits.read.response.json +1 -1
- package/generated/v1/examples/projectnotifications.rotateSigningKey.response.json +1 -0
- package/generated/v1/examples/projects.list.response.json +0 -6
- package/generated/v1/examples/projects.read.response.json +14 -0
- package/generated/v1/examples/schedules.list.response.json +1 -3
- package/generated/v1/examples/schedules.summary.response.json +174 -0
- package/generated/v1/examples/timing.summary.response.json +33 -1
- package/generated/v1/manifest.json +79 -2
- package/generated/v1/openapi.json +4084 -2023
- package/generated/v1/policy-matrix.json +22 -1
- package/generated/v1/schemas/attempts.list.response.json +10 -0
- package/generated/v1/schemas/attempts.read.response.json +10 -0
- package/generated/v1/schemas/attempts.search.response.json +4 -0
- package/generated/v1/schemas/billing.notices.query.json +4 -0
- package/generated/v1/schemas/billing.noticesdismiss.request.json +12 -0
- package/generated/v1/schemas/billing.noticesdismiss.response.json +22 -0
- package/generated/v1/schemas/billing.reconcile.response.json +43 -0
- package/generated/v1/schemas/billing.summary.response.json +43 -0
- package/generated/v1/schemas/endpoints.list.query.json +15 -0
- package/generated/v1/schemas/executions.list.response.json +4 -0
- package/generated/v1/schemas/occurrences.list.response.json +4 -0
- package/generated/v1/schemas/occurrences.read.response.json +4 -0
- package/generated/v1/schemas/occurrences.redeliver.response.json +10 -0
- package/generated/v1/schemas/projectnotifications.read.response.json +4 -0
- package/generated/v1/schemas/projectnotifications.rotateSigningKey.response.json +4 -0
- package/generated/v1/schemas/projectnotifications.update.request.json +41 -17
- package/generated/v1/schemas/projectnotifications.update.response.json +4 -0
- package/generated/v1/schemas/projects.list.response.json +0 -27
- package/generated/v1/schemas/projects.read.response-headers.json +11 -0
- package/generated/v1/schemas/projects.read.response.json +79 -0
- package/generated/v1/schemas/schedules.archive.response.json +8 -0
- package/generated/v1/schemas/schedules.cancel.response.json +8 -0
- package/generated/v1/schemas/schedules.create.request.json +1 -1
- package/generated/v1/schemas/schedules.create.response.json +8 -0
- package/generated/v1/schemas/schedules.list.query.json +6 -0
- package/generated/v1/schemas/schedules.list.response.json +4 -0
- package/generated/v1/schemas/schedules.pause.response.json +8 -0
- package/generated/v1/schemas/schedules.read.response.json +8 -0
- package/generated/v1/schemas/schedules.replace.request.json +1 -1
- package/generated/v1/schemas/schedules.replace.response.json +8 -0
- package/generated/v1/schemas/schedules.resume.response.json +8 -0
- package/generated/v1/schemas/schedules.summary.query.json +11 -0
- package/generated/v1/schemas/schedules.summary.response.json +144 -0
- package/generated/v1/schemas/timing.summary.response.json +69 -0
- package/generated/v1/snippets/billing.noticesdismiss.json +6 -0
- package/generated/v1/snippets/projectnotifications.rotateSigningKey.json +1 -1
- package/generated/v1/snippets/projects.read.json +6 -0
- package/generated/v1/snippets/schedules.summary.json +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** The modes a request may name. `auto` is what omission means. */
|
|
2
|
+
export declare const REQUESTED_LATENCY_CLASSES: readonly ["auto", "bulk", "realtime"];
|
|
3
|
+
export type RequestedLatencyClass = (typeof REQUESTED_LATENCY_CLASSES)[number];
|
|
4
|
+
/** The concrete services an occurrence resolves to. */
|
|
5
|
+
export type ResolvedLatencyClass = "bulk" | "realtime";
|
|
6
|
+
/** The internal bands. Not on the wire: only the concrete service is. */
|
|
7
|
+
export declare const SERVICE_BANDS: readonly ["realtime", "short_bulk", "bulk"];
|
|
8
|
+
export type ServiceBand = (typeof SERVICE_BANDS)[number];
|
|
9
|
+
export interface ServiceResolution {
|
|
10
|
+
readonly band: ServiceBand;
|
|
11
|
+
readonly latencyClass: ResolvedLatencyClass;
|
|
12
|
+
readonly quantumMs: number;
|
|
13
|
+
}
|
|
14
|
+
/** The concrete service a band is charged and reported as. */
|
|
15
|
+
export declare function bandLatencyClass(band: ServiceBand): ResolvedLatencyClass;
|
|
16
|
+
/**
|
|
17
|
+
* The service, band and quantum for one occurrence.
|
|
18
|
+
*
|
|
19
|
+
* `spacingMs` is the one-off lead or the recurrence's shorter adjacent gap
|
|
20
|
+
* (see the module comment). A non-finite spacing — a recurrence with no
|
|
21
|
+
* adjacent instant — is treated as long.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveService(requested: RequestedLatencyClass, spacingMs: number): ServiceResolution;
|
|
24
|
+
/** A one-off's lead: planned instant minus the captured acceptance time. */
|
|
25
|
+
export declare function oneOffSpacingMs(acceptedAtMs: number, plannedForMs: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* A recurrence occurrence's shorter adjacent planned gap. Either neighbour may
|
|
28
|
+
* be unknown — a series' first occurrence has no predecessor — and the gap to
|
|
29
|
+
* a known neighbour decides alone.
|
|
30
|
+
*/
|
|
31
|
+
export declare function recurrenceSpacingMs(previousMs: number | undefined, plannedForMs: number, nextMs: number | undefined): number;
|
|
32
|
+
/**
|
|
33
|
+
* An instant's first eligibility in its band: `ceil(instant / quantum) *
|
|
34
|
+
* quantum`, so a boundary stays where it is.
|
|
35
|
+
*
|
|
36
|
+
* Applied once, to the planned instant or to a retry's backed-off instant.
|
|
37
|
+
* Work that becomes claimable late is claimable now, never at another
|
|
38
|
+
* boundary: nothing rounds a rounded instant, and nothing rounds `now`. The
|
|
39
|
+
* last quantum of year 9999 has no boundary the contract can name, so it is
|
|
40
|
+
* eligible at the end of that year instead.
|
|
41
|
+
*/
|
|
42
|
+
export declare function roundEligibilityMs(instantMs: number, band: ServiceBand): number;
|
|
43
|
+
export interface OccurrenceResolution extends ServiceResolution {
|
|
44
|
+
/** `plannedForMs` rounded up to the band's quantum. */
|
|
45
|
+
readonly eligibleAtMs: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One occurrence's service, band and rounded first eligibility: the entry
|
|
49
|
+
* point create, replacement and a recurrence's successor all use.
|
|
50
|
+
*
|
|
51
|
+
* A one-off is measured from its captured acceptance time; a recurrence by its
|
|
52
|
+
* neighbours, which the caller took from the existing recurrence evaluator.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveOccurrence(input: {
|
|
55
|
+
readonly kind: "one-off";
|
|
56
|
+
readonly requested: RequestedLatencyClass;
|
|
57
|
+
readonly acceptedAtMs: number;
|
|
58
|
+
readonly plannedForMs: number;
|
|
59
|
+
} | {
|
|
60
|
+
readonly kind: "recurring";
|
|
61
|
+
readonly requested: RequestedLatencyClass;
|
|
62
|
+
readonly plannedForMs: number;
|
|
63
|
+
readonly previousMs?: number;
|
|
64
|
+
readonly nextMs?: number;
|
|
65
|
+
}): OccurrenceResolution;
|
|
66
|
+
/** Stored requested intent; `legacy` predates recording it (W12-02). */
|
|
67
|
+
export type StoredRequestedLatencyClass = RequestedLatencyClass | "legacy";
|
|
68
|
+
/**
|
|
69
|
+
* What a replacement runs under (implementation-guide § W02, step 4).
|
|
70
|
+
*
|
|
71
|
+
* - A request naming a mode other than the stored one is a mode edit.
|
|
72
|
+
* - A timing or cadence edit resolves again. An explicit stored mode survives
|
|
73
|
+
* it; a legacy schedule's enters `auto`.
|
|
74
|
+
* - Anything else — payload, headers, name, tags, a mode named as it already
|
|
75
|
+
* was — preserves the stored service and tariff, and a legacy schedule stays
|
|
76
|
+
* legacy.
|
|
77
|
+
*/
|
|
78
|
+
export declare function replacementMode(input: {
|
|
79
|
+
readonly stored: StoredRequestedLatencyClass;
|
|
80
|
+
readonly requested: RequestedLatencyClass | undefined;
|
|
81
|
+
readonly timingChanged: boolean;
|
|
82
|
+
}): {
|
|
83
|
+
readonly kind: "preserve";
|
|
84
|
+
readonly requested: StoredRequestedLatencyClass;
|
|
85
|
+
} | {
|
|
86
|
+
readonly kind: "resolve";
|
|
87
|
+
readonly requested: RequestedLatencyClass;
|
|
88
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W12-02 (FXO-3171): the one resolver for service modes.
|
|
3
|
+
*
|
|
4
|
+
* Design: docs/archive/warm-worker-scheduling/project-spec.md § Customer
|
|
5
|
+
* contract, and implementation-guide.md § W02. A request names a mode —
|
|
6
|
+
* `auto`, `bulk` or `realtime` — and every occurrence resolves to a concrete
|
|
7
|
+
* service (`bulk` or `realtime`, what is charged and what a read reports), an
|
|
8
|
+
* internal band (`realtime`, `short_bulk` or `bulk`) and that band's quantum.
|
|
9
|
+
*
|
|
10
|
+
* Pure: every input is a parameter, nothing here reads a clock, and the
|
|
11
|
+
* spacing it classifies by is measured by the caller from instants the
|
|
12
|
+
* existing recurrence evaluator produced. There is deliberately no cron
|
|
13
|
+
* arithmetic in this module: a second evaluator is a second opinion about when
|
|
14
|
+
* a schedule fires, and two opinions eventually disagree.
|
|
15
|
+
*
|
|
16
|
+
* The spacing is:
|
|
17
|
+
*
|
|
18
|
+
* - for a one-off, the lead from the captured acceptance time to the original
|
|
19
|
+
* planned instant (an instant already past has no lead), measured once at
|
|
20
|
+
* acceptance — a replayed request reads the stored answer and never measures
|
|
21
|
+
* again; and
|
|
22
|
+
* - for a recurrence, the shorter adjacent planned gap. A one-minute heartbeat
|
|
23
|
+
* is short bulk however soon its next run happens to be due.
|
|
24
|
+
*
|
|
25
|
+
* Every recurrence the product accepts is at least a minute apart — `every`
|
|
26
|
+
* starts at sixty seconds and cron names whole minutes — so `auto` never
|
|
27
|
+
* resolves a recurrence to `realtime`, and an occurrence's service is the
|
|
28
|
+
* schedule's for its whole series. Only the band moves with irregular gaps.
|
|
29
|
+
*/
|
|
30
|
+
import { SERVICE_MODE_POLICY } from "./operational-config.js";
|
|
31
|
+
/** The modes a request may name. `auto` is what omission means. */
|
|
32
|
+
export const REQUESTED_LATENCY_CLASSES = ["auto", "bulk", "realtime"];
|
|
33
|
+
/** The internal bands. Not on the wire: only the concrete service is. */
|
|
34
|
+
export const SERVICE_BANDS = ["realtime", "short_bulk", "bulk"];
|
|
35
|
+
/** The concrete service a band is charged and reported as. */
|
|
36
|
+
export function bandLatencyClass(band) {
|
|
37
|
+
return band === "realtime" ? "realtime" : "bulk";
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The service, band and quantum for one occurrence.
|
|
41
|
+
*
|
|
42
|
+
* `spacingMs` is the one-off lead or the recurrence's shorter adjacent gap
|
|
43
|
+
* (see the module comment). A non-finite spacing — a recurrence with no
|
|
44
|
+
* adjacent instant — is treated as long.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveService(requested, spacingMs) {
|
|
47
|
+
const spacing = Number.isNaN(spacingMs) ? Infinity : Math.max(0, spacingMs);
|
|
48
|
+
const band = requested === "realtime" ||
|
|
49
|
+
(requested === "auto" && spacing < SERVICE_MODE_POLICY.autoRealtimeBelowMs)
|
|
50
|
+
? "realtime"
|
|
51
|
+
: spacing < SERVICE_MODE_POLICY.shortBulkBelowMs
|
|
52
|
+
? "short_bulk"
|
|
53
|
+
: "bulk";
|
|
54
|
+
return {
|
|
55
|
+
band,
|
|
56
|
+
latencyClass: bandLatencyClass(band),
|
|
57
|
+
quantumMs: SERVICE_MODE_POLICY.quantumMs[band],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** A one-off's lead: planned instant minus the captured acceptance time. */
|
|
61
|
+
export function oneOffSpacingMs(acceptedAtMs, plannedForMs) {
|
|
62
|
+
return plannedForMs - acceptedAtMs;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A recurrence occurrence's shorter adjacent planned gap. Either neighbour may
|
|
66
|
+
* be unknown — a series' first occurrence has no predecessor — and the gap to
|
|
67
|
+
* a known neighbour decides alone.
|
|
68
|
+
*/
|
|
69
|
+
export function recurrenceSpacingMs(previousMs, plannedForMs, nextMs) {
|
|
70
|
+
const gaps = [
|
|
71
|
+
previousMs === undefined ? Infinity : plannedForMs - previousMs,
|
|
72
|
+
nextMs === undefined ? Infinity : nextMs - plannedForMs,
|
|
73
|
+
].filter((gap) => gap > 0);
|
|
74
|
+
return gaps.length === 0 ? Infinity : Math.min(...gaps);
|
|
75
|
+
}
|
|
76
|
+
/** The last instant the contract's four-digit-year timestamps express. */
|
|
77
|
+
const LATEST_EXPRESSIBLE_MS = Date.parse("9999-12-31T23:59:59.999Z");
|
|
78
|
+
/**
|
|
79
|
+
* An instant's first eligibility in its band: `ceil(instant / quantum) *
|
|
80
|
+
* quantum`, so a boundary stays where it is.
|
|
81
|
+
*
|
|
82
|
+
* Applied once, to the planned instant or to a retry's backed-off instant.
|
|
83
|
+
* Work that becomes claimable late is claimable now, never at another
|
|
84
|
+
* boundary: nothing rounds a rounded instant, and nothing rounds `now`. The
|
|
85
|
+
* last quantum of year 9999 has no boundary the contract can name, so it is
|
|
86
|
+
* eligible at the end of that year instead.
|
|
87
|
+
*/
|
|
88
|
+
export function roundEligibilityMs(instantMs, band) {
|
|
89
|
+
const quantum = SERVICE_MODE_POLICY.quantumMs[band];
|
|
90
|
+
return Math.min(Math.ceil(instantMs / quantum) * quantum, Math.max(instantMs, LATEST_EXPRESSIBLE_MS));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* One occurrence's service, band and rounded first eligibility: the entry
|
|
94
|
+
* point create, replacement and a recurrence's successor all use.
|
|
95
|
+
*
|
|
96
|
+
* A one-off is measured from its captured acceptance time; a recurrence by its
|
|
97
|
+
* neighbours, which the caller took from the existing recurrence evaluator.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveOccurrence(input) {
|
|
100
|
+
const spacing = input.kind === "one-off"
|
|
101
|
+
? oneOffSpacingMs(input.acceptedAtMs, input.plannedForMs)
|
|
102
|
+
: recurrenceSpacingMs(input.previousMs, input.plannedForMs, input.nextMs);
|
|
103
|
+
const resolution = resolveService(input.requested, spacing);
|
|
104
|
+
return {
|
|
105
|
+
...resolution,
|
|
106
|
+
eligibleAtMs: roundEligibilityMs(input.plannedForMs, resolution.band),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* What a replacement runs under (implementation-guide § W02, step 4).
|
|
111
|
+
*
|
|
112
|
+
* - A request naming a mode other than the stored one is a mode edit.
|
|
113
|
+
* - A timing or cadence edit resolves again. An explicit stored mode survives
|
|
114
|
+
* it; a legacy schedule's enters `auto`.
|
|
115
|
+
* - Anything else — payload, headers, name, tags, a mode named as it already
|
|
116
|
+
* was — preserves the stored service and tariff, and a legacy schedule stays
|
|
117
|
+
* legacy.
|
|
118
|
+
*/
|
|
119
|
+
export function replacementMode(input) {
|
|
120
|
+
if (input.requested !== undefined && input.requested !== input.stored) {
|
|
121
|
+
return { kind: "resolve", requested: input.requested };
|
|
122
|
+
}
|
|
123
|
+
if (input.timingChanged) {
|
|
124
|
+
return {
|
|
125
|
+
kind: "resolve",
|
|
126
|
+
requested: input.stored === "legacy" ? "auto" : input.stored,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { kind: "preserve", requested: input.stored };
|
|
130
|
+
}
|
package/dist/signatures.d.ts
CHANGED
|
@@ -409,7 +409,7 @@ export declare const CALLBACK_DELIVERY_PROTOCOL: {
|
|
|
409
409
|
};
|
|
410
410
|
readonly secret: "greatwait-signature-vector-key-not-a-credential";
|
|
411
411
|
readonly signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA3LTAxIiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjEsImRhdGEiOnt9fQ==";
|
|
412
|
-
readonly signingInputUtf8:
|
|
412
|
+
readonly signingInputUtf8: string;
|
|
413
413
|
};
|
|
414
414
|
/**
|
|
415
415
|
* The retired `2026-08-08` envelope **without** `completion`, kept as a known
|
|
@@ -445,7 +445,7 @@ export declare const CALLBACK_DELIVERY_PROTOCOL: {
|
|
|
445
445
|
};
|
|
446
446
|
readonly secret: "greatwait-signature-vector-key-not-a-credential";
|
|
447
447
|
readonly signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA4LTA4IiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjEsInRyaWdnZXIiOnsia2luZCI6ImF1dG9tYXRpYyJ9LCJkYXRhIjp7fX0=";
|
|
448
|
-
readonly signingInputUtf8:
|
|
448
|
+
readonly signingInputUtf8: string;
|
|
449
449
|
};
|
|
450
450
|
/**
|
|
451
451
|
* The retired `2026-08-08` envelope **with** `completion`, kept as a known
|
|
@@ -489,7 +489,7 @@ export declare const CALLBACK_DELIVERY_PROTOCOL: {
|
|
|
489
489
|
};
|
|
490
490
|
readonly secret: "greatwait-signature-vector-key-not-a-credential";
|
|
491
491
|
readonly signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA4LTA4IiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjEsImNvbXBsZXRpb24iOnsidG9rZW4iOiJ2MS5leGFtcGxlLWNsYWltLW5vdC1hLXJlYWwtdG9rZW4uZXhhbXBsZS1tYWMiLCJ1cmwiOiJodHRwczovL2dyZWF0d2FpdC1zY2hlZHVsZXIuZXhhbXBsZS9jYWxsYmFja3MvY29tcGxldGlvbiJ9LCJ0cmlnZ2VyIjp7ImtpbmQiOiJhdXRvbWF0aWMifSwiZGF0YSI6e319";
|
|
492
|
-
readonly signingInputUtf8:
|
|
492
|
+
readonly signingInputUtf8: string;
|
|
493
493
|
};
|
|
494
494
|
/**
|
|
495
495
|
* A redelivery, which is the only vector that discriminates this change.
|
|
@@ -530,7 +530,7 @@ export declare const CALLBACK_DELIVERY_PROTOCOL: {
|
|
|
530
530
|
};
|
|
531
531
|
readonly secret: "greatwait-signature-vector-key-not-a-credential";
|
|
532
532
|
readonly signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA4LTIzIiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjIsImNvbXBsZXRpb24iOnsidG9rZW4iOiJ2Mi5leGFtcGxlLWNsYWltLW5vdC1hLXJlYWwtdG9rZW4uZXhhbXBsZS1tYWMiLCJ1cmwiOiJodHRwczovL2dyZWF0d2FpdC1zY2hlZHVsZXIuZXhhbXBsZS9jYWxsYmFja3MvY29tcGxldGlvbiIsImRlYWRsaW5lIjoiMjAyNi0wNy0yOFQxNTowMDowNS4wMDBaIn0sInRyaWdnZXIiOnsia2luZCI6InJlZGVsaXZlcnkiLCJzb3VyY2VBdHRlbXB0SWQiOiJhdHRfMDEyMzQ1Njc4OWFiY2RlZmdoamttbnBxcnMifSwiZGF0YSI6e319";
|
|
533
|
-
readonly signingInputUtf8:
|
|
533
|
+
readonly signingInputUtf8: string;
|
|
534
534
|
};
|
|
535
535
|
/**
|
|
536
536
|
* The ordinary current-version delivery: automatic, and carrying completion
|
|
@@ -566,7 +566,7 @@ export declare const CALLBACK_DELIVERY_PROTOCOL: {
|
|
|
566
566
|
};
|
|
567
567
|
readonly secret: "greatwait-signature-vector-key-not-a-credential";
|
|
568
568
|
readonly signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA4LTIzIiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjEsImNvbXBsZXRpb24iOnsidG9rZW4iOiJ2Mi5leGFtcGxlLWNsYWltLW5vdC1hLXJlYWwtdG9rZW4uZXhhbXBsZS1tYWMiLCJ1cmwiOiJodHRwczovL2dyZWF0d2FpdC1zY2hlZHVsZXIuZXhhbXBsZS9jYWxsYmFja3MvY29tcGxldGlvbiIsImRlYWRsaW5lIjoiMjAyNi0wNy0yOFQxNTowMDowNS4wMDBaIn0sInRyaWdnZXIiOnsia2luZCI6ImF1dG9tYXRpYyJ9LCJkYXRhIjp7fX0=";
|
|
569
|
-
readonly signingInputUtf8:
|
|
569
|
+
readonly signingInputUtf8: string;
|
|
570
570
|
};
|
|
571
571
|
readonly unicodeTestVector: {
|
|
572
572
|
readonly envelope: {
|
|
@@ -600,7 +600,7 @@ export declare const CALLBACK_DELIVERY_PROTOCOL: {
|
|
|
600
600
|
};
|
|
601
601
|
readonly secret: "greatwait-signature-vector-key-not-a-credential";
|
|
602
602
|
readonly signingInputBase64: "MjAyNi0wNy0yOFQxNTowMDowMC4wMDBaLmRsdl8wMTIzNDU2Nzg5YWJjZGVmZ2hqa21ucHFycy57InZlcnNpb24iOiIyMDI2LTA4LTIzIiwidHlwZSI6InNjaGVkdWxlLm9jY3VycmVuY2UiLCJzY2hlZHVsZUlkIjoic2NoXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwib2NjdXJyZW5jZUlkIjoib2NjXzAxMjM0NTY3ODlhYmNkZWZnaGprbW5wcXJzIiwicGxhbm5lZEZvciI6IjIwMjYtMDctMjhUMTU6MDA6MDAuMDAwWiIsImF0dGVtcHQiOjEsImNvbXBsZXRpb24iOnsidG9rZW4iOiJ2Mi5leGFtcGxlLWNsYWltLW5vdC1hLXJlYWwtdG9rZW4uZXhhbXBsZS1tYWMiLCJ1cmwiOiJodHRwczovL2dyZWF0d2FpdC1zY2hlZHVsZXIuZXhhbXBsZS9jYWxsYmFja3MvY29tcGxldGlvbiIsImRlYWRsaW5lIjoiMjAyNi0wNy0yOFQxNTowMDowNS4wMDBaIn0sInRyaWdnZXIiOnsia2luZCI6ImF1dG9tYXRpYyJ9LCJkYXRhIjp7Im5vdGUiOiJjYWbDqSDimIMifX0=";
|
|
603
|
-
readonly signingInputUtf8:
|
|
603
|
+
readonly signingInputUtf8: string;
|
|
604
604
|
};
|
|
605
605
|
readonly version: "2026-08-23";
|
|
606
606
|
};
|
package/dist/signatures.js
CHANGED
|
@@ -34,9 +34,9 @@ export const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
|
34
34
|
export const CALLBACK_DELIVERY_PROTOCOL_VERSION = "2026-08-23";
|
|
35
35
|
/** The media type sent on every occurrence delivery. */
|
|
36
36
|
export const CALLBACK_CONTENT_TYPE = "application/json";
|
|
37
|
-
export const signatureHeaderSchema = z
|
|
37
|
+
export const signatureHeaderSchema = /* @__PURE__ */ (() => z
|
|
38
38
|
.string()
|
|
39
|
-
.regex(/^v1=[0-9a-f]{64}$/u, "must be v1= followed by a lowercase hex SHA-256 HMAC");
|
|
39
|
+
.regex(/^v1=[0-9a-f]{64}$/u, "must be v1= followed by a lowercase hex SHA-256 HMAC"))();
|
|
40
40
|
/**
|
|
41
41
|
* The exact bytes that are signed: `<timestamp>.<delivery-id>.` followed by the
|
|
42
42
|
* raw body, with no re-serialization anywhere in the path. Re-encoding JSON
|
|
@@ -130,13 +130,13 @@ export function isTimestampFresh(timestamp, nowMs, toleranceSeconds = SIGNATURE_
|
|
|
130
130
|
* an unauthenticated hint has handed anyone who can reach its endpoint a
|
|
131
131
|
* duplicate-execution button.
|
|
132
132
|
*/
|
|
133
|
-
export const callbackTriggerSchema = z.discriminatedUnion("kind", [
|
|
133
|
+
export const callbackTriggerSchema = /* @__PURE__ */ (() => z.discriminatedUnion("kind", [
|
|
134
134
|
z.object({ kind: z.literal("automatic") }),
|
|
135
135
|
z.object({
|
|
136
136
|
kind: z.literal("redelivery"),
|
|
137
137
|
sourceAttemptId: attemptIdSchema,
|
|
138
138
|
}),
|
|
139
|
-
]);
|
|
139
|
+
]))();
|
|
140
140
|
/**
|
|
141
141
|
* The envelope version this service emits.
|
|
142
142
|
*
|
|
@@ -178,7 +178,7 @@ export const CALLBACK_ENVELOPE_VERSIONS = [
|
|
|
178
178
|
CALLBACK_ENVELOPE_VERSION_2026_08_08,
|
|
179
179
|
CALLBACK_ENVELOPE_VERSION,
|
|
180
180
|
];
|
|
181
|
-
const callbackEnvelopeShape = {
|
|
181
|
+
const callbackEnvelopeShape = /* @__PURE__ */ (() => ({
|
|
182
182
|
attempt: z.number().int().positive(),
|
|
183
183
|
/**
|
|
184
184
|
* The customer's payload, delivered verbatim. Deliberately any JSON value:
|
|
@@ -190,7 +190,7 @@ const callbackEnvelopeShape = {
|
|
|
190
190
|
plannedFor: timestampSchema,
|
|
191
191
|
scheduleId: scheduleIdSchema,
|
|
192
192
|
type: z.literal("schedule.occurrence"),
|
|
193
|
-
};
|
|
193
|
+
}))();
|
|
194
194
|
/**
|
|
195
195
|
* Where a `callback` receiver reports that it finished, and with what.
|
|
196
196
|
*
|
|
@@ -207,12 +207,12 @@ const callbackEnvelopeShape = {
|
|
|
207
207
|
* later attempt is refused by the same fence that refuses a late executor
|
|
208
208
|
* result.
|
|
209
209
|
*/
|
|
210
|
-
export const callbackCompletionSchema = z.object({
|
|
210
|
+
export const callbackCompletionSchema = /* @__PURE__ */ (() => z.object({
|
|
211
211
|
/** Bearer credential for the report. Send it, do not inspect it. */
|
|
212
212
|
token: z.string().min(1),
|
|
213
213
|
/** Absolute URL to POST to when the work is done. */
|
|
214
214
|
url: z.url(),
|
|
215
|
-
});
|
|
215
|
+
}))();
|
|
216
216
|
/**
|
|
217
217
|
* The completion material every `2026-08-23` delivery carries.
|
|
218
218
|
*
|
|
@@ -227,21 +227,21 @@ export const callbackCompletionSchema = z.object({
|
|
|
227
227
|
* ask for an earlier instant in its acknowledgement and may not ask for a
|
|
228
228
|
* later one.
|
|
229
229
|
*/
|
|
230
|
-
export const callbackDeferredCompletionSchema = z.object({
|
|
230
|
+
export const callbackDeferredCompletionSchema = /* @__PURE__ */ (() => z.object({
|
|
231
231
|
/** When this attempt stops being waited for. Absolute, not a duration. */
|
|
232
232
|
deadline: timestampSchema,
|
|
233
233
|
/** Bearer credential for the report. Send it, do not inspect it. */
|
|
234
234
|
token: z.string().min(1),
|
|
235
235
|
/** Absolute URL to POST to when the work is done. */
|
|
236
236
|
url: z.url(),
|
|
237
|
-
});
|
|
237
|
+
}))();
|
|
238
238
|
/**
|
|
239
239
|
* The report body, whose vocabulary lives in the Zod-free `delivery` module so
|
|
240
240
|
* the scheduler's completion route can read it without importing Zod.
|
|
241
241
|
*/
|
|
242
|
-
export const callbackCompletionReportSchema = z.object({
|
|
242
|
+
export const callbackCompletionReportSchema = /* @__PURE__ */ (() => z.object({
|
|
243
243
|
outcome: z.enum(CALLBACK_COMPLETION_OUTCOMES).default("succeeded"),
|
|
244
|
-
});
|
|
244
|
+
}))();
|
|
245
245
|
/**
|
|
246
246
|
* The versioned JSON envelope delivered to a customer endpoint.
|
|
247
247
|
*
|
|
@@ -251,7 +251,7 @@ export const callbackCompletionReportSchema = z.object({
|
|
|
251
251
|
* envelope. Collapsing the two would leave a receiver unable to know whether
|
|
252
252
|
* silence about a redelivery is an answer or an absence.
|
|
253
253
|
*/
|
|
254
|
-
export const callbackEnvelopeSchema = z.discriminatedUnion("version", [
|
|
254
|
+
export const callbackEnvelopeSchema = /* @__PURE__ */ (() => z.discriminatedUnion("version", [
|
|
255
255
|
z.object({
|
|
256
256
|
...callbackEnvelopeShape,
|
|
257
257
|
/** Envelope version, dated and independent of the contract version. */
|
|
@@ -284,7 +284,7 @@ export const callbackEnvelopeSchema = z.discriminatedUnion("version", [
|
|
|
284
284
|
trigger: callbackTriggerSchema,
|
|
285
285
|
version: z.literal(CALLBACK_ENVELOPE_VERSION),
|
|
286
286
|
}),
|
|
287
|
-
]);
|
|
287
|
+
]))();
|
|
288
288
|
/**
|
|
289
289
|
* What caused a delivery, for an envelope of any published version.
|
|
290
290
|
*
|
|
@@ -436,20 +436,20 @@ export function extractCanonicalEnvelopeIds(rawBody) {
|
|
|
436
436
|
? { occurrenceId: occurrenceId.data, scheduleId: scheduleId.data }
|
|
437
437
|
: undefined;
|
|
438
438
|
}
|
|
439
|
-
export const callbackHeadersSchema = z.object({
|
|
439
|
+
export const callbackHeadersSchema = /* @__PURE__ */ (() => z.object({
|
|
440
440
|
[CALLBACK_HEADERS.deliveryId]: deliveryIdSchema,
|
|
441
441
|
[CALLBACK_HEADERS.keyId]: callbackSigningKeyIdSchema,
|
|
442
442
|
[CALLBACK_HEADERS.occurrenceId]: occurrenceIdSchema,
|
|
443
443
|
[CALLBACK_HEADERS.signature]: signatureHeaderSchema,
|
|
444
444
|
[CALLBACK_HEADERS.timestamp]: timestampSchema,
|
|
445
|
-
});
|
|
445
|
+
}))();
|
|
446
446
|
const VECTOR_PREFIX = "2026-07-28T15:00:00.000Z.dlv_0123456789abcdefghjkmnpqrs.";
|
|
447
447
|
const CALLBACK_DELIVERY_VECTOR_BODY = '{"version":"2026-08-23","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":1,"completion":{"token":"v2.example-claim-not-a-real-token.example-mac","url":"https://greatwait-scheduler.example/callbacks/completion","deadline":"2026-07-28T15:00:05.000Z"},"trigger":{"kind":"automatic"},"data":{}}';
|
|
448
|
-
const CALLBACK_DELIVERY_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_VECTOR_BODY}
|
|
448
|
+
const CALLBACK_DELIVERY_VECTOR_INPUT = /* @__PURE__ */ (() => `${VECTOR_PREFIX}${CALLBACK_DELIVERY_VECTOR_BODY}`)();
|
|
449
449
|
const CALLBACK_DELIVERY_UNICODE_VECTOR_BODY = '{"version":"2026-08-23","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":1,"completion":{"token":"v2.example-claim-not-a-real-token.example-mac","url":"https://greatwait-scheduler.example/callbacks/completion","deadline":"2026-07-28T15:00:05.000Z"},"trigger":{"kind":"automatic"},"data":{"note":"café ☃"}}';
|
|
450
|
-
const CALLBACK_DELIVERY_UNICODE_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_UNICODE_VECTOR_BODY}
|
|
450
|
+
const CALLBACK_DELIVERY_UNICODE_VECTOR_INPUT = /* @__PURE__ */ (() => `${VECTOR_PREFIX}${CALLBACK_DELIVERY_UNICODE_VECTOR_BODY}`)();
|
|
451
451
|
const CALLBACK_DELIVERY_REDELIVERY_VECTOR_BODY = '{"version":"2026-08-23","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":2,"completion":{"token":"v2.example-claim-not-a-real-token.example-mac","url":"https://greatwait-scheduler.example/callbacks/completion","deadline":"2026-07-28T15:00:05.000Z"},"trigger":{"kind":"redelivery","sourceAttemptId":"att_0123456789abcdefghjkmnpqrs"},"data":{}}';
|
|
452
|
-
const CALLBACK_DELIVERY_REDELIVERY_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_REDELIVERY_VECTOR_BODY}
|
|
452
|
+
const CALLBACK_DELIVERY_REDELIVERY_VECTOR_INPUT = /* @__PURE__ */ (() => `${VECTOR_PREFIX}${CALLBACK_DELIVERY_REDELIVERY_VECTOR_BODY}`)();
|
|
453
453
|
/**
|
|
454
454
|
* The `2026-08-08` body with no `completion`, byte for byte as published.
|
|
455
455
|
*
|
|
@@ -461,9 +461,9 @@ const CALLBACK_DELIVERY_REDELIVERY_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DE
|
|
|
461
461
|
* receiver mid-rollout is still being handed.
|
|
462
462
|
*/
|
|
463
463
|
const CALLBACK_DELIVERY_TRIGGER_VECTOR_BODY = '{"version":"2026-08-08","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":1,"trigger":{"kind":"automatic"},"data":{}}';
|
|
464
|
-
const CALLBACK_DELIVERY_TRIGGER_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_TRIGGER_VECTOR_BODY}
|
|
464
|
+
const CALLBACK_DELIVERY_TRIGGER_VECTOR_INPUT = /* @__PURE__ */ (() => `${VECTOR_PREFIX}${CALLBACK_DELIVERY_TRIGGER_VECTOR_BODY}`)();
|
|
465
465
|
const CALLBACK_DELIVERY_COMPLETION_VECTOR_BODY = '{"version":"2026-08-08","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":1,"completion":{"token":"v1.example-claim-not-a-real-token.example-mac","url":"https://greatwait-scheduler.example/callbacks/completion"},"trigger":{"kind":"automatic"},"data":{}}';
|
|
466
|
-
const CALLBACK_DELIVERY_COMPLETION_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_COMPLETION_VECTOR_BODY}
|
|
466
|
+
const CALLBACK_DELIVERY_COMPLETION_VECTOR_INPUT = /* @__PURE__ */ (() => `${VECTOR_PREFIX}${CALLBACK_DELIVERY_COMPLETION_VECTOR_BODY}`)();
|
|
467
467
|
/**
|
|
468
468
|
* The `2026-07-01` body, byte for byte as it was published, with the MAC it was
|
|
469
469
|
* published with.
|
|
@@ -476,12 +476,12 @@ const CALLBACK_DELIVERY_COMPLETION_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DE
|
|
|
476
476
|
* published verifier is wrong.
|
|
477
477
|
*/
|
|
478
478
|
const CALLBACK_DELIVERY_LEGACY_VECTOR_BODY = '{"version":"2026-07-01","type":"schedule.occurrence","scheduleId":"sch_0123456789abcdefghjkmnpqrs","occurrenceId":"occ_0123456789abcdefghjkmnpqrs","plannedFor":"2026-07-28T15:00:00.000Z","attempt":1,"data":{}}';
|
|
479
|
-
const CALLBACK_DELIVERY_LEGACY_VECTOR_INPUT = `${VECTOR_PREFIX}${CALLBACK_DELIVERY_LEGACY_VECTOR_BODY}
|
|
479
|
+
const CALLBACK_DELIVERY_LEGACY_VECTOR_INPUT = /* @__PURE__ */ (() => `${VECTOR_PREFIX}${CALLBACK_DELIVERY_LEGACY_VECTOR_BODY}`)();
|
|
480
480
|
/**
|
|
481
481
|
* Language-neutral known-answer contract for one complete delivery request.
|
|
482
482
|
* The secret is synthetic and deliberately not shaped like a credential.
|
|
483
483
|
*/
|
|
484
|
-
export const CALLBACK_DELIVERY_PROTOCOL = {
|
|
484
|
+
export const CALLBACK_DELIVERY_PROTOCOL = /* @__PURE__ */ (() => ({
|
|
485
485
|
envelope: {
|
|
486
486
|
/**
|
|
487
487
|
* The frozen wire order, and `completion` sits inside it.
|
|
@@ -790,7 +790,7 @@ export const CALLBACK_DELIVERY_PROTOCOL = {
|
|
|
790
790
|
signingInputUtf8: CALLBACK_DELIVERY_UNICODE_VECTOR_INPUT,
|
|
791
791
|
},
|
|
792
792
|
version: CALLBACK_DELIVERY_PROTOCOL_VERSION,
|
|
793
|
-
};
|
|
793
|
+
}))();
|
|
794
794
|
/**
|
|
795
795
|
* Every published vector, derived from the protocol rather than listed.
|
|
796
796
|
*
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The headers a send carries: the endpoint's defaults with the schedule's own
|
|
3
|
+
* headers over them, matched case-insensitively, the later entry winning, and
|
|
4
|
+
* suppressed names removed — kept in the order they were written.
|
|
5
|
+
*/
|
|
6
|
+
export declare function mergeStoredHttpHeaders(defaults: readonly unknown[] | null | undefined, overrides: readonly unknown[] | null | undefined): unknown[];
|
|
7
|
+
/**
|
|
8
|
+
* Whether stored headers fit their bounds: an array of at most 32, each name
|
|
9
|
+
* present, at most 128 bytes and distinct case-insensitively, and each value
|
|
10
|
+
* that is not suppressed at most 8,192 bytes, 16,384 bytes in all.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validStoredHttpHeaders(headers: unknown): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Whether a schedule's stored HTTP settings, sent through an endpoint with
|
|
15
|
+
* these default headers, are within bounds: the settings' own shape and
|
|
16
|
+
* headers, and the headers a send would carry after the merge.
|
|
17
|
+
*/
|
|
18
|
+
export declare function validScheduleHttp(request: unknown, endpointHeaders: readonly unknown[] | null | undefined): boolean;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stored custom-HTTP invariants, as pure functions (W9-73).
|
|
3
|
+
*
|
|
4
|
+
* Until W9-73 a schedule insert ran these inside PostgreSQL, in the
|
|
5
|
+
* `greatwait_schedule_http_bounds` trigger (migration 0050): a lock and read of
|
|
6
|
+
* the endpoint's default headers, `http_merge_headers_v1` and
|
|
7
|
+
* `http_validate_headers_v1`. They are the same rules here, over the same JSON,
|
|
8
|
+
* so the create can validate before it writes and spend no statement on it.
|
|
9
|
+
* Schedule edits keep the trigger; the endpoint's own trigger still guards an
|
|
10
|
+
* endpoint header change against every schedule using it.
|
|
11
|
+
*/
|
|
12
|
+
import { HTTP_HEADER_BOUNDS, HTTP_METHODS } from "./http-request.js";
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
const BODY_MODES = ["envelope", "json", "none"];
|
|
15
|
+
function record(value) {
|
|
16
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
17
|
+
? value
|
|
18
|
+
: undefined;
|
|
19
|
+
}
|
|
20
|
+
/** `value->>'suppressed' = 'true'`: the JSON `true`, or the string. */
|
|
21
|
+
function suppressed(header) {
|
|
22
|
+
const flag = record(header)?.["suppressed"];
|
|
23
|
+
return flag === true || flag === "true";
|
|
24
|
+
}
|
|
25
|
+
function headerName(header) {
|
|
26
|
+
const name = record(header)?.["name"];
|
|
27
|
+
return typeof name === "string" ? name : undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The headers a send carries: the endpoint's defaults with the schedule's own
|
|
31
|
+
* headers over them, matched case-insensitively, the later entry winning, and
|
|
32
|
+
* suppressed names removed — kept in the order they were written.
|
|
33
|
+
*/
|
|
34
|
+
export function mergeStoredHttpHeaders(defaults, overrides) {
|
|
35
|
+
// Positions as the SQL numbered them: defaults from 1, overrides from 101,
|
|
36
|
+
// so an override always outranks a default of the same name.
|
|
37
|
+
const winners = new Map();
|
|
38
|
+
const consider = (headers, offset) => {
|
|
39
|
+
headers.forEach((header, index) => {
|
|
40
|
+
const key = headerName(header)?.toLowerCase();
|
|
41
|
+
const position = offset + index + 1;
|
|
42
|
+
const prior = winners.get(key);
|
|
43
|
+
if (prior === undefined || position > prior[0])
|
|
44
|
+
winners.set(key, [position, header]);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
consider(defaults ?? [], 0);
|
|
48
|
+
consider(overrides ?? [], 100);
|
|
49
|
+
return [...winners.values()]
|
|
50
|
+
.filter(([, header]) => !suppressed(header))
|
|
51
|
+
.sort(([left], [right]) => left - right)
|
|
52
|
+
.map(([, header]) => header);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether stored headers fit their bounds: an array of at most 32, each name
|
|
56
|
+
* present, at most 128 bytes and distinct case-insensitively, and each value
|
|
57
|
+
* that is not suppressed at most 8,192 bytes, 16,384 bytes in all.
|
|
58
|
+
*/
|
|
59
|
+
export function validStoredHttpHeaders(headers) {
|
|
60
|
+
if (!Array.isArray(headers) || headers.length > HTTP_HEADER_BOUNDS.count)
|
|
61
|
+
return false;
|
|
62
|
+
const names = new Set();
|
|
63
|
+
let total = 0;
|
|
64
|
+
for (const header of headers) {
|
|
65
|
+
const name = headerName(header);
|
|
66
|
+
if (name === undefined || name.length === 0)
|
|
67
|
+
return false;
|
|
68
|
+
const nameBytes = encoder.encode(name).byteLength;
|
|
69
|
+
if (nameBytes > HTTP_HEADER_BOUNDS.nameBytes)
|
|
70
|
+
return false;
|
|
71
|
+
if (names.has(name.toLowerCase()))
|
|
72
|
+
return false;
|
|
73
|
+
names.add(name.toLowerCase());
|
|
74
|
+
if (suppressed(header))
|
|
75
|
+
continue;
|
|
76
|
+
const size = record(header)?.["valueBytes"];
|
|
77
|
+
// The SQL read the number as text and required one to five digits.
|
|
78
|
+
if (typeof size !== "number" ||
|
|
79
|
+
!Number.isSafeInteger(size) ||
|
|
80
|
+
size < 0 ||
|
|
81
|
+
size > HTTP_HEADER_BOUNDS.valueBytes)
|
|
82
|
+
return false;
|
|
83
|
+
total += nameBytes + size;
|
|
84
|
+
}
|
|
85
|
+
return total <= HTTP_HEADER_BOUNDS.totalBytes;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Whether a schedule's stored HTTP settings, sent through an endpoint with
|
|
89
|
+
* these default headers, are within bounds: the settings' own shape and
|
|
90
|
+
* headers, and the headers a send would carry after the merge.
|
|
91
|
+
*/
|
|
92
|
+
export function validScheduleHttp(request, endpointHeaders) {
|
|
93
|
+
const settings = record(request);
|
|
94
|
+
if (request !== null && request !== undefined) {
|
|
95
|
+
const method = settings?.["method"];
|
|
96
|
+
const bodyMode = settings?.["bodyMode"];
|
|
97
|
+
if (settings === undefined ||
|
|
98
|
+
!HTTP_METHODS.includes(method) ||
|
|
99
|
+
!BODY_MODES.includes(bodyMode) ||
|
|
100
|
+
(method === "GET") !== (bodyMode === "none") ||
|
|
101
|
+
!validStoredHttpHeaders(settings["headers"]))
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const overrides = settings?.["headers"];
|
|
105
|
+
return validStoredHttpHeaders(mergeStoredHttpHeaders(endpointHeaders, Array.isArray(overrides) ? overrides : undefined));
|
|
106
|
+
}
|