@volter/world-core 3.0.18 → 3.0.20

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.
@@ -1137,6 +1137,48 @@
1137
1137
  },
1138
1138
  "serveExport": "createGroqTwinServer"
1139
1139
  },
1140
+ "hackernews": {
1141
+ "adoption": {
1142
+ "sdks": [],
1143
+ "envStems": [
1144
+ "HN",
1145
+ "HACKERNEWS"
1146
+ ]
1147
+ },
1148
+ "hosts": [
1149
+ {
1150
+ "host": "hacker-news.firebaseio.com"
1151
+ },
1152
+ {
1153
+ "host": "news.ycombinator.com"
1154
+ }
1155
+ ],
1156
+ "archetype": "crud",
1157
+ "protocol": {
1158
+ "declared": "3",
1159
+ "major": 3,
1160
+ "standing": "current"
1161
+ },
1162
+ "transport": "rest",
1163
+ "resources": [
1164
+ "item",
1165
+ "user",
1166
+ "_vote",
1167
+ "_person",
1168
+ "_web_session",
1169
+ "_web_session_issue"
1170
+ ],
1171
+ "endpointEnvNone": "Hacker News publishes no SDK: clients fetch https://hacker-news.firebaseio.com/v0 directly and people use news.ycombinator.com in a browser, both reached by host",
1172
+ "credentialDoor": {
1173
+ "path": "/_twin/app-credentials",
1174
+ "body": {},
1175
+ "fill": {
1176
+ "HN_USERNAME": "acct",
1177
+ "HN_PASSWORD": "pw"
1178
+ }
1179
+ },
1180
+ "serveExport": "createHackernewsTwinServer"
1181
+ },
1140
1182
  "hubspot": {
1141
1183
  "adoption": {
1142
1184
  "sdks": [
@@ -1844,6 +1886,88 @@
1844
1886
  },
1845
1887
  "serveExport": "createPosthogTwinServer"
1846
1888
  },
1889
+ "reddit": {
1890
+ "adoption": {
1891
+ "sdks": [
1892
+ "snoowrap"
1893
+ ],
1894
+ "pypi": [
1895
+ "praw",
1896
+ "asyncpraw"
1897
+ ],
1898
+ "envStems": [
1899
+ "REDDIT"
1900
+ ]
1901
+ },
1902
+ "hosts": [
1903
+ {
1904
+ "host": "oauth.reddit.com"
1905
+ },
1906
+ {
1907
+ "host": "www.reddit.com"
1908
+ },
1909
+ {
1910
+ "host": "reddit.com"
1911
+ },
1912
+ {
1913
+ "host": "reddit-uploaded-media.s3-accelerate.amazonaws.com"
1914
+ },
1915
+ {
1916
+ "host": "reddit-uploaded-video.s3-accelerate.amazonaws.com"
1917
+ },
1918
+ {
1919
+ "host": "v.redd.it"
1920
+ },
1921
+ {
1922
+ "host": "i.redd.it"
1923
+ },
1924
+ {
1925
+ "host": "preview.redd.it"
1926
+ },
1927
+ {
1928
+ "host": "external-preview.redd.it"
1929
+ },
1930
+ {
1931
+ "host": "b.thumbs.redditmedia.com"
1932
+ }
1933
+ ],
1934
+ "archetype": "crud",
1935
+ "protocol": {
1936
+ "declared": "3",
1937
+ "major": 3,
1938
+ "standing": "current"
1939
+ },
1940
+ "transport": "rest",
1941
+ "resourcesUnreachable": {},
1942
+ "resources": [
1943
+ "account",
1944
+ "subreddit",
1945
+ "link",
1946
+ "comment",
1947
+ "asset",
1948
+ "app",
1949
+ "_access_token",
1950
+ "_token_issue",
1951
+ "_client_secret",
1952
+ "_rate_window",
1953
+ "_vote",
1954
+ "_membership",
1955
+ "_person",
1956
+ "_web_session",
1957
+ "_web_session_issue"
1958
+ ],
1959
+ "endpointEnvNone": "Reddit's clients name oauth.reddit.com and www.reddit.com themselves (the campaign's sender writes them as constants); the World's injector routes those hosts, and no client of the demand reads a base-URL variable",
1960
+ "credentialDoor": {
1961
+ "path": "/_twin/app-credentials",
1962
+ "body": {
1963
+ "username": "volter"
1964
+ },
1965
+ "fill": {
1966
+ "REDDIT_ACCESS_TOKEN": "access_token"
1967
+ }
1968
+ },
1969
+ "serveExport": "createRedditTwinServer"
1970
+ },
1847
1971
  "resend": {
1848
1972
  "adoption": {
1849
1973
  "pypi": [
@@ -2817,27 +2941,27 @@
2817
2941
  },
2818
2942
  {
2819
2943
  "host": "x.com",
2820
- "pathPattern": "^/i/oauth2/authorize/?$|^/_twin/|^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
2944
+ "pathPattern": "^/i/oauth2/authorize/?$|^/_twin/|^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$|^/home/?$|^/i/flow/login/?$|^/$"
2821
2945
  },
2822
2946
  {
2823
2947
  "host": "twitter.com",
2824
- "pathPattern": "^/i/oauth2/authorize/?$|^/_twin/|^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
2948
+ "pathPattern": "^/i/oauth2/authorize/?$|^/_twin/|^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$|^/home/?$|^/i/flow/login/?$|^/$"
2825
2949
  },
2826
2950
  {
2827
2951
  "host": "www.x.com",
2828
- "pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
2952
+ "pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$|^/home/?$|^/i/flow/login/?$|^/$"
2829
2953
  },
2830
2954
  {
2831
2955
  "host": "www.twitter.com",
2832
- "pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
2956
+ "pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$|^/home/?$|^/i/flow/login/?$|^/$"
2833
2957
  },
2834
2958
  {
2835
2959
  "host": "mobile.twitter.com",
2836
- "pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
2960
+ "pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$|^/home/?$|^/i/flow/login/?$|^/$"
2837
2961
  },
2838
2962
  {
2839
2963
  "host": "mobile.x.com",
2840
- "pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$"
2964
+ "pathPattern": "^/(?!_twin/)[A-Za-z0-9_]{1,15}/status/[0-9]{1,19}/?$|^/(?!(?:i|en|home|explore|notifications|messages|settings|search|compose|login|logout|signup|tos|privacy|about|account|intent|share|hashtag|lists|bookmarks|jobs|download|grok|premium|communities|creators|twin|_twin)/?$)[A-Za-z0-9_]{1,15}/?$|^/home/?$|^/i/flow/login/?$|^/$"
2841
2965
  },
2842
2966
  {
2843
2967
  "host": "pbs.twimg.com",
@@ -2863,7 +2987,6 @@
2863
2987
  "oauth_client",
2864
2988
  "oauth1_app",
2865
2989
  "account",
2866
- "session",
2867
2990
  "auth_request",
2868
2991
  "post",
2869
2992
  "rate_limit",
@@ -2873,6 +2996,7 @@
2873
2996
  "repost",
2874
2997
  "bookmark",
2875
2998
  "_impressions",
2999
+ "_card",
2876
3000
  "_grant",
2877
3001
  "_oauth1_grant",
2878
3002
  "_rate_window",
@@ -0,0 +1,8 @@
1
+ /** A `/@<host>/<path>` address: the host normalised, the path under it (slashes collapsed), and why the spelling cannot
2
+ * be the host's content (`refused`: a port other than the default), when it cannot. */
3
+ export type ContentAddress = {
4
+ host: string;
5
+ path: string;
6
+ refused?: string;
7
+ };
8
+ export declare function contentAddressOf(pathname: string): ContentAddress | undefined;
@@ -0,0 +1,27 @@
1
+ // THE CONTENT ROUTE'S ADDRESS (architecture, "Screens", "A page at a World's place"): at a World's place, what a page
2
+ // reaches on another of the pack's hosts that a content screen serves is `<base>/@<host>/<path>`. This reads that
3
+ // address as a browser would have written it: a doubled slash collapsed, `%40` for `@`, the host's case, a trailing dot
4
+ // and a default port (80, 443) normalised away. What it reads is a candidate only: the route is taken when the host is
5
+ // one a built content screen names (pack-fetch.ts), and a request naming no such host is general routing's, as before.
6
+ export function contentAddressOf(pathname) {
7
+ const collapsed = pathname.replace(/\/{2,}/g, '/');
8
+ const named = /^\/(?:@|%40)([^/]+)(\/.*)?$/i.exec(collapsed);
9
+ if (!named)
10
+ return undefined;
11
+ let token;
12
+ try {
13
+ token = decodeURIComponent(named[1]).toLowerCase();
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ const at = /^([^:]*)(?::(\d*))?$/.exec(token);
19
+ if (!at)
20
+ return undefined;
21
+ const host = at[1].replace(/\.$/, '');
22
+ if (!/^[a-z0-9_-]+(?:\.[a-z0-9_-]+)+$/.test(host))
23
+ return undefined;
24
+ const port = at[2];
25
+ const refused = port && port !== '80' && port !== '443' ? `port ${port} is not the host's` : undefined;
26
+ return { host, path: named[2] ?? '/', ...(refused ? { refused } : {}) };
27
+ }
@@ -3,7 +3,7 @@ import { GitObjectStore } from './git/objects.js';
3
3
  import { GitRefs } from './git/refs.js';
4
4
  import { type SocketDecl, type SocketSession } from './sockets.js';
5
5
  import { type MachinePool } from './machines.js';
6
- import { type ApplicationAnswer, type EventRender, type EventScheme, type EventsDecl, type EventValues } from './events.js';
6
+ import { type ApplicationAnswer, type ApplicationStandIn, type EventRender, type EventScheme, type EventsDecl, type EventValues } from './events.js';
7
7
  import type { RateBudgetDeclaration } from './rateBudget.js';
8
8
  import { type HandlerCrypto } from './signing.js';
9
9
  import { type ManagedDatabase } from './managed-database.js';
@@ -268,6 +268,8 @@ export type VendorManifest = {
268
268
  descriptor: Omit<PackDescriptor, 'vendor'>;
269
269
  ingest?: IngestDecl;
270
270
  rateBudget?: RateBudgetDeclaration;
271
+ /** the vendor has no vendor-backed half (architecture, "The descriptor"): packOf copies it, and derives no state system */
272
+ vendorBacked?: DerivedManifest['vendorBacked'];
271
273
  };
272
274
  /** `code` as the vendor writes it: a string, or a number (Discord's JSON error codes are integers) */
273
275
  export type ErrorSpec = {
@@ -293,6 +295,15 @@ export type ScreenDecl = {
293
295
  demand: string;
294
296
  status: 'done' | 'todo';
295
297
  controls?: string[];
298
+ /** wherever this screen would answer (its host, a World's place, its path alone), a request the pack's API names for
299
+ * its method is the operation's, not this screen's: a site whose pages draw while its forms and links act on
300
+ * operations the spec writes at the same paths (Hacker News's /login page posts to POST /login; its front page at `/`
301
+ * would otherwise take GET /vote) (architecture, "Screens") */
302
+ yieldsToApi?: true;
303
+ /** a content screen that takes a form at a World's place (`<base>/@<host>/…`), beside GET and HEAD: the methods it
304
+ * takes, with its demand (an upload bucket a page posts a lease's form to: Reddit's S3 hosts) (architecture, "A page
305
+ * at a World's place") */
306
+ takes?: ReadonlyArray<'POST'>;
296
307
  /** the vendor's documentation of the round trip or the screen */
297
308
  source: string;
298
309
  };
@@ -651,6 +662,9 @@ export declare function flagOf(m: Pick<DerivedManifest, 'booleans'>, value: unkn
651
662
  * text: a field the operation's spec types as a number or a boolean (`scalars`, by bracket path, written by
652
663
  * world-tooling's formScalarsOf) is read as one when its text is that literal; every other field stays the text sent
653
664
  * (`metadata[order]=007`, `name=2024`). */
665
+ /** A field set as the object's own, whatever its name (`__proto__`, `constructor` and `prototype` are data a caller
666
+ * may name: Stripe's metadata keys are the caller's), never through what the object inherits. */
667
+ export declare function ownField(node: Record<string, unknown> | unknown[], key: string, value: unknown): void;
654
668
  export declare function parseBracketForm(text: string, scalars?: Readonly<Record<string, string | undefined>>): Record<string, unknown>;
655
669
  /** A request's parameters, its query's and its body's. With the manifest's `body.form.coerce`, a form's fields the
656
670
  * operation's spec types as numbers or booleans are read as them (parseBracketForm); without an operation (a twin door)
@@ -686,6 +700,8 @@ export type CoreScope = {
686
700
  readOnly?: boolean;
687
701
  /** `semantics/tenant.ts`: the tenant the caller acts in (the manifest's `tenant`), or undefined for none */
688
702
  tenant?: (ctx: HandlerContext) => string | undefined | Promise<string | undefined>;
703
+ /** the application a walk stands in for: what the vendor asks it (`ctx.ask`) is answered by it (events.ts) */
704
+ application?: ApplicationStandIn;
689
705
  };
690
706
  /** The lane a vendor's router sent a request to (pack-fetch's laneRouter sets it). */
691
707
  export declare const LANE_HEADER = "x-volter-lane";
@@ -124,6 +124,13 @@ export function flagOf(m, value) {
124
124
  * text: a field the operation's spec types as a number or a boolean (`scalars`, by bracket path, written by
125
125
  * world-tooling's formScalarsOf) is read as one when its text is that literal; every other field stays the text sent
126
126
  * (`metadata[order]=007`, `name=2024`). */
127
+ /** A field set as the object's own, whatever its name (`__proto__`, `constructor` and `prototype` are data a caller
128
+ * may name: Stripe's metadata keys are the caller's), never through what the object inherits. */
129
+ export function ownField(node, key, value) {
130
+ Object.defineProperty(node, key, { value, writable: true, enumerable: true, configurable: true });
131
+ }
132
+ /** A field the object holds as its own, never one it inherits (`constructor` is every object's). */
133
+ const ownValue = (node, key) => (Object.hasOwn(node, key) ? node[key] : undefined);
127
134
  export function parseBracketForm(text, scalars) {
128
135
  const out = {};
129
136
  for (const [rawKey, raw] of new URLSearchParams(text)) {
@@ -133,22 +140,35 @@ export function parseBracketForm(text, scalars) {
133
140
  : kind === 'integer' ? (/^-?\d+$/.test(raw) ? Number(raw) : raw)
134
141
  : kind === 'number' ? (/^-?\d+(\.\d+)?$/.test(raw) ? Number(raw) : raw)
135
142
  : raw;
143
+ // every field is the node's own, read and written as such: a key naming an object's machinery (`__proto__[x]`,
144
+ // `constructor[prototype][x]`) is a field of that name, never a walk into what every object inherits
136
145
  let node = out;
137
- parts.forEach((key, i) => {
146
+ for (const [i, key] of parts.entries()) {
147
+ // an array holds its items by index (`a[]`, `a[0]`): a named key under one (`a[]=1&a[length]=-1`) has nowhere to
148
+ // go, as a key under text has none (`a=1&a[b]=2`): the first shape stands
149
+ if (Array.isArray(node) && key !== '' && !/^\d+$/.test(key))
150
+ break;
138
151
  if (i === parts.length - 1) {
139
152
  if (key === '') {
140
153
  if (Array.isArray(node))
141
154
  node.push(value);
142
155
  }
143
156
  else
144
- node[key] = value;
145
- return;
157
+ ownField(node, key, value);
158
+ break;
146
159
  }
147
160
  const next = parts[i + 1];
148
- if (node[key] === undefined)
149
- node[key] = next === '' || /^\d+$/.test(next) ? [] : {};
150
- node = node[key];
151
- });
161
+ if (key === '')
162
+ break;
163
+ let child = ownValue(node, key);
164
+ if (child === undefined) {
165
+ child = next === '' || /^\d+$/.test(next) ? [] : {};
166
+ ownField(node, key, child);
167
+ }
168
+ if (child === null || typeof child !== 'object')
169
+ break;
170
+ node = child;
171
+ }
152
172
  }
153
173
  return out;
154
174
  }
@@ -186,24 +206,32 @@ export async function readParams(manifest, request, operation) {
186
206
  const parts = boundary === null ? [] : multipartParts(new Uint8Array(await request.arrayBuffer()), boundary);
187
207
  const out = { ...query };
188
208
  for (const part of parts) {
209
+ // each part a field of its own name, whatever the name (`__proto__` included: ownField)
189
210
  if (part.filename === null) {
190
- out[part.name] = new TextDecoder().decode(part.body);
211
+ ownField(out, part.name, new TextDecoder().decode(part.body));
191
212
  continue;
192
213
  }
193
214
  const bytes = part.body;
194
215
  const parsed = { name: part.filename, type: part.type ?? '', size: bytes.byteLength, content: new TextDecoder().decode(bytes) };
195
- out[part.name] = Object.defineProperty(parsed, 'bytes', { value: bytes, enumerable: false });
216
+ ownField(out, part.name, Object.defineProperty(parsed, 'bytes', { value: bytes, enumerable: false }));
196
217
  }
197
218
  return out;
198
219
  }
220
+ // NDJSON (`application/x-ndjson`) is lines of JSON, not a JSON document: the handler reads its text
221
+ const media = (type.split(';')[0] ?? '').trim().toLowerCase();
222
+ const isJson = media.includes('json') && !media.includes('ndjson');
223
+ // a form is a body labelled one (application/x-www-form-urlencoded); a body of any other type (an image, a video, an
224
+ // octet-stream PUT to an upload URL, a text/plain SQL statement, an NDJSON batch) is no form: its parameters are the
225
+ // query's, and the handler reads its text or bytes itself
226
+ const isForm = media === 'application/x-www-form-urlencoded';
227
+ const asJson = isJson || manifest.body.json === 'always';
228
+ if (!isForm && !asJson)
229
+ return query;
199
230
  const text = await bodyTextOf(request);
200
231
  if (!text)
201
232
  return query;
202
- // NDJSON (`application/x-ndjson`) is lines of JSON, not a JSON document: the handler reads its text
203
- const isJson = type.includes('json') && !type.includes('ndjson');
204
- const asJson = isJson || manifest.body.json === 'always';
205
233
  // `json: 'always'` reads a body labelled otherwise as JSON (GitHub reads curl -d's); a body that is not JSON is read
206
- // as the form it is labelled, never a crash (a sign-in page's own form, posted to the vendor's page)
234
+ // as a form only when it is labelled one (a sign-in page's own form, posted to the vendor's page), never a crash
207
235
  let body;
208
236
  if (asJson) {
209
237
  try {
@@ -212,6 +240,8 @@ export async function readParams(manifest, request, operation) {
212
240
  catch {
213
241
  if (isJson)
214
242
  throw new SyntaxError('malformed JSON body');
243
+ if (!isForm)
244
+ return query;
215
245
  body = parseBracketForm(text, scalars);
216
246
  }
217
247
  }
@@ -598,6 +628,8 @@ function recordedInput(m, call, merged) {
598
628
  return { door: id, ...asSent };
599
629
  return { operationId: id, ...asSent };
600
630
  }
631
+ /** The scope each call is served in (serveCore, contextFor), for the contexts a write opens for its hooks. */
632
+ const scopeOfCall = new WeakMap();
601
633
  async function writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt) {
602
634
  const withFiles = async (input) => { const files = input.operationId ? await keptFiles(m, call, root) : undefined; return files ? { ...input, files } : input; };
603
635
  const { resource: row, result } = await applyTwinWrite(m.service,
@@ -609,7 +641,9 @@ async function writeDetailed(m, call, resource, id, fields, operation, params, r
609
641
  : { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) } }, root);
610
642
  await keepCounts(m, call, resource, row, params, root, occurredAt);
611
643
  const body = view(m, resource, row);
612
- const context = () => contextFor(m, { ...call, request: call.request.clone() }, { ...(root !== undefined ? { root } : {}), clock: () => occurredAt });
644
+ // the write's own context for its hooks, in the scope the call was served in (a walk's application answers, a tenant,
645
+ // the managed database), at the write's moment
646
+ const context = () => contextFor(m, { ...call, request: call.request.clone() }, { ...(scopeOfCall.get(call) ?? {}), ...(root !== undefined ? { root } : {}), clock: () => occurredAt });
613
647
  if (m.onWrite)
614
648
  await m.onWrite({ operation, storedType: storedType(m, resource), body, ...(root !== undefined ? { root } : {}), occurredAt, request: call.request, context });
615
649
  if (m.events) {
@@ -940,6 +974,7 @@ export async function serveCore(m, call, scope = {}) {
940
974
  if (!resource || !decl)
941
975
  return { unmodeled: `no manifest entry for resource ${resource ?? '(none)'}` };
942
976
  call = adoptedPath(m, call, root);
977
+ scopeOfCall.set(call, scope);
943
978
  // the body is read from a copy, so the call's own request is still unread for the write hook's context
944
979
  const params = await boundaryParams(m, { ...call, request: call.request.clone() }, root);
945
980
  const idParam = subjectOf(m, resource, call);
@@ -1094,6 +1129,7 @@ export async function contextFor(m, call, scope) {
1094
1129
  const root = scope.root;
1095
1130
  const asAsked = call;
1096
1131
  call = adoptedPath(m, call, root);
1132
+ scopeOfCall.set(call, scope);
1097
1133
  const at = (scope.clock ?? worldNow)();
1098
1134
  // the guard's parse when it made one (taken once), else this context's own
1099
1135
  const parsed = takeParsedBody(call.request);
@@ -1134,7 +1170,7 @@ export async function contextFor(m, call, scope) {
1134
1170
  publicBase: twinPublicBase(call.request),
1135
1171
  scenario: scenarioDecisionOf(call.request),
1136
1172
  deliver: (url, init) => deliverToApplication(url, init),
1137
- ask: (url, init, within) => askApplication(url, init, within),
1173
+ ask: (url, init, within) => askApplication(url, init, within, scope.application),
1138
1174
  worldEnv: (name) => worldEnvValue(name),
1139
1175
  asVendor: (fn) => runAsVendorMove(fn),
1140
1176
  git: (name) => {
@@ -117,6 +117,9 @@ export type DerivedFetch = ((request: Request) => Promise<Response>) & {
117
117
  boards?: boolean;
118
118
  /** Whether a built screen of it takes a URL: a board frame it draws. */
119
119
  draws?(url: string): boolean;
120
+ /** The hosts its built content screens name: a vendor's lane router sends `<base>/@<host>/<path>` for one of them to
121
+ * this fetch (pack-fetch.ts). */
122
+ contentHosts?: ReadonlyArray<string>;
120
123
  };
121
124
  /** A workspace screen a pack draws, as `GET /twin` lists it: its id and path (Viewing a World). */
122
125
  export type Workspace = {
@@ -198,9 +198,20 @@ export type ApplicationAnswer = {
198
198
  body: string;
199
199
  missed?: 'timeout' | 'unreachable';
200
200
  };
201
+ /** Who answers what the vendor asks the application (`ctx.ask`) before the World's application route does, given to a
202
+ * pack's fetch (`PackFetchOptions.application`) and belonging to that fetch alone: an answer it gives is the
203
+ * application's; `'unreachable'` is a question nobody answers (a walk's life, a sealed World, takes no other); undefined
204
+ * is a question it does not take, which goes on as any other does: the application route, the World's egress rule, the
205
+ * network. A walk's stand-in answers every question (its life's answers, else unreachable); a hosted World's router
206
+ * answers only what it can answer truthfully (WorldDoors.askInWorld) and leaves the rest. */
207
+ export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{
208
+ status: number;
209
+ body: string;
210
+ } | 'unreachable' | undefined>;
201
211
  /** A message the vendor sends the application's server and decides by its answer (Stripe's real-time authorization
202
- * request, answered within its window): over the same transport, waited on for at most `within` milliseconds. */
203
- export declare function askApplication(url: string, init: RequestInit, within: number): Promise<ApplicationAnswer>;
212
+ * request, answered within its window): over the same transport, waited on for at most `within` milliseconds, the
213
+ * fetch's stand-in first when it was given one (its window the same). */
214
+ export declare function askApplication(url: string, init: RequestInit, within: number, standIn?: ApplicationStandIn): Promise<ApplicationAnswer>;
204
215
  /** Every event a write sends, rendered, stored when the vendor keeps its events, signed, delivered to each endpoint
205
216
  * that takes it, and recorded. */
206
217
  export declare function deliverEvents(service: string, decl: EventsDecl, write: EventWrite, data: (type: string) => Promise<Record<string, unknown>>, transport?: EventTransport, values?: (type: string) => Promise<Record<string, unknown>>, onAnswer?: (answer: DeliveryAnswer) => Promise<void>): Promise<EventDelivery[]>;
@@ -223,8 +223,20 @@ export async function deliverToApplication(url, init) {
223
223
  }
224
224
  }
225
225
  /** A message the vendor sends the application's server and decides by its answer (Stripe's real-time authorization
226
- * request, answered within its window): over the same transport, waited on for at most `within` milliseconds. */
227
- export async function askApplication(url, init, within) {
226
+ * request, answered within its window): over the same transport, waited on for at most `within` milliseconds, the
227
+ * fetch's stand-in first when it was given one (its window the same). */
228
+ export async function askApplication(url, init, within, standIn) {
229
+ if (standIn) {
230
+ let timer;
231
+ const late = new Promise((done) => { timer = setTimeout(() => done('late'), within); });
232
+ const stood = await Promise.race([standIn(url, init), late]).finally(() => clearTimeout(timer));
233
+ if (stood === 'late')
234
+ return { status: 0, body: '', missed: 'timeout' };
235
+ if (stood === 'unreachable')
236
+ return { status: 0, body: '', missed: 'unreachable' };
237
+ if (stood !== undefined)
238
+ return stood;
239
+ }
228
240
  const [{ appDestination, appFetch }, { worldEgressRefusal }] = await Promise.all([
229
241
  import('../app-route.cjs'),
230
242
  import('../network-policy.cjs'),
@@ -2,8 +2,9 @@
2
2
  export declare function contentTypeOf(path: string): string;
3
3
  export declare function fileResponse(path: string, init?: ResponseInit): Response;
4
4
  /**
5
- * Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: a range that does not parse, a
6
- * first-byte-pos past its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed
7
- * range that starts past the end is a 416. `nosniff` stops a browser second-guessing the content type the vendor gave.
5
+ * Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: every satisfiable range is a
6
+ * 206 with its Content-Range, `bytes=0-` (the whole body) included; a range that does not parse, a first-byte-pos past
7
+ * its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed range that starts past
8
+ * the end (any range of an empty body) is a 416. `nosniff` stops a browser second-guessing the content type the vendor gave.
8
9
  */
9
10
  export declare function bytesResponse(request: Request, bytes: Uint8Array, contentType: string, cacheControl?: string): Response;
@@ -21,9 +21,10 @@ export function fileResponse(path, init) {
21
21
  return new Response(readFileSync(path), { ...init, headers });
22
22
  }
23
23
  /**
24
- * Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: a range that does not parse, a
25
- * first-byte-pos past its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed
26
- * range that starts past the end is a 416. `nosniff` stops a browser second-guessing the content type the vendor gave.
24
+ * Stored bytes as a response, honouring a single `Range: bytes=` as RFC 9110 §14 has it: every satisfiable range is a
25
+ * 206 with its Content-Range, `bytes=0-` (the whole body) included; a range that does not parse, a first-byte-pos past
26
+ * its last-byte-pos (`bytes=5-2`) included, is ignored and the whole body is a 200; a well-formed range that starts past
27
+ * the end (any range of an empty body) is a 416. `nosniff` stops a browser second-guessing the content type the vendor gave.
27
28
  */
28
29
  export function bytesResponse(request, bytes, contentType, cacheControl = 'public, max-age=604800, immutable') {
29
30
  const total = bytes.length;
@@ -43,7 +44,9 @@ export function bytesResponse(request, bytes, contentType, cacheControl = 'publi
43
44
  if (start >= total || (range[1] === '' && Number(range[2]) === 0))
44
45
  return new Response(null, { status: 416, headers: { ...base, 'content-range': `bytes */${total}` } });
45
46
  }
46
- const partial = start !== 0 || end !== total - 1;
47
+ // every satisfiable range is answered as one, the whole body's `bytes=0-` included: a media player (Safari's) reads a
48
+ // 206 with its Content-Range as the server taking ranges, and a 200 as one it cannot seek in
49
+ const partial = valid;
47
50
  const body = bytes.slice(start, end + 1);
48
51
  return new Response(request.method === 'HEAD' ? null : new Blob([body]), {
49
52
  status: partial ? 206 : 200,
@@ -1,6 +1,7 @@
1
1
  import { type DerivedManifest, type Handler, type HandlerContext, type VendorManifest } from './derived-core.js';
2
2
  import { type DerivedFetch, type DerivedOwner, type DerivedSurface, type Workspace, type BoardFrame } from './derived.js';
3
3
  import { type PackScenarioAdapter } from './scenario.js';
4
+ import type { ApplicationStandIn } from './events.js';
4
5
  import { type SocketEngine } from './sockets.js';
5
6
  import { type GraphqlPart } from './graphql-wire.js';
6
7
  /** What a pack declares and writes, and nothing it assembles itself. */
@@ -56,6 +57,8 @@ export type LanePart = {
56
57
  serves?: (method: string, path: string) => boolean;
57
58
  boards?: boolean;
58
59
  draws?: (url: string) => boolean;
60
+ /** the hosts the lane's built content screens name: its vendor's router sends `<base>/@<host>/` there */
61
+ contentHosts?: ReadonlyArray<string>;
59
62
  };
60
63
  manifest: {
61
64
  doors?: ReadonlyArray<{
@@ -73,6 +76,8 @@ export type PackFetchOptions = {
73
76
  clock?: () => string;
74
77
  database?: string;
75
78
  scenarioPath?: string;
79
+ /** the application a walk stands in for (a life's `application` answers): this fetch's `ctx.ask` is answered by it */
80
+ application?: ApplicationStandIn;
76
81
  };
77
82
  /** A pack's HTTP surface, as the kernel serves it from the pack's parts. */
78
83
  export declare function createPackFetch(parts: PackParts, options?: PackFetchOptions): DerivedFetch;