@astrale-os/adapter-cloudflare 0.5.0-beta.176 → 0.5.0-beta.178

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.
Files changed (100) hide show
  1. package/README.md +119 -78
  2. package/dist/adapter.d.ts +6 -5
  3. package/dist/adapter.js +8 -8
  4. package/dist/build/index.d.ts +1 -0
  5. package/dist/build/index.js +1 -0
  6. package/dist/hosting/activation.d.ts +37 -0
  7. package/dist/hosting/activation.js +108 -0
  8. package/dist/hosting/assets.d.ts +14 -0
  9. package/dist/hosting/assets.js +53 -0
  10. package/dist/hosting/bounded.d.ts +10 -0
  11. package/dist/hosting/bounded.js +32 -0
  12. package/dist/hosting/bytes.d.ts +6 -0
  13. package/dist/hosting/bytes.js +25 -0
  14. package/dist/hosting/client.d.ts +144 -0
  15. package/dist/hosting/client.js +491 -0
  16. package/dist/hosting/dispatcher/cron.d.mts +10 -0
  17. package/dist/hosting/dispatcher/cron.mjs +135 -0
  18. package/dist/hosting/dispatcher/deployments.d.mts +12 -0
  19. package/dist/hosting/dispatcher/deployments.mjs +164 -0
  20. package/dist/hosting/dispatcher/dispatcher.d.mts +43 -0
  21. package/dist/hosting/dispatcher/dispatcher.mjs +96 -0
  22. package/dist/hosting/dispatcher/env.d.mts +51 -0
  23. package/dist/hosting/dispatcher/failure.d.mts +4 -0
  24. package/dist/hosting/dispatcher/failure.mjs +15 -0
  25. package/dist/hosting/dispatcher/index.d.mts +4 -0
  26. package/dist/hosting/dispatcher/index.mjs +10 -0
  27. package/dist/hosting/dispatcher/modules.d.ts +25 -0
  28. package/dist/hosting/dispatcher/modules.js +56 -0
  29. package/dist/hosting/dispatcher/services.d.mts +28 -0
  30. package/dist/hosting/dispatcher/services.mjs +231 -0
  31. package/dist/hosting/identity.d.ts +15 -0
  32. package/dist/hosting/identity.js +36 -0
  33. package/dist/hosting/index.d.ts +34 -0
  34. package/dist/hosting/index.js +32 -0
  35. package/dist/hosting/kv.d.ts +89 -0
  36. package/dist/hosting/kv.js +206 -0
  37. package/dist/hosting/listing.d.ts +34 -0
  38. package/dist/hosting/listing.js +50 -0
  39. package/dist/hosting/names.d.ts +16 -0
  40. package/dist/hosting/names.js +28 -0
  41. package/dist/hosting/provision/account.d.ts +11 -0
  42. package/dist/hosting/provision/account.js +86 -0
  43. package/dist/hosting/provision/dispatcher.d.ts +44 -0
  44. package/dist/hosting/provision/dispatcher.js +101 -0
  45. package/dist/hosting/provision/ingress.d.ts +12 -0
  46. package/dist/hosting/provision/ingress.js +31 -0
  47. package/dist/hosting/provision/plan.d.ts +143 -0
  48. package/dist/hosting/provision/plan.js +246 -0
  49. package/dist/hosting/provision/routing.d.ts +82 -0
  50. package/dist/hosting/provision/routing.js +226 -0
  51. package/dist/hosting/readiness.d.ts +53 -0
  52. package/dist/hosting/readiness.js +82 -0
  53. package/dist/hosting/refusal.d.ts +28 -0
  54. package/dist/hosting/refusal.js +13 -0
  55. package/dist/hosting/retirement.d.ts +69 -0
  56. package/dist/hosting/retirement.js +106 -0
  57. package/dist/hosting/retry-after.d.ts +6 -0
  58. package/dist/hosting/retry-after.js +55 -0
  59. package/dist/hosting/script.d.ts +186 -0
  60. package/dist/hosting/script.js +324 -0
  61. package/dist/hosting/testing/fake-cloudflare.d.ts +211 -0
  62. package/dist/hosting/testing/fake-cloudflare.js +756 -0
  63. package/dist/hosting/testing/index.d.ts +6 -0
  64. package/dist/hosting/testing/index.js +6 -0
  65. package/dist/hosting/time.d.ts +4 -0
  66. package/dist/hosting/time.js +20 -0
  67. package/dist/identity/material.d.ts +0 -1
  68. package/dist/identity/material.js +1 -10
  69. package/dist/legacy/direct/index.d.ts +5 -9
  70. package/dist/legacy/direct/index.js +5 -9
  71. package/dist/namespace/configure.d.ts +2 -6
  72. package/dist/namespace/configure.js +15 -12
  73. package/dist/namespace/coordinates.d.ts +3 -26
  74. package/dist/namespace/coordinates.js +13 -45
  75. package/dist/namespace/deploy-release.d.ts +23 -30
  76. package/dist/namespace/deploy-release.js +123 -153
  77. package/dist/namespace/hosting.d.ts +29 -0
  78. package/dist/namespace/hosting.js +120 -0
  79. package/dist/namespace/index.d.ts +7 -10
  80. package/dist/namespace/index.js +7 -10
  81. package/dist/namespace/list.d.ts +11 -26
  82. package/dist/namespace/list.js +49 -125
  83. package/dist/namespace/readiness.d.ts +10 -20
  84. package/dist/namespace/readiness.js +15 -65
  85. package/dist/namespace/record.d.ts +21 -0
  86. package/dist/namespace/record.js +53 -0
  87. package/dist/release/index.d.ts +0 -1
  88. package/dist/release/index.js +0 -1
  89. package/package.json +11 -7
  90. package/template/astrale.config.ts +10 -7
  91. package/dist/namespace/api.d.ts +0 -71
  92. package/dist/namespace/api.js +0 -265
  93. package/dist/namespace/calls.d.ts +0 -29
  94. package/dist/namespace/calls.js +0 -104
  95. package/dist/namespace/routing.d.ts +0 -55
  96. package/dist/namespace/routing.js +0 -165
  97. package/dist/namespace/script.d.ts +0 -106
  98. package/dist/namespace/script.js +0 -278
  99. package/dist/release/staged-worker-name.d.ts +0 -3
  100. package/dist/release/staged-worker-name.js +0 -10
@@ -0,0 +1,82 @@
1
+ import { wait } from './time.js';
2
+ /** The response header the generated Worker answers its provider version in. */
3
+ export const VERSION_HEADER = 'x-astrale-worker-version';
4
+ /** Reach a deployment through its public URL, with `fetch`. */
5
+ export function publicFetcher(fetcher) {
6
+ return (request) => fetcher(request);
7
+ }
8
+ /** Reach one deployment through a dispatch namespace binding (`env.<BINDING>.get(label)`). */
9
+ export function dispatchFetcher(namespace, label) {
10
+ return (request) => namespace.get(label).fetch(request);
11
+ }
12
+ /**
13
+ * Wait until a deployment answers from exactly the provider version this deploy made: every
14
+ * `check` passes on responses carrying `x-astrale-worker-version` equal to `version` (the UUID
15
+ * form of the script's `deployment_id`). A response from another version is propagation still in
16
+ * progress; any failure of `check` is retried every `retryMs` until `timeoutMs`. Requests never
17
+ * leave `url`'s origin, never follow redirects and send no version override: a dispatcher always
18
+ * serves a script's current version.
19
+ *
20
+ * `check` defaults to: every path of `paths` (default `/`) answers 200. A deployer that verifies
21
+ * the release it sealed passes its own check over the versioned fetch it receives.
22
+ */
23
+ export async function waitForReadiness(input) {
24
+ const origin = new URL(input.url);
25
+ if (origin.protocol !== 'https:' || origin.origin !== input.url.replace(/\/$/u, '')) {
26
+ throw new TypeError('Deployment readiness requires one exact HTTPS origin.');
27
+ }
28
+ const now = input.now ?? Date.now;
29
+ const pause = input.pause ?? wait;
30
+ const deadline = now() + input.timeoutMs;
31
+ const versioned = versionFetch(origin.origin, input.version, input.fetcher);
32
+ const check = input.check ?? pathsAnswer(input.paths ?? ['/']);
33
+ let last;
34
+ for (;;) {
35
+ input.signal?.throwIfAborted();
36
+ const attempt = AbortSignal.timeout(input.attemptTimeoutMs ?? 5_000);
37
+ try {
38
+ await check(versioned, input.signal === undefined ? attempt : AbortSignal.any([input.signal, attempt]));
39
+ return;
40
+ }
41
+ catch (cause) {
42
+ input.signal?.throwIfAborted();
43
+ last = cause;
44
+ }
45
+ if (now() >= deadline)
46
+ break;
47
+ await pause(input.retryMs ?? 1_000, input.signal);
48
+ }
49
+ throw new Error(`The deployment did not serve from version ${input.version} within ` +
50
+ `${input.timeoutMs / 1_000} s.`, { cause: last });
51
+ }
52
+ /**
53
+ * A fetch that stays on the deployment origin and admits only responses of the expected provider
54
+ * version: a response of another version is refused, as propagation still in progress.
55
+ */
56
+ export function versionFetch(origin, version, fetcher) {
57
+ const expected = new URL(origin).origin;
58
+ return async (input, init) => {
59
+ const request = input instanceof Request
60
+ ? new Request(input, { ...init, redirect: 'manual' })
61
+ : new Request(new URL(String(input), expected), { ...init, redirect: 'manual' });
62
+ if (new URL(request.url).origin !== expected) {
63
+ throw new TypeError('Deployment readiness cannot leave the deployment origin.');
64
+ }
65
+ const response = await fetcher(request);
66
+ if (response.headers.get(VERSION_HEADER) !== version) {
67
+ await response.body?.cancel();
68
+ throw new Error('The deployment answered from another provider version.');
69
+ }
70
+ return response;
71
+ };
72
+ }
73
+ function pathsAnswer(paths) {
74
+ return async (fetcher, signal) => {
75
+ for (const path of paths) {
76
+ const response = await fetcher(path, { method: 'GET', signal });
77
+ await response.body?.cancel();
78
+ if (response.status !== 200)
79
+ throw new Error(`${path} answered HTTP ${response.status}.`);
80
+ }
81
+ };
82
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Why the hosting kit refused before, or instead of, a write. A refusal is a decision about what
3
+ * the account holds, never a transport failure (`CloudflareApiError`): rerunning without changing
4
+ * the account or the request is refused again.
5
+ */
6
+ export declare class HostingRefusal extends Error {
7
+ readonly name = "HostingRefusal";
8
+ readonly reason:
9
+ /** The namespace's routing KV does not exist: provision the namespace first. */
10
+ 'not-provisioned'
11
+ /** The label names another deployment's script or record; nothing was written. */
12
+ | 'deployment-conflict'
13
+ /** The label is withdrawn and is never served again. */
14
+ | 'deployment-retired'
15
+ /** A stored `record:` is not a record the kit publishes (media type, bytes, metadata). */
16
+ | 'record-invalid'
17
+ /** A stored `retirement:` holds another value than `withdrawn`. */
18
+ | 'retirement-invalid'
19
+ /** A `current:` value (stored or asked) is not a deployment of its name. */
20
+ | 'route-invalid'
21
+ /** A `cron:` value (stored or asked) is not distinct canonical UTC crons. */
22
+ | 'schedules-invalid'
23
+ /** Provisioning would take over what serves something else. */
24
+ | 'resource-conflict'
25
+ /** The API token cannot read what provisioning must observe first. */
26
+ | 'token-scope';
27
+ constructor(reason: HostingRefusal['reason'], message: string);
28
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Why the hosting kit refused before, or instead of, a write. A refusal is a decision about what
3
+ * the account holds, never a transport failure (`CloudflareApiError`): rerunning without changing
4
+ * the account or the request is refused again.
5
+ */
6
+ export class HostingRefusal extends Error {
7
+ name = 'HostingRefusal';
8
+ reason;
9
+ constructor(reason, message) {
10
+ super(message);
11
+ this.reason = reason;
12
+ }
13
+ }
@@ -0,0 +1,69 @@
1
+ import type { WorkerRecord } from '@astrale-os/sdk/deployment/worker';
2
+ import type { CloudflareClient } from './client.js';
3
+ import type { RoutingKv } from './kv.js';
4
+ import type { NamespaceScript } from './script.js';
5
+ import { WITHDRAWN } from './kv.js';
6
+ /** The dispatcher's retirement probe path; it is never a call of the deployment. */
7
+ export declare const RETIREMENT_PROBE_PATH = "/__services_retirement_probe__";
8
+ /** How long Workers KV takes to carry a tombstone to every dispatcher. */
9
+ export declare const RETIREMENT_PROPAGATION_MS = 65000;
10
+ /** How long a caller waits before probing again when the dispatcher does not answer yet. */
11
+ export declare const RETIREMENT_PROBE_RETRY_MS = 15000;
12
+ /**
13
+ * Phase one of a retirement: write the tombstone `retirement:<label>` = `withdrawn`, unless it is
14
+ * already there, read back before it counts. From then on the dispatcher answers
15
+ * `DEPLOYMENT_RETIRED` to every call of the label, while its record endpoint keeps serving
16
+ * `record:<label>`. A withdrawal is permanent: nothing deletes the tombstone.
17
+ */
18
+ export declare function markRetired(kv: RoutingKv, label: string): Promise<{
19
+ readonly previous: 'absent' | typeof WITHDRAWN;
20
+ }>;
21
+ /** The tombstone of a label: `withdrawn`, or undefined; any other value is refused. */
22
+ export declare function readRetirement(kv: RoutingKv, label: string): Promise<typeof WITHDRAWN | undefined>;
23
+ /**
24
+ * Whether the dispatcher answers `DEPLOYMENT_RETIRED` for the deployment at `url`, exactly as
25
+ * kernel-protocol defines it: 410, `x-astrale-deployment-retired: 1`, and the envelope naming the
26
+ * deployment origin. The probe path is never counted as a call. A failed probe answers false;
27
+ * only the caller's cancellation rejects.
28
+ */
29
+ export declare function retirementVisible(input: {
30
+ readonly fetch: typeof fetch;
31
+ readonly url: string;
32
+ readonly timeoutMs?: number;
33
+ readonly signal?: AbortSignal;
34
+ }): Promise<boolean>;
35
+ /** Where phase two of a retirement stands. */
36
+ export type RetirementCompletion = {
37
+ readonly kind: 'removed';
38
+ } | {
39
+ readonly kind: 'propagating';
40
+ readonly retryAfterMs: number;
41
+ } | {
42
+ readonly kind: 'conflict';
43
+ readonly message: string;
44
+ };
45
+ /**
46
+ * Phase two of a retirement: once the tombstone has propagated (`RETIREMENT_PROPAGATION_MS` after
47
+ * `retiredAt`, when the caller kept it) and the dispatcher answers `DEPLOYMENT_RETIRED` for the
48
+ * label (`probe`, usually `retirementVisible` through the public URL), delete its script, and with
49
+ * it the key bound to it; until then delete nothing and answer `propagating`. The record stays at
50
+ * `record:<label>`.
51
+ *
52
+ * The script is deleted only when it is the caller's deployment: a stored record must be the one
53
+ * the caller published (`record`: the same media type and bytes, or `null` for none) when the
54
+ * caller passes it, and the script must satisfy `owns` (its tags, its etag) when the caller passes
55
+ * it; otherwise nothing is deleted (`conflict`). An absent script is already removed. Rerunning
56
+ * converges.
57
+ */
58
+ export declare function completeRetirement(input: {
59
+ readonly client: CloudflareClient;
60
+ readonly kv: RoutingKv;
61
+ readonly namespace: string;
62
+ readonly label: string;
63
+ readonly probe: () => Promise<boolean>;
64
+ readonly record?: WorkerRecord | null;
65
+ readonly owns?: (script: NamespaceScript) => boolean;
66
+ /** When the tombstone was written (epoch ms), if the caller keeps it. */
67
+ readonly retiredAt?: number;
68
+ readonly now?: () => number;
69
+ }): Promise<RetirementCompletion>;
@@ -0,0 +1,106 @@
1
+ import { readRecord, sameRecord } from './activation.js';
2
+ import { retirementKey, WITHDRAWN } from './kv.js';
3
+ import { HostingRefusal } from './refusal.js';
4
+ import { deleteScript, observeScript } from './script.js';
5
+ /** The dispatcher's retirement probe path; it is never a call of the deployment. */
6
+ export const RETIREMENT_PROBE_PATH = '/__services_retirement_probe__';
7
+ /** How long Workers KV takes to carry a tombstone to every dispatcher. */
8
+ export const RETIREMENT_PROPAGATION_MS = 65_000;
9
+ /** How long a caller waits before probing again when the dispatcher does not answer yet. */
10
+ export const RETIREMENT_PROBE_RETRY_MS = 15_000;
11
+ const DEFAULT_PROBE_TIMEOUT_MS = 10_000;
12
+ /**
13
+ * Phase one of a retirement: write the tombstone `retirement:<label>` = `withdrawn`, unless it is
14
+ * already there, read back before it counts. From then on the dispatcher answers
15
+ * `DEPLOYMENT_RETIRED` to every call of the label, while its record endpoint keeps serving
16
+ * `record:<label>`. A withdrawal is permanent: nothing deletes the tombstone.
17
+ */
18
+ export async function markRetired(kv, label) {
19
+ if ((await readRetirement(kv, label)) === WITHDRAWN)
20
+ return Object.freeze({ previous: WITHDRAWN });
21
+ await kv.put(retirementKey(label), WITHDRAWN, 'text/plain');
22
+ return Object.freeze({ previous: 'absent' });
23
+ }
24
+ /** The tombstone of a label: `withdrawn`, or undefined; any other value is refused. */
25
+ export async function readRetirement(kv, label) {
26
+ const value = await kv.get(retirementKey(label));
27
+ if (value === undefined || value === WITHDRAWN)
28
+ return value;
29
+ throw new HostingRefusal('retirement-invalid', `The tombstone of ${label} holds another value than ${WITHDRAWN}.`);
30
+ }
31
+ /**
32
+ * Whether the dispatcher answers `DEPLOYMENT_RETIRED` for the deployment at `url`, exactly as
33
+ * kernel-protocol defines it: 410, `x-astrale-deployment-retired: 1`, and the envelope naming the
34
+ * deployment origin. The probe path is never counted as a call. A failed probe answers false;
35
+ * only the caller's cancellation rejects.
36
+ */
37
+ export async function retirementVisible(input) {
38
+ const deployment = new URL(input.url).origin;
39
+ const probe = new URL(RETIREMENT_PROBE_PATH, deployment);
40
+ probe.searchParams.set('retirement', Date.now().toString(36));
41
+ const timeout = AbortSignal.timeout(input.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS);
42
+ try {
43
+ const response = await input.fetch(probe, {
44
+ method: 'GET',
45
+ headers: { accept: 'application/json', 'cache-control': 'no-cache' },
46
+ redirect: 'manual',
47
+ signal: input.signal === undefined ? timeout : AbortSignal.any([input.signal, timeout]),
48
+ });
49
+ const text = await response.text();
50
+ if (response.status !== 410 || response.headers.get('x-astrale-deployment-retired') !== '1') {
51
+ return false;
52
+ }
53
+ const body = JSON.parse(text);
54
+ return (body.error?.code === 3002 &&
55
+ body.error.data?.code === 'DEPLOYMENT_RETIRED' &&
56
+ body.error.data.details?.deployment === deployment);
57
+ }
58
+ catch (cause) {
59
+ if (input.signal?.aborted === true)
60
+ throw input.signal.reason ?? cause;
61
+ return false;
62
+ }
63
+ }
64
+ /**
65
+ * Phase two of a retirement: once the tombstone has propagated (`RETIREMENT_PROPAGATION_MS` after
66
+ * `retiredAt`, when the caller kept it) and the dispatcher answers `DEPLOYMENT_RETIRED` for the
67
+ * label (`probe`, usually `retirementVisible` through the public URL), delete its script, and with
68
+ * it the key bound to it; until then delete nothing and answer `propagating`. The record stays at
69
+ * `record:<label>`.
70
+ *
71
+ * The script is deleted only when it is the caller's deployment: a stored record must be the one
72
+ * the caller published (`record`: the same media type and bytes, or `null` for none) when the
73
+ * caller passes it, and the script must satisfy `owns` (its tags, its etag) when the caller passes
74
+ * it; otherwise nothing is deleted (`conflict`). An absent script is already removed. Rerunning
75
+ * converges.
76
+ */
77
+ export async function completeRetirement(input) {
78
+ const { client, label } = input;
79
+ if (input.record !== undefined) {
80
+ const published = await readRecord(input.kv, label);
81
+ if (published !== undefined && !sameRecord(published, input.record)) {
82
+ return conflict(`The record published for ${label} is not this deployment's.`);
83
+ }
84
+ }
85
+ const script = await observeScript(client, input.namespace, label);
86
+ if (script !== undefined && input.owns !== undefined && !input.owns(script)) {
87
+ return conflict(`The script ${label} is not this deployment's; nothing was deleted.`);
88
+ }
89
+ if (input.retiredAt !== undefined) {
90
+ const remaining = input.retiredAt + RETIREMENT_PROPAGATION_MS - (input.now ?? Date.now)();
91
+ if (remaining > 0)
92
+ return Object.freeze({ kind: 'propagating', retryAfterMs: remaining });
93
+ }
94
+ if (!(await input.probe())) {
95
+ return Object.freeze({ kind: 'propagating', retryAfterMs: RETIREMENT_PROBE_RETRY_MS });
96
+ }
97
+ if (script !== undefined)
98
+ await deleteScript(client, input.namespace, label);
99
+ return Object.freeze({ kind: 'removed' });
100
+ }
101
+ function conflict(message) {
102
+ return Object.freeze({
103
+ kind: 'conflict',
104
+ message: message.length <= 500 ? message : `${message.slice(0, 497)}...`,
105
+ });
106
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The wait an RFC 9110 `Retry-After` states, in milliseconds: delay seconds or an HTTP-date in any
3
+ * of its three forms, as of `now`. Anything else states no wait (undefined). Delay seconds too
4
+ * large to count in milliseconds state an unbounded wait, which no policy admits.
5
+ */
6
+ export declare function retryAfterMs(value: string | null, now: number): number | undefined;
@@ -0,0 +1,55 @@
1
+ // RFC 9110 §5.6.7: a recipient accepts all three HTTP-date forms, each case-sensitive and in GMT.
2
+ const HTTP_DATES = [
3
+ // IMF-fixdate: Sun, 06 Nov 1994 08:49:37 GMT
4
+ /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), (?<day>\d{2}) (?<month>[A-Z][a-z]{2}) (?<year>\d{4}) (?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2}) GMT$/u,
5
+ // Obsolete RFC 850 form: Sunday, 06-Nov-94 08:49:37 GMT
6
+ /^(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (?<day>\d{2})-(?<month>[A-Z][a-z]{2})-(?<year>\d{2}) (?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2}) GMT$/u,
7
+ // Obsolete asctime form: Sun Nov 6 08:49:37 1994
8
+ /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) (?<month>[A-Z][a-z]{2}) (?<day>\d{2}| \d) (?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2}) (?<year>\d{4})$/u,
9
+ ];
10
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
11
+ /**
12
+ * The wait an RFC 9110 `Retry-After` states, in milliseconds: delay seconds or an HTTP-date in any
13
+ * of its three forms, as of `now`. Anything else states no wait (undefined). Delay seconds too
14
+ * large to count in milliseconds state an unbounded wait, which no policy admits.
15
+ */
16
+ export function retryAfterMs(value, now) {
17
+ if (value === null)
18
+ return undefined;
19
+ const text = value.trim();
20
+ if (/^\d+$/u.test(text)) {
21
+ const waitMs = Number(text) * 1_000;
22
+ return Number.isSafeInteger(waitMs) ? waitMs : Number.POSITIVE_INFINITY;
23
+ }
24
+ const at = httpDate(text, now);
25
+ return at === undefined ? undefined : Math.max(0, at - now);
26
+ }
27
+ /** The instant an RFC 9110 HTTP-date names, or undefined when the text is not a real one. */
28
+ function httpDate(text, now) {
29
+ const fields = HTTP_DATES.map((form) => form.exec(text)?.groups).find(Boolean);
30
+ if (fields === undefined)
31
+ return undefined;
32
+ const month = MONTHS.indexOf(fields.month);
33
+ const day = Number(fields.day);
34
+ const [hour, minute, second] = [fields.hour, fields.minute, fields.second].map(Number);
35
+ if (month < 0 || hour > 23 || minute > 59 || second > 60)
36
+ return undefined;
37
+ const year = fields.year.length === 2 ? rfc850Year(Number(fields.year), now) : Number(fields.year);
38
+ // setUTCFullYear keeps years below 100 literal, which Date.UTC would move to the 1900s.
39
+ const date = new Date(0);
40
+ date.setUTCFullYear(year, month, day);
41
+ if (date.getUTCDate() !== day)
42
+ return undefined;
43
+ return date.getTime() + ((hour * 60 + minute) * 60 + second) * 1_000;
44
+ }
45
+ /**
46
+ * RFC 9110 §5.6.7: a two-digit year that would be more than 50 years ahead names the most recent
47
+ * past year with those last two digits.
48
+ */
49
+ function rfc850Year(lastTwoDigits, now) {
50
+ const current = new Date(now).getUTCFullYear();
51
+ const year = current - (current % 100) + lastTwoDigits;
52
+ if (year > current + 50)
53
+ return year - 100;
54
+ return year <= current - 50 ? year + 100 : year;
55
+ }
@@ -0,0 +1,186 @@
1
+ import type { CloudflareClient } from './client.js';
2
+ import { SIGNING_IDENTITY_BINDING } from '../identity/binding.js';
3
+ import { BUILD_DIGEST_BINDING } from '../worker/build-digest.js';
4
+ import { SETTINGS_BINDING } from '../worker/settings.js';
5
+ import { type ScriptAsset } from './assets.js';
6
+ /** The deployment URL, which is also its issuer. */
7
+ export declare const WORKER_URL_BINDING = "WORKER_URL";
8
+ /** The binding a deployment's Worker reads its provider version from, for readiness. */
9
+ export declare const VERSION_METADATA_BINDING = "ASTRALE_VERSION_METADATA";
10
+ /** The service binding through which a deployment reaches an instance router. */
11
+ export declare const ROUTER_BINDING = "ROUTER";
12
+ /** The binding of a deployment's static assets, when its build has a frontend. */
13
+ export declare const ASSETS_BINDING = "ASSETS";
14
+ /** The secret the generated Worker reads its private signing JWK from (`ASTRALE_SIGNING_IDENTITY`). */
15
+ export { BUILD_DIGEST_BINDING, SETTINGS_BINDING, SIGNING_IDENTITY_BINDING };
16
+ /** The build a script runs: `astrale-artifact:<hex digest of its build manifest>`. */
17
+ export declare const ARTIFACT_TAG_PREFIX = "astrale-artifact:";
18
+ /** The deployment line a script is a version of: `astrale-line:<line>`. */
19
+ export declare const LINE_TAG_PREFIX = "astrale-line:";
20
+ /** The signing key a script binds: `astrale-key:<RFC 7638 thumbprint>`. */
21
+ export declare const KEY_TAG_PREFIX = "astrale-key:";
22
+ /** The multipart type of each Worker module kind, as the Workers upload API reads it. */
23
+ declare const MODULE_PART_TYPES: Readonly<{
24
+ esm: "application/javascript+module";
25
+ wasm: "application/wasm";
26
+ text: "text/plain";
27
+ data: "application/octet-stream";
28
+ }>;
29
+ /** One module of a script upload. */
30
+ export interface ScriptModule {
31
+ readonly name: string;
32
+ readonly kind: keyof typeof MODULE_PART_TYPES;
33
+ readonly bytes: Uint8Array;
34
+ }
35
+ /** One binding of a script upload, as Cloudflare's upload metadata names it. */
36
+ export type ScriptBinding = {
37
+ readonly type: 'plain_text';
38
+ readonly name: string;
39
+ readonly text: string;
40
+ } | {
41
+ readonly type: 'secret_text';
42
+ readonly name: string;
43
+ readonly text: string;
44
+ } | {
45
+ readonly type: 'service';
46
+ readonly name: string;
47
+ readonly service: string;
48
+ } | {
49
+ readonly type: 'dispatch_namespace';
50
+ readonly name: string;
51
+ readonly namespace: string;
52
+ } | {
53
+ readonly type: 'version_metadata';
54
+ readonly name: string;
55
+ } | {
56
+ readonly type: 'assets';
57
+ readonly name: string;
58
+ };
59
+ /**
60
+ * What identifies one namespace script at the provider: its code hash (`etag`), its tags (the
61
+ * kit's `astrale-artifact:`, `astrale-line:` and `astrale-key:` decoded, every tag kept), and when
62
+ * Cloudflare created it. `version` is the provider version serving now (`deployment_id`), read
63
+ * only to check readiness: a secret written in place mints a new one, so nothing keeps it.
64
+ */
65
+ export interface NamespaceScript {
66
+ readonly etag: string;
67
+ readonly version: string;
68
+ readonly tags: readonly string[];
69
+ readonly artifact?: string;
70
+ readonly line?: string;
71
+ readonly keyId?: string;
72
+ readonly createdOn?: string;
73
+ }
74
+ /** Everything one new deployment binds, all fixed at its create-only upload. */
75
+ export interface DeploymentBindingsInput {
76
+ /** The deployment URL, which is its issuer (`WORKER_URL`). */
77
+ readonly url: string;
78
+ /** The build digest of the bytes uploaded (`ASTRALE_BUILD_DIGEST`). */
79
+ readonly buildDigest: string;
80
+ /**
81
+ * A generated Worker's execution settings, already encoded (`ASTRALE_SETTINGS`), bound after the
82
+ * build digest; none when absent (a Worker host binds a deployer's settings among its variables).
83
+ */
84
+ readonly settings?: string;
85
+ /** The author's plain variables, bound in name order. */
86
+ readonly vars: Readonly<Record<string, string>>;
87
+ /** Service bindings, in the order given. */
88
+ readonly services?: readonly {
89
+ readonly name: string;
90
+ readonly service: string;
91
+ }[];
92
+ /** Dispatch namespace bindings, in the order given. */
93
+ readonly dispatchNamespaces?: readonly {
94
+ readonly name: string;
95
+ readonly namespace: string;
96
+ }[];
97
+ /** The Worker the `ROUTER` service binding reaches; none when absent. */
98
+ readonly router?: string;
99
+ /** The private JWK text of its key (`ASTRALE_SIGNING_IDENTITY`); none when absent. */
100
+ readonly signingIdentity?: string;
101
+ /** Its secrets, bound in name order. */
102
+ readonly secrets: Readonly<Record<string, string>>;
103
+ }
104
+ /**
105
+ * The bindings of one new deployment, in their order: `WORKER_URL`, `ASTRALE_BUILD_DIGEST`,
106
+ * `ASTRALE_SETTINGS` (when given), the variables, the service and dispatch namespace bindings,
107
+ * `ROUTER` (when given), the version metadata `ASTRALE_VERSION_METADATA`, the key
108
+ * `ASTRALE_SIGNING_IDENTITY` (when given), then the secrets. A script upload with assets adds
109
+ * `ASSETS` last. Nothing ever adds a binding afterwards: only a secret's value is rewritten in
110
+ * place. A name bound twice is refused.
111
+ */
112
+ export declare function deploymentBindings(input: DeploymentBindingsInput): readonly ScriptBinding[];
113
+ /**
114
+ * The tags of one deployment script: the caller's `extra` tags first (a host's own, such as
115
+ * Services' `astrale-service` and `astrale-install:`), then its artifact (build), line and, when
116
+ * it binds one, key.
117
+ */
118
+ export declare function deploymentTags(input: {
119
+ readonly buildDigest: string;
120
+ readonly line: string;
121
+ readonly keyId?: string;
122
+ readonly extra?: readonly string[];
123
+ }): readonly string[];
124
+ /** The artifact tag value of one build: the hexadecimal digest of its manifest. */
125
+ export declare function artifactOf(buildDigest: string): string;
126
+ /** What one create-only script upload carries. */
127
+ export interface ScriptUpload {
128
+ readonly client: CloudflareClient;
129
+ readonly namespace: string;
130
+ /** The script name: the deployment label. */
131
+ readonly label: string;
132
+ readonly mainModule: string;
133
+ readonly modules: readonly ScriptModule[];
134
+ readonly assets?: readonly ScriptAsset[];
135
+ readonly compatibilityDate: string;
136
+ readonly compatibilityFlags: readonly string[];
137
+ /** `deploymentBindings`; `ASSETS` is added when there are assets. */
138
+ readonly bindings: readonly ScriptBinding[];
139
+ /** `deploymentTags`. */
140
+ readonly tags: readonly string[];
141
+ }
142
+ /**
143
+ * Upload one deployment script, create-only: `If-None-Match: *` makes the provider refuse with
144
+ * 412 (`isExistingScript`) when the label already names a script, so an upload never replaces
145
+ * another deployment. Its static assets are uploaded first, through the namespace's asset session;
146
+ * the Worker routes first and serves its frontend itself.
147
+ */
148
+ export declare function uploadDeploymentScript(input: ScriptUpload): Promise<NamespaceScript>;
149
+ /** Whether a create-only upload was refused because the label already names a script. */
150
+ export declare function isExistingScript(cause: unknown): boolean;
151
+ /** Read one script's identity, or undefined when the namespace holds no script by that name. */
152
+ export declare function observeScript(client: CloudflareClient, namespace: string, label: string): Promise<NamespaceScript | undefined>;
153
+ /** The names of one script's `secret_text` bindings, sorted. */
154
+ export declare function scriptSecretNames(client: CloudflareClient, namespace: string, label: string): Promise<readonly string[]>;
155
+ /** One script's settings as the provider reads them back: runtime and bindings, never secrets. */
156
+ export interface ScriptSettings {
157
+ readonly compatibilityDate: string;
158
+ readonly compatibilityFlags: readonly string[];
159
+ readonly bindings: readonly Readonly<Record<string, unknown>>[];
160
+ }
161
+ export declare function scriptSettings(client: CloudflareClient, namespace: string, label: string): Promise<ScriptSettings>;
162
+ /**
163
+ * Whether settings read back bind exactly this runtime and these bindings: compatibility date and
164
+ * flags (in any order), every binding by name, type and target or text, secrets by name only.
165
+ */
166
+ export declare function settingsBind(settings: ScriptSettings, expected: {
167
+ readonly compatibilityDate: string;
168
+ readonly compatibilityFlags: readonly string[];
169
+ readonly bindings: readonly ScriptBinding[];
170
+ }): boolean;
171
+ /**
172
+ * Rewrite one secret of one script in place. Cloudflare keeps the script's name, etag, tags and
173
+ * other bindings, mints a new version, and answers only the secret's name and type.
174
+ */
175
+ export declare function putScriptSecret(client: CloudflareClient, namespace: string, label: string, name: string, value: string): Promise<void>;
176
+ /**
177
+ * Delete one script (with `?force=true`) and require that it is gone. A delete Cloudflare answers
178
+ * as unavailable counts when the script is observed absent afterwards.
179
+ */
180
+ export declare function deleteScript(client: CloudflareClient, namespace: string, label: string): Promise<void>;
181
+ /** A provider version in the UUID form a Worker's version metadata reports. */
182
+ export declare function versionUuid(version: string): string;
183
+ /** `/accounts/<a>/workers/dispatch/namespaces/<namespace>/scripts/<label><suffix>`. */
184
+ export declare function scriptPath(client: CloudflareClient, namespace: string, label: string, suffix?: string): string;
185
+ /** Decode the script identity an upload or a script read returns. */
186
+ export declare function decodeScript(value: unknown): NamespaceScript;