@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.
- package/dist/generated/pack-facts.json +131 -7
- package/dist/src/content-route.d.ts +8 -0
- package/dist/src/content-route.js +27 -0
- package/dist/src/derived-core.d.ts +17 -1
- package/dist/src/derived-core.js +51 -15
- package/dist/src/derived.d.ts +3 -0
- package/dist/src/events.d.ts +13 -2
- package/dist/src/events.js +14 -2
- package/dist/src/file-response.d.ts +4 -3
- package/dist/src/file-response.js +7 -4
- package/dist/src/pack-fetch.d.ts +5 -0
- package/dist/src/pack-fetch.js +99 -13
- package/dist/src/twin-fetch.d.ts +8 -0
- package/dist/src/twin-fetch.js +30 -0
- package/dist/vendor-hosts.cjs +35 -10
- package/dist/vendor-hosts.d.cts +3 -0
- package/generated/pack-facts.json +131 -7
- package/package.json +1 -1
- package/src/content-route.ts +24 -0
- package/src/derived-core.ts +67 -19
- package/src/derived.ts +3 -0
- package/src/events.ts +19 -2
- package/src/file-response.ts +7 -4
- package/src/pack-fetch.ts +104 -15
- package/src/twin-fetch.ts +26 -0
- package/vendor-hosts.cjs +35 -10
- package/vendor-hosts.d.cts +3 -0
|
@@ -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";
|
package/dist/src/derived-core.js
CHANGED
|
@@ -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.
|
|
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
|
|
145
|
-
|
|
157
|
+
ownField(node, key, value);
|
|
158
|
+
break;
|
|
146
159
|
}
|
|
147
160
|
const next = parts[i + 1];
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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) => {
|
package/dist/src/derived.d.ts
CHANGED
|
@@ -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 = {
|
package/dist/src/events.d.ts
CHANGED
|
@@ -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
|
-
|
|
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[]>;
|
package/dist/src/events.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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:
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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
|
-
|
|
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,
|
package/dist/src/pack-fetch.d.ts
CHANGED
|
@@ -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;
|