@takosjp/yurucommu-core 4.1.1 → 4.1.3
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/package.json
CHANGED
package/src/backend/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Hono, type Context } from "hono";
|
|
1
|
+
import { Hono, type Context, type Next } from "hono";
|
|
2
2
|
import { MOBILE_PUSH_REGISTRATION_PATH } from "./lib/mobile-contract.ts";
|
|
3
3
|
import { NOTIFICATION_PUSHER_REGISTRATION_PATH } from "./lib/notification-pusher-contract.ts";
|
|
4
4
|
import type { Env, EnvVars, Variables } from "./types.ts";
|
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
wrapRuntimeMessageBatch,
|
|
11
11
|
type PortableWorkerBindings,
|
|
12
12
|
} from "./runtime/lane.ts";
|
|
13
|
+
import {
|
|
14
|
+
configuredAppUrl,
|
|
15
|
+
establishRequestPublicOrigin,
|
|
16
|
+
withRequiredBackgroundPublicOrigin,
|
|
17
|
+
} from "./runtime/public-origin.ts";
|
|
13
18
|
import type { EdgeQueueBatch } from "./runtime/edge-facades.ts";
|
|
14
19
|
import {
|
|
15
20
|
getMobileOidcAudience,
|
|
@@ -925,6 +930,63 @@ function mountStaticFallback(app: YurucommuApp): void {
|
|
|
925
930
|
});
|
|
926
931
|
}
|
|
927
932
|
|
|
933
|
+
/**
|
|
934
|
+
* Give this request an `APP_URL` when the Host, not the deployer, chose it.
|
|
935
|
+
*
|
|
936
|
+
* Registered BEFORE every other route so `/readyz` and the `.well-known`
|
|
937
|
+
* discovery documents see the same origin the rest of the app mints ids from —
|
|
938
|
+
* a readiness probe that reported `APP_URL` missing on a Worker whose origin
|
|
939
|
+
* only its own traffic can reveal would never go ready.
|
|
940
|
+
*
|
|
941
|
+
* It runs on EVERY lane, because the lane names the SHAPE OF THE BINDINGS and
|
|
942
|
+
* not who chose the endpoint. A Takoform-hosted Worker on the production
|
|
943
|
+
* Takoserver receives raw Cloudflare bindings — the `cloudflare` lane — and its
|
|
944
|
+
* origin is still allocated by a `WorkerEndpoint` after the `WorkerVersion`
|
|
945
|
+
* that would have carried `APP_URL` was sealed, and the Takoform module passes
|
|
946
|
+
* no `APP_URL` either. Gating the inference on the lane left exactly that
|
|
947
|
+
* install unable to ever name itself, and so never ready.
|
|
948
|
+
*
|
|
949
|
+
* What makes the request URL trustworthy is a property of the runtime rather
|
|
950
|
+
* than of the binding shape: the origin on the request is the origin the
|
|
951
|
+
* request actually arrived on, and no forwarded header is ever consulted (see
|
|
952
|
+
* runtime/public-origin.ts). The remaining risk on a Worker deployed straight
|
|
953
|
+
* to Cloudflare — it also answers on workers.dev and on every custom domain and
|
|
954
|
+
* route pattern its account holds, so the first hostname to arrive would name
|
|
955
|
+
* the instance for good — is answered the same way it always was: an operator
|
|
956
|
+
* who owns more than one of those hostnames sets `APP_URL`, which always wins.
|
|
957
|
+
*
|
|
958
|
+
* It never fails a request. When no origin can be established — the request is
|
|
959
|
+
* plain http on a routable host, KV is unbound, an operator hand-wrote the pin
|
|
960
|
+
* — `APP_URL` simply stays unset, which `/readyz` already reports as a hard
|
|
961
|
+
* missing binding. Turning that into a 500 would take the readiness probe down
|
|
962
|
+
* with it and replace a precise answer with a generic one.
|
|
963
|
+
*/
|
|
964
|
+
function publicOriginMiddleware() {
|
|
965
|
+
let warned = false;
|
|
966
|
+
return async (
|
|
967
|
+
c: Context<{ Bindings: Env; Variables: Variables }>,
|
|
968
|
+
next: Next,
|
|
969
|
+
) => {
|
|
970
|
+
if (configuredAppUrl(c.env) !== null) return next();
|
|
971
|
+
try {
|
|
972
|
+
const origin = await establishRequestPublicOrigin(c.env, c.req.raw);
|
|
973
|
+
c.env = { ...c.env, APP_URL: origin };
|
|
974
|
+
warned = false;
|
|
975
|
+
} catch (error) {
|
|
976
|
+
// Once per isolate: this condition is a property of the deployment, not
|
|
977
|
+
// of the request, so it would otherwise repeat on every single one.
|
|
978
|
+
if (!warned) {
|
|
979
|
+
warned = true;
|
|
980
|
+
log.warn("Could not establish this instance's public origin", {
|
|
981
|
+
event: "runtime.public_origin.unestablished",
|
|
982
|
+
error,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return next();
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
928
990
|
export function createYurucommuBackendApp(
|
|
929
991
|
options: CreateYurucommuBackendAppOptionsV1 = {},
|
|
930
992
|
): YurucommuApp {
|
|
@@ -941,6 +1003,10 @@ export function createYurucommuBackendApp(
|
|
|
941
1003
|
}
|
|
942
1004
|
}
|
|
943
1005
|
|
|
1006
|
+
// Before the readiness probes, because they report on APP_URL and the
|
|
1007
|
+
// `.well-known` discovery documents publish it. Reads no body and consults no
|
|
1008
|
+
// route, so it does not weaken the body-cap ordering below.
|
|
1009
|
+
app.use("*", publicOriginMiddleware());
|
|
944
1010
|
mountReadinessRoutes(app, options.discovery);
|
|
945
1011
|
// Body-size cap must run BEFORE any handler reads the body or executes
|
|
946
1012
|
// expensive auth / rate-limit logic. Mounted after readiness probes so
|
|
@@ -1056,9 +1122,15 @@ export default {
|
|
|
1056
1122
|
bindings: WorkerBindings,
|
|
1057
1123
|
): Promise<void> {
|
|
1058
1124
|
const lane = resolveRuntimeLane(bindings.YURUCOMMU_RUNTIME_LANE);
|
|
1125
|
+
// Federation delivery signs and addresses from this instance's own actor
|
|
1126
|
+
// ids, and a queue invocation has no request to learn the origin from. When
|
|
1127
|
+
// `APP_URL` is unset and no request has pinned one yet, this THROWS: the
|
|
1128
|
+
// batch is retried later, after traffic has established the origin, instead
|
|
1129
|
+
// of being delivered under `undefined/ap/users/…` to peers that would cache
|
|
1130
|
+
// it. See runtime/public-origin.ts.
|
|
1059
1131
|
return handleYurucommuQueueBatch(
|
|
1060
1132
|
wrapRuntimeMessageBatch(batch, lane),
|
|
1061
|
-
wrapRuntimeBindings(bindings),
|
|
1133
|
+
await withRequiredBackgroundPublicOrigin(wrapRuntimeBindings(bindings)),
|
|
1062
1134
|
);
|
|
1063
1135
|
},
|
|
1064
1136
|
|
package/src/backend/public.ts
CHANGED
|
@@ -53,6 +53,21 @@ export {
|
|
|
53
53
|
wrapRuntimeBindings,
|
|
54
54
|
wrapRuntimeMessageBatch,
|
|
55
55
|
} from "./runtime/lane.ts";
|
|
56
|
+
// The public origin: `APP_URL` when the deployment could carry one, and the
|
|
57
|
+
// origin one request established when only the Host knew it. A product that
|
|
58
|
+
// composes its own Worker entry uses these for the handlers the core default
|
|
59
|
+
// export does not own.
|
|
60
|
+
export {
|
|
61
|
+
CANONICAL_ORIGIN_KV_KEY,
|
|
62
|
+
PublicOriginError,
|
|
63
|
+
canonicalPublicOrigin,
|
|
64
|
+
configuredAppUrl,
|
|
65
|
+
establishRequestPublicOrigin,
|
|
66
|
+
peekObservedPublicOrigin,
|
|
67
|
+
requireBackgroundPublicOrigin,
|
|
68
|
+
resetObservedPublicOrigin,
|
|
69
|
+
withRequiredBackgroundPublicOrigin,
|
|
70
|
+
} from "./runtime/public-origin.ts";
|
|
56
71
|
export {
|
|
57
72
|
EDGE_KV_MAX_EXPIRATION_TTL_SECONDS,
|
|
58
73
|
EDGE_KV_MIN_EXPIRATION_TTL_SECONDS,
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one absolute origin this instance is: `APP_URL`, or the one a request
|
|
3
|
+
* established when the Host — not the deployer — chose it.
|
|
4
|
+
*
|
|
5
|
+
* Every federated identity this app mints is absolute. Actor ids, activity and
|
|
6
|
+
* object ids, `inbox` / `outbox` / `followers` collections, the OIDC
|
|
7
|
+
* `redirect_uri`, notification links, and the `.well-known` discovery documents
|
|
8
|
+
* are all `${APP_URL}/…`, and a wrong one is not a broken page — it is a
|
|
9
|
+
* permanent, federated wrong answer that remote servers have already cached.
|
|
10
|
+
*
|
|
11
|
+
* `APP_URL` is a plain variable, and on a Host-assigned endpoint it cannot
|
|
12
|
+
* always be one. A Takoform `WorkerEndpoint` allocates the Worker's public
|
|
13
|
+
* origin AFTER the `WorkerVersion` that would have carried the variable is
|
|
14
|
+
* already immutable, so the deployer does not know the value at apply time and
|
|
15
|
+
* there is no second apply that could inject it. The origin exists, but only
|
|
16
|
+
* the Host knows it, and the only place it is ever spoken is on the requests
|
|
17
|
+
* the Host routes here.
|
|
18
|
+
*
|
|
19
|
+
* This has nothing to do with the runtime lane. The lane names the SHAPE OF THE
|
|
20
|
+
* BINDINGS; a Takoform-hosted Worker on the production Takoserver runs on raw
|
|
21
|
+
* Cloudflare bindings — the `cloudflare` lane — and is in exactly the same
|
|
22
|
+
* position, because the Takoform module passes no `APP_URL` there either. So on
|
|
23
|
+
* EVERY lane an unset `APP_URL` is answered by OBSERVING one request and
|
|
24
|
+
* PINNING what it observed:
|
|
25
|
+
*
|
|
26
|
+
* 1. `APP_URL` is authoritative whenever it is set. It is used exactly as the
|
|
27
|
+
* operator wrote it and is never validated, cached, or persisted here —
|
|
28
|
+
* an operator who sets it has already decided, and this module has no
|
|
29
|
+
* standing to refuse a value the previous release accepted.
|
|
30
|
+
* 2. Otherwise the origin PINNED IN KV wins, for every request and for
|
|
31
|
+
* background work alike. First writer wins: once a value is stored, no
|
|
32
|
+
* later request replaces it, whatever `Host` that request carried.
|
|
33
|
+
* 3. Otherwise a request may establish it, from the request URL's own origin
|
|
34
|
+
* and from nothing else.
|
|
35
|
+
* 4. Otherwise there is no origin, and background work refuses rather than
|
|
36
|
+
* minting `undefined/ap/users/alice`.
|
|
37
|
+
*
|
|
38
|
+
* WHAT IS TRUSTED. The request URL as the runtime delivers it, and only that.
|
|
39
|
+
* Not `X-Forwarded-Host`, not `X-Forwarded-Proto`, not `Host` read out of the
|
|
40
|
+
* headers — nothing a client can write. That URL is the endpoint the request
|
|
41
|
+
* genuinely arrived on, under every host this bundle runs on. Cloudflare
|
|
42
|
+
* Workers build `request.url` from the connection the edge terminated. Both
|
|
43
|
+
* wrapper hosts route by hostname and hand over the request they received on
|
|
44
|
+
* the Worker's own public endpoint: Takoserver's managed Workers-for-Platforms
|
|
45
|
+
* gateway looks up a host route for `new URL(request.url).hostname` and
|
|
46
|
+
* dispatches the SAME `Request` object, and the self-host workerd router picks
|
|
47
|
+
* a service from a table keyed by hostname and forwards unchanged. A hostname
|
|
48
|
+
* nobody published for this Worker is a 404 before any of this code runs, so
|
|
49
|
+
* the origin on the request is one the Host assigned — which is exactly the
|
|
50
|
+
* value that could not be delivered as a var.
|
|
51
|
+
*
|
|
52
|
+
* WHAT THIS DOES NOT DECIDE. A Worker published straight to Cloudflare by an
|
|
53
|
+
* operator who holds several hostnames answers on all of them, and first writer
|
|
54
|
+
* wins means the first one to arrive names the instance. That operator knows
|
|
55
|
+
* their hostnames and sets `APP_URL`, which always wins and is never pinned.
|
|
56
|
+
* Inference is the answer for the deployment that CANNOT be told its origin,
|
|
57
|
+
* not a preference over being told.
|
|
58
|
+
*
|
|
59
|
+
* WHY HTTPS. A public fediverse origin is https; Cloudflare terminates TLS in
|
|
60
|
+
* front of a Worker on a public hostname, and Takoserver's own `WorkerEndpoint`
|
|
61
|
+
* can only ever assign an https origin. Requiring it here means an http request
|
|
62
|
+
* cannot pin an origin that would then sign deliveries and mint actor ids.
|
|
63
|
+
* Loopback http is the one exception, because `localhost` is not routable and
|
|
64
|
+
* is the origin a developer actually serves on — `wrangler dev` included.
|
|
65
|
+
*
|
|
66
|
+
* A host that terminates TLS in FRONT of the runtime and speaks plain http to
|
|
67
|
+
* it therefore establishes nothing: `request.url` is `http://…` on a routable
|
|
68
|
+
* name and the derivation refuses. That deployment must set `APP_URL`, which it
|
|
69
|
+
* can, because an operator who terminates TLS chose the hostname themselves.
|
|
70
|
+
* Refusing is the point — the alternative is trusting a forwarded-proto header
|
|
71
|
+
* that the same proxy may or may not be the only writer of.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
import type { Env, EnvVars } from "../types.ts";
|
|
75
|
+
import type { IKeyValueStore } from "./types.ts";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Where the observed origin is pinned.
|
|
79
|
+
*
|
|
80
|
+
* The key is shared with the origin pin Yurucommu's own generated Worker entry
|
|
81
|
+
* writes, so a deployment that pinned an origin under the product's
|
|
82
|
+
* implementation keeps it when the product delegates to this one.
|
|
83
|
+
*/
|
|
84
|
+
export const CANONICAL_ORIGIN_KV_KEY =
|
|
85
|
+
"__yurucommu/runtime/canonical-origin/v1";
|
|
86
|
+
|
|
87
|
+
/** No usable public origin, or a candidate that may not become one. */
|
|
88
|
+
export class PublicOriginError extends Error {
|
|
89
|
+
constructor(message: string) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.name = "PublicOriginError";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Hostnames whose http origin is still trustworthy, because they are not
|
|
97
|
+
* routable off the machine. `*.localhost` is included: RFC 6761 reserves the
|
|
98
|
+
* whole tree for loopback, and a self-host Worker endpoint on a local Takoserver
|
|
99
|
+
* is `<script>.localhost`.
|
|
100
|
+
*/
|
|
101
|
+
function isLoopbackHostname(hostname: string): boolean {
|
|
102
|
+
return (
|
|
103
|
+
hostname === "localhost" ||
|
|
104
|
+
hostname === "127.0.0.1" ||
|
|
105
|
+
hostname === "[::1]" ||
|
|
106
|
+
hostname.endsWith(".localhost")
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Reduce a candidate to a bare origin, or refuse it.
|
|
112
|
+
*
|
|
113
|
+
* Refuses anything that is not just an origin — a path, a query, a fragment,
|
|
114
|
+
* embedded credentials — because the value is concatenated with `/ap/users/…`
|
|
115
|
+
* at hundreds of call sites and a stray path would silently produce a second,
|
|
116
|
+
* parallel set of actor ids.
|
|
117
|
+
*/
|
|
118
|
+
export function canonicalPublicOrigin(value: string): string {
|
|
119
|
+
let url: URL;
|
|
120
|
+
try {
|
|
121
|
+
url = new URL(value);
|
|
122
|
+
} catch {
|
|
123
|
+
throw new PublicOriginError(
|
|
124
|
+
`"${value}" is not a URL and cannot be this instance's public origin.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
url.username !== "" ||
|
|
129
|
+
url.password !== "" ||
|
|
130
|
+
url.search !== "" ||
|
|
131
|
+
url.hash !== "" ||
|
|
132
|
+
url.pathname !== "/"
|
|
133
|
+
) {
|
|
134
|
+
throw new PublicOriginError(
|
|
135
|
+
`"${value}" is not a bare origin; this instance's public origin must ` +
|
|
136
|
+
`carry no path, query, fragment, or credentials.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (
|
|
140
|
+
url.protocol !== "https:" &&
|
|
141
|
+
!(url.protocol === "http:" && isLoopbackHostname(url.hostname))
|
|
142
|
+
) {
|
|
143
|
+
throw new PublicOriginError(
|
|
144
|
+
`"${value}" is not an https origin. A public origin observed from a ` +
|
|
145
|
+
`request must be https (loopback http is the only exception); set ` +
|
|
146
|
+
`APP_URL explicitly when this Worker is served over plain http.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return url.origin;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** `APP_URL` exactly as the operator set it, or null when it is not set. */
|
|
153
|
+
export function configuredAppUrl(env: Partial<EnvVars>): string | null {
|
|
154
|
+
const raw = typeof env.APP_URL === "string" ? env.APP_URL.trim() : "";
|
|
155
|
+
return raw.length > 0 ? raw : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The origin this isolate has already established.
|
|
160
|
+
*
|
|
161
|
+
* Cached because the alternative is a KV read on the hot path of every single
|
|
162
|
+
* request, and because the value cannot legitimately change: first writer wins,
|
|
163
|
+
* so a second read can only ever return what the first one did. An operator who
|
|
164
|
+
* deliberately re-pins a different origin (see {@link resetObservedPublicOrigin})
|
|
165
|
+
* is served the new value by isolates started after the change.
|
|
166
|
+
*/
|
|
167
|
+
let observedPublicOrigin: string | null = null;
|
|
168
|
+
|
|
169
|
+
/** Forget this isolate's observation. Tests, and an operator-driven re-pin. */
|
|
170
|
+
export function resetObservedPublicOrigin(): void {
|
|
171
|
+
observedPublicOrigin = null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** What this isolate has observed so far, without touching KV. */
|
|
175
|
+
export function peekObservedPublicOrigin(): string | null {
|
|
176
|
+
return observedPublicOrigin;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type PublicOriginEnv = Partial<EnvVars> & { KV?: IKeyValueStore };
|
|
180
|
+
|
|
181
|
+
function requireKv(env: PublicOriginEnv): IKeyValueStore {
|
|
182
|
+
if (!env.KV) {
|
|
183
|
+
throw new PublicOriginError(
|
|
184
|
+
"KV is not bound, so this instance's public origin can be neither read " +
|
|
185
|
+
"nor pinned. Bind KV, or set APP_URL.",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return env.KV;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function readPinnedOrigin(kv: IKeyValueStore): Promise<string | null> {
|
|
192
|
+
const stored = await kv.get(CANONICAL_ORIGIN_KV_KEY);
|
|
193
|
+
if (stored === null) return null;
|
|
194
|
+
// A stored value that no longer canonicalizes is a refusal, never a silent
|
|
195
|
+
// fallback to the current request: it means somebody wrote the key by hand.
|
|
196
|
+
return canonicalPublicOrigin(stored);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Establish this instance's public origin from one request, once.
|
|
201
|
+
*
|
|
202
|
+
* CONSISTENCY. The pin lives in KV, the one store both lanes always have (`DB`
|
|
203
|
+
* is equally present, but the origin is needed by the readiness probe and by
|
|
204
|
+
* queue work that must not open a transaction to learn its own name). KV is
|
|
205
|
+
* eventually consistent and has no compare-and-swap, so "first writer wins" is
|
|
206
|
+
* enforced by reading before writing and then READING BACK: an isolate that
|
|
207
|
+
* finds a different origin on the read-back lost the race and refuses this
|
|
208
|
+
* request rather than serving two identities. The next request reads the
|
|
209
|
+
* winner's value through the ordinary stored-value path. A read-back that has
|
|
210
|
+
* not converged yet (null) is treated as our own write, because we wrote it.
|
|
211
|
+
*/
|
|
212
|
+
export async function establishRequestPublicOrigin(
|
|
213
|
+
env: PublicOriginEnv,
|
|
214
|
+
request: Request,
|
|
215
|
+
): Promise<string> {
|
|
216
|
+
if (observedPublicOrigin !== null) return observedPublicOrigin;
|
|
217
|
+
|
|
218
|
+
const kv = requireKv(env);
|
|
219
|
+
const pinned = await readPinnedOrigin(kv);
|
|
220
|
+
if (pinned !== null) {
|
|
221
|
+
observedPublicOrigin = pinned;
|
|
222
|
+
return pinned;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const requestOrigin = canonicalPublicOrigin(new URL(request.url).origin);
|
|
226
|
+
await kv.put(CANONICAL_ORIGIN_KV_KEY, requestOrigin);
|
|
227
|
+
const readback = await kv.get(CANONICAL_ORIGIN_KV_KEY);
|
|
228
|
+
if (readback !== null && canonicalPublicOrigin(readback) !== requestOrigin) {
|
|
229
|
+
throw new PublicOriginError(
|
|
230
|
+
`this instance's public origin was concurrently pinned to ` +
|
|
231
|
+
`"${readback}" while this request was establishing ` +
|
|
232
|
+
`"${requestOrigin}". The pinned origin stands; retry.`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
observedPublicOrigin = requestOrigin;
|
|
236
|
+
return requestOrigin;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The public origin for work that has no request to read it from.
|
|
241
|
+
*
|
|
242
|
+
* Queue consumers sign federation deliveries and address them from this
|
|
243
|
+
* instance's actor ids; there is no request in scope and nothing to derive one
|
|
244
|
+
* from. `APP_URL` first, then the pinned origin, then a refusal — never a
|
|
245
|
+
* guess, and never `undefined` concatenated into an actor id.
|
|
246
|
+
*/
|
|
247
|
+
export async function requireBackgroundPublicOrigin(
|
|
248
|
+
env: PublicOriginEnv,
|
|
249
|
+
): Promise<string> {
|
|
250
|
+
const configured = configuredAppUrl(env);
|
|
251
|
+
if (configured !== null) return configured;
|
|
252
|
+
if (observedPublicOrigin !== null) return observedPublicOrigin;
|
|
253
|
+
|
|
254
|
+
const pinned = await readPinnedOrigin(requireKv(env));
|
|
255
|
+
if (pinned === null) {
|
|
256
|
+
throw new PublicOriginError(
|
|
257
|
+
"this instance's public origin has not been observed yet: APP_URL is " +
|
|
258
|
+
"unset and no request has pinned an origin. Serve one request on the " +
|
|
259
|
+
"Worker's public endpoint before background delivery can address " +
|
|
260
|
+
"anything.",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
observedPublicOrigin = pinned;
|
|
264
|
+
return pinned;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The env background work should run with: `APP_URL` present, or a refusal.
|
|
269
|
+
*
|
|
270
|
+
* Returned as a copy rather than by mutating the caller's bindings, so the same
|
|
271
|
+
* `env` may be handed to several handlers without one of them rewriting what
|
|
272
|
+
* the others read.
|
|
273
|
+
*/
|
|
274
|
+
export async function withRequiredBackgroundPublicOrigin(
|
|
275
|
+
env: Env,
|
|
276
|
+
): Promise<Env> {
|
|
277
|
+
if (configuredAppUrl(env) !== null) return env;
|
|
278
|
+
return { ...env, APP_URL: await requireBackgroundPublicOrigin(env) };
|
|
279
|
+
}
|