@volter/world-core 3.0.163 → 3.0.165
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 +44 -3660
- package/dist/pack-facts.cjs +4 -4
- package/dist/src/browser-session.d.ts +7 -1
- package/dist/src/browser-session.js +21 -10
- package/dist/src/browser.d.ts +4 -0
- package/dist/src/browser.js +10 -0
- package/dist/src/derived-core.d.ts +46 -6
- package/dist/src/derived-core.js +60 -13
- package/dist/src/derived-real.js +159 -5
- package/dist/src/derived.d.ts +1 -1
- package/dist/src/events.d.ts +6 -3
- package/dist/src/events.js +52 -7
- package/dist/src/executor.d.ts +2 -2
- package/dist/src/executor.js +13 -0
- package/dist/src/file-response.d.ts +6 -2
- package/dist/src/file-response.js +29 -5
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +1 -0
- package/dist/src/git/smart-http-client.d.ts +28 -0
- package/dist/src/git/smart-http-client.js +95 -0
- package/dist/src/git/smart-http.d.ts +3 -0
- package/dist/src/git/smart-http.js +9 -0
- package/dist/src/head.d.ts +16 -1
- package/dist/src/head.js +52 -7
- package/dist/src/html.d.ts +29 -0
- package/dist/src/html.js +64 -0
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.js +5 -1
- package/dist/src/managed-database.d.ts +4 -2
- package/dist/src/managed-database.js +1 -0
- package/dist/src/media-header.d.ts +34 -0
- package/dist/src/media-header.js +129 -0
- package/dist/src/pack-fetch.d.ts +24 -3
- package/dist/src/pack-fetch.js +155 -96
- package/dist/src/pack-paths.d.ts +2 -3
- package/dist/src/pack-paths.js +3 -13
- package/dist/src/packRegistry.d.ts +17 -1
- package/dist/src/request-body.d.ts +2 -3
- package/dist/src/request-body.js +2 -3
- package/dist/src/runtime.d.ts +4 -1
- package/dist/src/runtime.js +5 -1
- package/dist/src/serve-http.d.ts +2 -2
- package/dist/src/serve-http.js +104 -19
- package/dist/src/smtp-server.d.ts +78 -0
- package/dist/src/smtp-server.js +160 -0
- package/dist/src/smtp.d.ts +29 -11
- package/dist/src/smtp.js +221 -52
- package/generated/pack-facts.json +44 -3660
- package/pack-facts.cjs +4 -4
- package/package.json +4 -2
- package/src/browser-session.ts +17 -5
- package/src/browser.ts +10 -0
- package/src/derived-core.ts +77 -19
- package/src/derived-real.ts +131 -4
- package/src/derived.ts +2 -2
- package/src/events.ts +54 -9
- package/src/executor.ts +13 -2
- package/src/file-response.ts +28 -6
- package/src/git/index.ts +1 -0
- package/src/git/smart-http-client.ts +99 -0
- package/src/git/smart-http.ts +7 -0
- package/src/head.ts +37 -6
- package/src/html.ts +72 -0
- package/src/index.ts +6 -2
- package/src/managed-database.ts +4 -3
- package/src/media-header.ts +108 -0
- package/src/pack-fetch.ts +142 -87
- package/src/pack-paths.ts +3 -11
- package/src/packRegistry.ts +9 -1
- package/src/request-body.ts +2 -3
- package/src/runtime.ts +5 -1
- package/src/serve-http.ts +81 -19
- package/src/smtp-server.ts +166 -0
- package/src/smtp.ts +199 -53
package/dist/pack-facts.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// THE PACK FACTS a process routes by (docs/contributing/architecture.md, "The catalog: where twins come from"): the facts
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// the platform owns (generated/pack-facts.json: its server backings, scripts/pack-facts.ts), overlaid by the facts of the
|
|
3
|
+
// packs the World installs (ADR 0034, decision 2). Each pack carries its own (`generated/pack-facts.json`), and the
|
|
4
|
+
// World's instance file (VOLTER_WORLD_INSTANCE, which every World process already has) names them as `packFacts`; a
|
|
5
|
+
// pack the World has not installed is unknown to it. Plain CommonJS, no
|
|
6
6
|
// dependency: the injector preloads it. Read once per process.
|
|
7
7
|
'use strict';
|
|
8
8
|
const fs = require('fs');
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { HandlerContext } from './derived-core.js';
|
|
2
|
+
import { type Person } from './people.js';
|
|
2
3
|
type Writer = Pick<HandlerContext, 'record' | 'secret' | 'occurredAt'>;
|
|
3
|
-
/** The
|
|
4
|
+
/** The value of the cookie `name` the request carries, as a browser sends it back. */
|
|
5
|
+
export declare function cookieOf(request: Request, name: string): string | undefined;
|
|
6
|
+
/** The named session cookie; malformed encodings do not name a session. */
|
|
4
7
|
export declare function browserSessionToken(request: Request, name: string): string | undefined;
|
|
8
|
+
/** The person the request's session cookie names, or undefined when nobody is signed in; with `scope`, only a session
|
|
9
|
+
* begun in it (a vendor's sandbox dashboard and its production one sign a person in apart). */
|
|
10
|
+
export declare function signedIn(ctx: HandlerContext, cookie: string, scope?: string): Person | undefined;
|
|
5
11
|
/** A new, independent sign-in, returning the cookie the vendor places on its response. */
|
|
6
12
|
export declare function startBrowserSession(ctx: Writer, email: string, cookie: string, scope?: string): Promise<string>;
|
|
7
13
|
/** End the session the request holds, then clear its cookie. Unknown cookies create no rows. */
|
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { personOf } from "./people.js";
|
|
2
|
+
/** The value of the cookie `name` the request carries, as a browser sends it back. */
|
|
3
|
+
export function cookieOf(request, name) {
|
|
3
4
|
for (const part of (request.headers.get('cookie') ?? '').split(';')) {
|
|
4
5
|
const at = part.indexOf('=');
|
|
5
|
-
if (at > 0 && part.slice(0, at).trim() === name)
|
|
6
|
-
|
|
7
|
-
return decodeURIComponent(part.slice(at + 1).trim());
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
return undefined;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
6
|
+
if (at > 0 && part.slice(0, at).trim() === name)
|
|
7
|
+
return decodeURIComponent(part.slice(at + 1).trim());
|
|
13
8
|
}
|
|
14
9
|
return undefined;
|
|
15
10
|
}
|
|
11
|
+
/** The named session cookie; malformed encodings do not name a session. */
|
|
12
|
+
export function browserSessionToken(request, name) {
|
|
13
|
+
try {
|
|
14
|
+
return cookieOf(request, name);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** The person the request's session cookie names, or undefined when nobody is signed in; with `scope`, only a session
|
|
21
|
+
* begun in it (a vendor's sandbox dashboard and its production one sign a person in apart). */
|
|
22
|
+
export function signedIn(ctx, cookie, scope) {
|
|
23
|
+
const token = cookieOf(ctx.call.request, cookie);
|
|
24
|
+
const session = token ? ctx.rowsRaw('_web_session').find((s) => s.token === token && (scope === undefined || s.scope === scope)) : undefined;
|
|
25
|
+
return session ? personOf(ctx, String(session.email)) : undefined;
|
|
26
|
+
}
|
|
16
27
|
/** A new, independent sign-in, returning the cookie the vendor places on its response. */
|
|
17
28
|
export async function startBrowserSession(ctx, email, cookie, scope) {
|
|
18
29
|
const issued = await ctx.record('_web_session_issue', { email, ...(scope !== undefined ? { scope } : {}) });
|
package/dist/src/browser.d.ts
CHANGED
|
@@ -9,5 +9,9 @@ export type SeedBrowser = {
|
|
|
9
9
|
context(options: BrowserContext): Promise<void>;
|
|
10
10
|
state(): Promise<BrowserState>;
|
|
11
11
|
};
|
|
12
|
+
/** The one password of an address in this World (ADR 0033 §5): `worldSecret('_world', <data>/_world,
|
|
13
|
+
* 'password:<address>')`, retained World state, so a seed signs a person up with it and a twin keeps only its hash. A
|
|
14
|
+
* seed reads it beside `browser(name)`; it is never a literal in a committed file. */
|
|
15
|
+
export declare function password(address: string): Promise<string>;
|
|
12
16
|
/** A seed's named browser. Every state write goes through the World's single HTTP loader. */
|
|
13
17
|
export declare function browser(name: string): Promise<SeedBrowser>;
|
package/dist/src/browser.js
CHANGED
|
@@ -64,6 +64,16 @@ function vendorResponse(response, vendor, twin, mount) {
|
|
|
64
64
|
}
|
|
65
65
|
return changed ? new Response(response.body, { status: response.status, statusText: response.statusText, headers }) : response;
|
|
66
66
|
}
|
|
67
|
+
/** The one password of an address in this World (ADR 0033 §5): `worldSecret('_world', <data>/_world,
|
|
68
|
+
* 'password:<address>')`, retained World state, so a seed signs a person up with it and a twin keeps only its hash. A
|
|
69
|
+
* seed reads it beside `browser(name)`; it is never a literal in a committed file. */
|
|
70
|
+
export async function password(address) {
|
|
71
|
+
const data = process.env.VOLTER_WORLD_DATA;
|
|
72
|
+
if (!process.env.VOLTER_WORLD || !data)
|
|
73
|
+
throw new Error('password(address) runs in a World seed, with its data directory');
|
|
74
|
+
const { worldSecret } = await import("./derived-core.js");
|
|
75
|
+
return worldSecret('_world', nodeBuiltin('path').join(data, '_world'), `password:${address}`);
|
|
76
|
+
}
|
|
67
77
|
/** A seed's named browser. Every state write goes through the World's single HTTP loader. */
|
|
68
78
|
export async function browser(name) {
|
|
69
79
|
assertBrowserName(name);
|
|
@@ -9,7 +9,7 @@ import type { RateBudgetDeclaration } from './rateBudget.js';
|
|
|
9
9
|
import { type HandlerCrypto } from './signing.js';
|
|
10
10
|
import { type ManagedDatabase } from './managed-database.js';
|
|
11
11
|
import { type RedisDialect, type RunItem } from './redis/engine.js';
|
|
12
|
-
import { type Mail, type
|
|
12
|
+
import { type Mail, type MailReply } from './smtp.js';
|
|
13
13
|
export { ownField, parseBracketForm } from './form.js';
|
|
14
14
|
import { type MultipartPart } from './multipart.js';
|
|
15
15
|
import type { CorsDecl } from './cors.js';
|
|
@@ -450,6 +450,31 @@ export type PerformHookArgs = {
|
|
|
450
450
|
blob: (key: string) => Promise<Uint8Array | null>;
|
|
451
451
|
};
|
|
452
452
|
export type PerformHook = (args: PerformHookArgs) => Promise<import('./head.js').PushOutcome>;
|
|
453
|
+
export type GitRepositoryDecl = {
|
|
454
|
+
resource: string;
|
|
455
|
+
name: string;
|
|
456
|
+
remote: string;
|
|
457
|
+
};
|
|
458
|
+
export type GitPushDecl = {
|
|
459
|
+
resource: string;
|
|
460
|
+
repository: string;
|
|
461
|
+
remote: string;
|
|
462
|
+
ref: string;
|
|
463
|
+
before: string;
|
|
464
|
+
after: string;
|
|
465
|
+
};
|
|
466
|
+
/** A template over a row's fields (`{field}`, a dotted path read through objects), every name filled or refused. */
|
|
467
|
+
export declare function fillTemplate(template: string, row: Record<string, unknown>): string;
|
|
468
|
+
/** A pack's git repository `name` in a World (the store and refs `ctx.git(name)` answers), under the twin's state and,
|
|
469
|
+
* for a namespaced account, that account's. */
|
|
470
|
+
export declare function gitRepositoryOf(m: DerivedManifest, name: string, root?: string): {
|
|
471
|
+
store: GitObjectStore;
|
|
472
|
+
refs: GitRefs;
|
|
473
|
+
};
|
|
474
|
+
/** The name a repository row's code is kept under: the manifest's `git.repository.name` (a template over `{id}` alone)
|
|
475
|
+
* filled with the row's first World id, the one an adoption rebound to the vendor's (walked back through the aliases),
|
|
476
|
+
* so the screen, a refresh, a deploy and a waiting push's protection name one repository before and after adoption. */
|
|
477
|
+
export declare function gitNameOf(m: DerivedManifest, id: string, root?: string): string;
|
|
453
478
|
export type DerivedManifest = {
|
|
454
479
|
/** Vendor data-plane hosts mapped to an already-running customer guest. */
|
|
455
480
|
machineRoutes?: ReadonlyArray<{
|
|
@@ -562,6 +587,15 @@ export type DerivedManifest = {
|
|
|
562
587
|
ingest?: IngestDecl;
|
|
563
588
|
/** the vendor's documented limits, charged by the executor on every live call (D8) */
|
|
564
589
|
rateBudget?: RateBudgetDeclaration;
|
|
590
|
+
/** where the vendor's repositories' code lives, as templates over a row's fields (`{field}`) (architecture, "The
|
|
591
|
+
* real-system adapters"; ADR 0031): the resource whose rows are the vendor's repositories, the name each is kept under
|
|
592
|
+
* (over `{id}` alone: the row's first World id, `gitNameOf`) and its remote (the vendor's absolute git URL), which a
|
|
593
|
+
* refresh fetches; and the resource a push to a World repository records, one row per ref, with the World id of the
|
|
594
|
+
* repository row it pushed to, which a deploy pushes */
|
|
595
|
+
git?: {
|
|
596
|
+
repository: GitRepositoryDecl;
|
|
597
|
+
push: GitPushDecl;
|
|
598
|
+
};
|
|
565
599
|
/** a pack whose wire has no derived perform (a socket, a line protocol, a managed database): binding a root is refused
|
|
566
600
|
* with this reason */
|
|
567
601
|
vendorBacked?: {
|
|
@@ -914,7 +948,7 @@ export type CoreScope = {
|
|
|
914
948
|
readOnly?: boolean;
|
|
915
949
|
/** `semantics/tenant.ts`: the tenant the caller acts in (the manifest's `tenant`), or undefined for none */
|
|
916
950
|
tenant?: (ctx: HandlerContext) => string | undefined | Promise<string | undefined>;
|
|
917
|
-
/** the application a walk stands in for:
|
|
951
|
+
/** the application a walk stands in for: vendor requests and declared events are answered by it (events.ts) */
|
|
918
952
|
application?: ApplicationStandIn;
|
|
919
953
|
};
|
|
920
954
|
/** World-owned generated values use the same private seed and log as pack secrets. */
|
|
@@ -937,7 +971,7 @@ type VendorNameRule = {
|
|
|
937
971
|
export declare const LANE_HEADER = "x-volter-lane";
|
|
938
972
|
/** What a coverage measurement sees of a pack's declared state logic: each move a request made and each refusal a
|
|
939
973
|
* guard gave, by the transition that decided it. Nothing is observed unless a measurement installs an observer
|
|
940
|
-
* (
|
|
974
|
+
* (`twin-standard coverage`); serving never depends on it. */
|
|
941
975
|
export type TransitionEntry = {
|
|
942
976
|
action: TwinAction;
|
|
943
977
|
field: string;
|
|
@@ -994,15 +1028,21 @@ export type SemanticsContext = {
|
|
|
994
1028
|
store: GitObjectStore;
|
|
995
1029
|
refs: GitRefs;
|
|
996
1030
|
};
|
|
1031
|
+
/** The git repository of a row of the manifest's `git.repository.resource`, under the one name it keeps through
|
|
1032
|
+
* adoption (`gitNameOf`). */
|
|
1033
|
+
gitRepository(row: Record<string, unknown>): {
|
|
1034
|
+
store: GitObjectStore;
|
|
1035
|
+
refs: GitRefs;
|
|
1036
|
+
};
|
|
997
1037
|
/** A value the World set for its application (world-env.ts): what the app was given and the vendor must agree with (a
|
|
998
1038
|
* webhook signing secret the app verifies with), never the caller's own shell; undefined when the World sets none. */
|
|
999
1039
|
worldEnv(name: string): string | undefined;
|
|
1000
1040
|
/** Runs `fn` as the vendor's own move, not the caller's: what time makes due (a renewal a read catches up) is
|
|
1001
1041
|
* recorded with no caller, wherever a handler reaches it. */
|
|
1002
1042
|
asVendor<T>(fn: () => Promise<T>): Promise<T>;
|
|
1003
|
-
/** A mail the vendor
|
|
1004
|
-
*
|
|
1005
|
-
mail(
|
|
1043
|
+
/** A mail the vendor hands on, over SMTP to the World's twin that receives each recipient's domain (smtp.ts, ADR 0032
|
|
1044
|
+
* §3): each recipient's reply, `250` delivered and `5xx` refused; a domain no twin receives is refused by the kernel. */
|
|
1045
|
+
mail(mail: Mail): Promise<MailReply[]>;
|
|
1006
1046
|
now(): unknown;
|
|
1007
1047
|
/** Render a stored row or vendor view using its resource declaration, retaining the stored subject id fallback. */
|
|
1008
1048
|
render(row: Record<string, unknown>, resource?: string): Record<string, unknown>;
|
package/dist/src/derived-core.js
CHANGED
|
@@ -61,6 +61,44 @@ export function stateOf(decl, value) {
|
|
|
61
61
|
}
|
|
62
62
|
return value === undefined || value === null ? String(decl.initial) : String(value);
|
|
63
63
|
}
|
|
64
|
+
/** A template over a row's fields (`{field}`, a dotted path read through objects), every name filled or refused. */
|
|
65
|
+
export function fillTemplate(template, row) {
|
|
66
|
+
return template.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
67
|
+
const value = name.split('.').reduce((at, k) => (at !== null && typeof at === 'object' ? at[k] : undefined), row);
|
|
68
|
+
if (value === undefined || value === null || typeof value === 'object')
|
|
69
|
+
throw new Error(`template ${template}: no value for ${name}`);
|
|
70
|
+
return String(value);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** A pack's git repository `name` in a World (the store and refs `ctx.git(name)` answers), under the twin's state and,
|
|
74
|
+
* for a namespaced account, that account's. */
|
|
75
|
+
export function gitRepositoryOf(m, name, root) {
|
|
76
|
+
if (!/^[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.-]+)*$/.test(name) || name.split('/').some((seg) => seg === '.' || seg === '..'))
|
|
77
|
+
throw new Error(`semantics: ${name} is not a git repository name`);
|
|
78
|
+
const account = accountScope(m);
|
|
79
|
+
const plane = `${worldPaths(m.service, root).dir}/git${account && account !== 'default' ? `/accounts/${encodeURIComponent(account)}` : ''}`;
|
|
80
|
+
return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
|
|
81
|
+
}
|
|
82
|
+
/** The name a repository row's code is kept under: the manifest's `git.repository.name` (a template over `{id}` alone)
|
|
83
|
+
* filled with the row's first World id, the one an adoption rebound to the vendor's (walked back through the aliases),
|
|
84
|
+
* so the screen, a refresh, a deploy and a waiting push's protection name one repository before and after adoption. */
|
|
85
|
+
export function gitNameOf(m, id, root) {
|
|
86
|
+
const decl = m.git?.repository;
|
|
87
|
+
if (!decl)
|
|
88
|
+
throw new Error(`${m.vendor}: the manifest declares no git repositories`);
|
|
89
|
+
if (decl.name.replace(/\{id\}/g, '').includes('{'))
|
|
90
|
+
throw new Error(`${m.vendor}: git.repository.name is a template over {id} alone`);
|
|
91
|
+
const type = `${storedType(m, decl.resource)}:`;
|
|
92
|
+
const aliases = aliasesNow(m.service, root);
|
|
93
|
+
let first = id;
|
|
94
|
+
for (let hops = 0; hops < 8; hops++) {
|
|
95
|
+
const from = [...aliases].find(([local, vendor]) => vendor === first && local.startsWith(type))?.[0];
|
|
96
|
+
if (from === undefined)
|
|
97
|
+
break;
|
|
98
|
+
first = from.slice(type.length);
|
|
99
|
+
}
|
|
100
|
+
return decl.name.replace(/\{id\}/g, first);
|
|
101
|
+
}
|
|
64
102
|
// ── request parsing ──────────────────────────────────────────────────────────────────────────
|
|
65
103
|
/** A body's fields as a strict vendor checks them (`ctx.fields`, the manifest's `body.strict`). Without `strict`, every
|
|
66
104
|
* field passes. An object or array field is not typed here: its own shape is the handler's. */
|
|
@@ -623,9 +661,9 @@ function mintId(m, resource, root, at, snapshot) {
|
|
|
623
661
|
* increment. One space over every snowflake resource of the vendor, as its one generator's: the instant's first, or one
|
|
624
662
|
* past the greatest the World holds when that is later (a frozen clock, or several ids in one millisecond), so ids
|
|
625
663
|
* rise with time and never repeat. */
|
|
626
|
-
/** The last snowflake minted per vendor and World
|
|
664
|
+
/** The last snowflake minted per store, vendor and World: two mints before the first is written (a message
|
|
627
665
|
* and its attachment) must not meet. */
|
|
628
|
-
const lastSnowflake = new
|
|
666
|
+
const lastSnowflake = new WeakMap();
|
|
629
667
|
function mintSnowflake(m, epoch, root, at, snapshot) {
|
|
630
668
|
const ms = BigInt(Date.parse(at) || 0);
|
|
631
669
|
const base = ms > epoch ? (ms - epoch) << 22n : 0n;
|
|
@@ -640,11 +678,13 @@ function mintSnowflake(m, epoch, root, at, snapshot) {
|
|
|
640
678
|
max = v;
|
|
641
679
|
}
|
|
642
680
|
}
|
|
681
|
+
const store = getActiveWorldStore();
|
|
682
|
+
const reservations = lastSnowflake.get(store) ?? lastSnowflake.set(store, new Map()).get(store);
|
|
643
683
|
const key = `${m.service}\0${root ?? ''}`;
|
|
644
|
-
const last =
|
|
684
|
+
const last = reservations.get(key) ?? -1n;
|
|
645
685
|
const floor = max > last ? max : last;
|
|
646
686
|
const id = base > floor ? base : floor + 1n;
|
|
647
|
-
|
|
687
|
+
reservations.set(key, id);
|
|
648
688
|
return String(id);
|
|
649
689
|
}
|
|
650
690
|
/** The next KSUID for a resource (Clerk's and Svix's ids, `ksuidFrom`): the World's instant, then the type's mint
|
|
@@ -901,6 +941,17 @@ function recordedInput(m, call, merged) {
|
|
|
901
941
|
const scopeOfCall = new WeakMap();
|
|
902
942
|
/** The intact copy contextFor already keeps for core(), before a handler can consume its request. */
|
|
903
943
|
const requestOfCall = new WeakMap();
|
|
944
|
+
/** A row of the manifest's `git.push.resource` a screen writes (a client's push to a World repository) records the push
|
|
945
|
+
* it is, which a deploy sends to the vendor as a git push (head.ts settlingEntries offers it; derived-real.ts performs
|
|
946
|
+
* it). One an operation of the surface writes (an API commit, a ref the API creates) is that operation's, which a
|
|
947
|
+
* deploy sends again. */
|
|
948
|
+
function pushedInput(m, resource, fields, input, root) {
|
|
949
|
+
const push = m.git?.push;
|
|
950
|
+
if (!push || push.resource !== resource || typeof input.door !== 'string')
|
|
951
|
+
return input;
|
|
952
|
+
const fill = (template) => fillTemplate(template, fields);
|
|
953
|
+
return { ...input, git: { name: gitNameOf(m, fill(push.repository), root), remote: fill(push.remote), ref: fill(push.ref), before: fill(push.before), after: fill(push.after) } };
|
|
954
|
+
}
|
|
904
955
|
async function writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt, checks) {
|
|
905
956
|
const before = stateBeforeWrite(m, resource, id, root);
|
|
906
957
|
const prior = counted(m, resource) ? stored(m, resource, root).find((r) => r.id === id) : undefined;
|
|
@@ -909,7 +960,7 @@ async function writeDetailed(m, call, resource, id, fields, operation, params, r
|
|
|
909
960
|
// a vendor's own move (ctx.asVendor, the clock) is the World's, as the clock's writes are: no API call a head performs
|
|
910
961
|
inVendorMove()
|
|
911
962
|
? { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { clock: true }, occurredAt, actor: { kind: 'system' } }
|
|
912
|
-
: { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: await withFiles(recordedInput(m, call, params)), occurredAt, actor: { kind: 'agent' } }, root);
|
|
963
|
+
: { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: pushedInput(m, resource, fields, await withFiles(recordedInput(m, call, params)), root), occurredAt, actor: { kind: 'agent' } }, root);
|
|
913
964
|
observeStateWrite(m, result, before, root, checks);
|
|
914
965
|
return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt, prior && { ...prior });
|
|
915
966
|
}
|
|
@@ -947,7 +998,7 @@ async function finishWrite(m, call, resource, row, result, operation, params, ro
|
|
|
947
998
|
const endpoint = (kind) => ({ ...kind, storedAs: storedType(m, kind.storedAs), ...(kind.joins ? { joins: Object.fromEntries(Object.entries(kind.joins).map(([name, join]) => [name, { ...join, storedAs: storedType(m, join.storedAs) }])) } : {}) });
|
|
948
999
|
const events = { ...m.events, ...(m.events.store ? { store: storedType(m, m.events.store) } : {}), endpoints: 'storedAs' in m.events.endpoints ? endpoint(m.events.endpoints) : m.events.endpoints.map(endpoint) };
|
|
949
1000
|
const write = { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() };
|
|
950
|
-
await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values(await context(), write, type) : undefined);
|
|
1001
|
+
await deliverEvents(m.service, events, write, async (type) => (events.render ? events.render(await context(), write, type) : body), undefined, events.values ? async (type) => events.values(await context(), write, type) : undefined, scopeOfCall.get(call)?.application);
|
|
951
1002
|
}
|
|
952
1003
|
if (m.sockets?.length)
|
|
953
1004
|
socketWrite(m.service, root, { operation, storedType: m.resources[resource]?.storedAs ?? resource, body, ...(root !== undefined ? { root } : {}), occurredAt, request: request.clone() });
|
|
@@ -1572,13 +1623,9 @@ export async function contextFor(m, call, scope) {
|
|
|
1572
1623
|
ask: (url, init, within) => askApplication(url, init, within, scope.application),
|
|
1573
1624
|
worldEnv: (name) => worldEnvValue(name),
|
|
1574
1625
|
asVendor: (fn) => runAsVendorMove(fn),
|
|
1575
|
-
git: (name) =>
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
const plane = `${worldPaths(m.service, root).dir}/git${accountScope(m) && accountScope(m) !== 'default' ? `/accounts/${encodeURIComponent(accountScope(m))}` : ''}`;
|
|
1579
|
-
return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
|
|
1580
|
-
},
|
|
1581
|
-
mail: (route, mail, headers) => sendMail(route, mail, headers),
|
|
1626
|
+
git: (name) => gitRepositoryOf(m, name, root),
|
|
1627
|
+
gitRepository: (row) => gitRepositoryOf(m, gitNameOf(m, String(row.id), root), root),
|
|
1628
|
+
mail: (mail) => sendMail(mail),
|
|
1582
1629
|
now: () => now(m, at),
|
|
1583
1630
|
render: (row, resource) => {
|
|
1584
1631
|
const raw = rawResources.has(row);
|
package/dist/src/derived-real.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { parseExactJson, exactInteger, compareExactNumbers } from "./exact-json.js";
|
|
2
2
|
import { aliasesFrom, parentEntries, readParentTreeMap, readTree, wholeLog } from "./log.js";
|
|
3
|
-
import { storedType } from "./derived-core.js";
|
|
3
|
+
import { fillTemplate, gitNameOf, gitRepositoryOf, storedType } from "./derived-core.js";
|
|
4
4
|
import { verifyEvent } from "./events.js";
|
|
5
5
|
import { hashFieldValue } from "./hash.js";
|
|
6
6
|
import { readResourceBlob, writeResourceBlob } from "./resource-blob.js";
|
|
7
|
-
import { RefusedWriteError } from "./head.js";
|
|
7
|
+
import { RefusedWriteError, settlingEntries } from "./head.js";
|
|
8
|
+
import { ZERO_SHA, fetchPack, isAncestor, lsRemote, sendPack } from "./git/index.js";
|
|
8
9
|
import { observeResources } from "./observe.js";
|
|
9
10
|
import { readRoot } from "./state-system.js";
|
|
10
11
|
/** A JSON path's value: `a.b`, `a[0].b`, `$body.a` (the leading `$body.` read as the root). */
|
|
@@ -490,6 +491,8 @@ function performOf(units) {
|
|
|
490
491
|
return async (execute, group, ctx) => {
|
|
491
492
|
await adoptAccount(units, execute, ctx);
|
|
492
493
|
const input = (group[0].input ?? {});
|
|
494
|
+
if (object(input.git))
|
|
495
|
+
return pushOf(units, execute, group, ctx);
|
|
493
496
|
const unit = unitOf(units, input);
|
|
494
497
|
// a protocol one request cannot express is the pack's hook, which composes the derived perform ("Protocols a
|
|
495
498
|
// request cannot express")
|
|
@@ -505,6 +508,133 @@ function performOf(units) {
|
|
|
505
508
|
});
|
|
506
509
|
};
|
|
507
510
|
}
|
|
511
|
+
const pushInputOf = (e) => object(e.input?.git);
|
|
512
|
+
/** A 4xx other than 408 and 429 from the vendor's git: a refusal, as a REST answer's is ("Perform", 3). */
|
|
513
|
+
class GitAnswer extends Error {
|
|
514
|
+
status;
|
|
515
|
+
constructor(status, message) {
|
|
516
|
+
super(message);
|
|
517
|
+
this.status = status;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
/** Git smart HTTP to a remote (the vendor's absolute git URL) over the root's executor, which applies the credential
|
|
521
|
+
* (HostRule.basic) and charges the vendor's budget. */
|
|
522
|
+
function gitTransport(execute, remote) {
|
|
523
|
+
return async ({ service, body }) => {
|
|
524
|
+
const answer = await execute({
|
|
525
|
+
method: body === undefined ? 'GET' : 'POST',
|
|
526
|
+
path: body === undefined ? `${remote}/info/refs?service=${service}` : `${remote}/${service}`,
|
|
527
|
+
headers: {
|
|
528
|
+
'user-agent': 'git/2.0 (volter-twin)',
|
|
529
|
+
accept: body === undefined ? `application/x-${service}-advertisement` : `application/x-${service}-result`,
|
|
530
|
+
...(body === undefined ? {} : { 'content-type': `application/x-${service}-request` }),
|
|
531
|
+
},
|
|
532
|
+
...(body === undefined ? {} : { body }),
|
|
533
|
+
responseType: 'bytes',
|
|
534
|
+
});
|
|
535
|
+
if (answer.status !== 200)
|
|
536
|
+
throw new GitAnswer(answer.status, `${answer.status}: git ${service} at ${remote}${answer.body ? `: ${answer.body.slice(0, 300)}` : ''}`);
|
|
537
|
+
return answer.bodyBytes ?? new TextEncoder().encode(answer.body);
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
/** DEPLOY a receive-pack's group (its push rows, one per ref): each ref's latest target decided on its own, the rest
|
|
541
|
+
* sent in one push against the root's refs (atomic when the root offers it), never forced. Each entry settles as its
|
|
542
|
+
* ref did (`PushOutcome.refused`); every ref refused refuses the group. */
|
|
543
|
+
async function pushOf(units, execute, group, ctx) {
|
|
544
|
+
const first = group[0];
|
|
545
|
+
const m = units.find((u) => u.manifest.git)?.manifest;
|
|
546
|
+
const pushes = group.map((e) => ({ entry: e, push: pushInputOf(e) }));
|
|
547
|
+
const refuseAll = (reason) => { throw new RefusedWriteError('vendor', reason, first.id); };
|
|
548
|
+
if (!m || pushes.some((p) => p.push === undefined))
|
|
549
|
+
refuseAll('the push names no repository of the vendor\'s');
|
|
550
|
+
const { name, remote } = pushes[0].push;
|
|
551
|
+
if (pushes.some((p) => p.push.name !== name || p.push.remote !== remote))
|
|
552
|
+
refuseAll('one push names one repository');
|
|
553
|
+
const { store } = gitRepositoryOf(m, name, ctx.root);
|
|
554
|
+
const transport = gitTransport(execute, remote);
|
|
555
|
+
const answered = (work) => work.catch((error) => {
|
|
556
|
+
if (error instanceof GitAnswer && error.status >= 400 && error.status < 500 && error.status !== 408 && error.status !== 429)
|
|
557
|
+
refuseAll(error.message);
|
|
558
|
+
throw error;
|
|
559
|
+
});
|
|
560
|
+
const root = await answered(lsRemote(transport, 'git-receive-pack'));
|
|
561
|
+
// each ref's latest target in the group, and why the root may not take it
|
|
562
|
+
const targets = new Map();
|
|
563
|
+
for (const p of pushes)
|
|
564
|
+
targets.set(p.push.ref, p.push.after);
|
|
565
|
+
const refused = new Map();
|
|
566
|
+
const commands = [];
|
|
567
|
+
for (const [ref, target] of targets) {
|
|
568
|
+
const old = root.refs.get(ref) ?? ZERO_SHA;
|
|
569
|
+
if (old === target)
|
|
570
|
+
continue;
|
|
571
|
+
if (target !== ZERO_SHA && old !== ZERO_SHA) {
|
|
572
|
+
if (ref.startsWith('refs/tags/')) {
|
|
573
|
+
refused.set(ref, 'the tag already exists on the root');
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
if (!(await store.has(old))) {
|
|
577
|
+
refused.set(ref, 'the root holds work this World has not fetched (fetch first)');
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (!(await isAncestor(store, old, target))) {
|
|
581
|
+
refused.set(ref, `not a fast-forward of the root's ${old.slice(0, 7)}`);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
commands.push({ name: ref, old, new: target });
|
|
586
|
+
}
|
|
587
|
+
if (commands.length) {
|
|
588
|
+
const report = await answered(sendPack(transport, store, commands, [...new Set(root.refs.values())], root.caps.includes('atomic')));
|
|
589
|
+
for (const c of commands) {
|
|
590
|
+
const reason = report.get(c.name);
|
|
591
|
+
if (reason !== null)
|
|
592
|
+
refused.set(c.name, `the root refused it: ${reason}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const took = pushes.filter((p) => !refused.has(p.push.ref));
|
|
596
|
+
if (!took.length)
|
|
597
|
+
refuseAll([...refused].map(([ref, reason]) => `${ref}: ${reason}`).join('; '));
|
|
598
|
+
return {
|
|
599
|
+
externalId: '', actionId: took[0].entry.id,
|
|
600
|
+
...(refused.size ? { refused: pushes.filter((p) => refused.has(p.push.ref)).map((p) => ({ actionId: p.entry.id, reason: `${p.push.ref}: ${refused.get(p.push.ref)}` })) } : {}),
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/** REFRESH a repository's code, read: the root's advertisement and the objects of its branches and tags the World's
|
|
604
|
+
* repository lacks (immutable: kept as they arrive). Its refs are staged, published by `publishMirror` once every read of
|
|
605
|
+
* the refresh succeeded. */
|
|
606
|
+
async function readMirror(m, execute, root, name, remote) {
|
|
607
|
+
const { store, refs } = gitRepositoryOf(m, name, root);
|
|
608
|
+
const transport = gitTransport(execute, remote);
|
|
609
|
+
const advertised = await lsRemote(transport, 'git-upload-pack');
|
|
610
|
+
const theirs = new Map([...advertised.refs].filter(([ref]) => mirrored(ref)));
|
|
611
|
+
const held = new Map(refs.list().map((r) => [r.name, r.sha]));
|
|
612
|
+
const wants = [];
|
|
613
|
+
for (const sha of new Set(theirs.values()))
|
|
614
|
+
if (!(await store.has(sha)))
|
|
615
|
+
wants.push(sha);
|
|
616
|
+
await fetchPack(transport, store, wants, [...new Set(held.values())]);
|
|
617
|
+
return { m, name, theirs, head: advertised.head, held, heldHead: refs.head() };
|
|
618
|
+
}
|
|
619
|
+
const mirrored = (ref) => ref.startsWith('refs/heads/') || ref.startsWith('refs/tags/');
|
|
620
|
+
/** REFRESH a repository's code, published: by compare-and-set under the repository's ref lock, the lock receive-pack
|
|
621
|
+
* holds. A ref takes the root's value, or is deleted when the root holds it no more, only while it still holds what the
|
|
622
|
+
* refresh read and no offered push names it (read inside the lock); HEAD follows the root's only while unchanged. */
|
|
623
|
+
function publishMirror(service, root, staged) {
|
|
624
|
+
const { refs } = gitRepositoryOf(staged.m, staged.name, root);
|
|
625
|
+
refs.withLock(() => {
|
|
626
|
+
const waiting = new Set(settlingEntries(service, root).offered.map(pushInputOf).filter((p) => p?.name === staged.name).map((p) => p.ref));
|
|
627
|
+
const untouched = (ref) => !waiting.has(ref) && (refs.get(ref) ?? undefined) === staged.held.get(ref);
|
|
628
|
+
for (const [ref, sha] of staged.theirs)
|
|
629
|
+
if (untouched(ref))
|
|
630
|
+
refs.set(ref, sha);
|
|
631
|
+
for (const ref of staged.held.keys())
|
|
632
|
+
if (mirrored(ref) && !staged.theirs.has(ref) && untouched(ref))
|
|
633
|
+
refs.delete(ref);
|
|
634
|
+
if (staged.head?.startsWith('refs/heads/') && refs.head() === staged.heldHead)
|
|
635
|
+
refs.setHead(staged.head);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
508
638
|
/** Path parameters that name a subject by a field rather than its id, sent as the vendor's value of that field, read
|
|
509
639
|
* from the adopted landed copy ("Perform", step 1): a parent found by `parent.where` (a repository addressed as
|
|
510
640
|
* `{owner}/{repo}`), and a subject named by the number it counts per parent (`number`, an issue's `issue_number`).
|
|
@@ -682,10 +812,10 @@ function refreshOf(units, service) {
|
|
|
682
812
|
const sent = asked.length ? `${target}${target.includes('?') ? '&' : '?'}${new URLSearchParams(asked)}` : target;
|
|
683
813
|
const answer = await execute({ method: op.method.toUpperCase(), path: sentTo(m, sent, subject), ...onLane, headers: fixedHeaders(op) });
|
|
684
814
|
// a door's subject the vendor does not show the root (another bot's private chat) stays as the World holds it
|
|
685
|
-
if (!rooted.has(key) && answer.status >= 400 && answer.status !== 429)
|
|
686
|
-
continue;
|
|
687
815
|
if (answer.status === 401)
|
|
688
816
|
throw new Error(`401: the vendor refused the root's credential (${op.method.toUpperCase()} ${path}); nothing folded`);
|
|
817
|
+
if (!rooted.has(key) && answer.status >= 400 && answer.status !== 429)
|
|
818
|
+
continue;
|
|
689
819
|
if (answer.status === 403) {
|
|
690
820
|
unread.push({ type, path, status: 403 });
|
|
691
821
|
refused = true;
|
|
@@ -700,7 +830,19 @@ function refreshOf(units, service) {
|
|
|
700
830
|
continue;
|
|
701
831
|
if (answer.status >= 400 || refusedIn(m, parse(answer)))
|
|
702
832
|
throw new Error(`${m.vendor}: ${resource} refresh ${op.id} (${op.method.toUpperCase()} ${op.path}): ${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
703
|
-
const
|
|
833
|
+
const parsed = parse(answer);
|
|
834
|
+
let full;
|
|
835
|
+
if (op.answers?.list) {
|
|
836
|
+
const items = itemsOf(op, parsed);
|
|
837
|
+
const matching = items.find((item) => (scopeIdOf(scope, item) ?? storedIdOf(decl, item)) === vendorId);
|
|
838
|
+
if (!matching && items.length)
|
|
839
|
+
throw new Error(`${m.vendor}: ${resource} refresh ${op.id} omitted the requested subject; nothing folded`);
|
|
840
|
+
if (!matching)
|
|
841
|
+
continue;
|
|
842
|
+
full = matching;
|
|
843
|
+
}
|
|
844
|
+
else
|
|
845
|
+
full = object(op.answers?.key ? at(parsed, op.answers.key) : parsed) ?? {};
|
|
704
846
|
const local = m.worldFields?.[resource] ?? {};
|
|
705
847
|
const fields = Object.fromEntries(Object.entries(full).filter(([k]) => !(k in local)));
|
|
706
848
|
if (decl.parent) {
|
|
@@ -1026,8 +1168,20 @@ function refreshOf(units, service) {
|
|
|
1026
1168
|
}
|
|
1027
1169
|
}
|
|
1028
1170
|
}
|
|
1171
|
+
// the code of each repository observed, read over git; its refs are published with the blobs, once every read succeeded
|
|
1172
|
+
const mirrors = [];
|
|
1173
|
+
for (const { manifest: m } of units) {
|
|
1174
|
+
if (!m.git)
|
|
1175
|
+
continue;
|
|
1176
|
+
const { resource, remote } = m.git.repository;
|
|
1177
|
+
for (const row of observed.filter((o) => o.type === storedType(m, resource) && o.deleted !== true)) {
|
|
1178
|
+
mirrors.push(await readMirror(m, execute, opts.root, gitNameOf(m, row.id, opts.root), fillTemplate(remote, { ...row.fields, id: row.id })));
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1029
1181
|
for (const download of downloads)
|
|
1030
1182
|
await writeResourceBlob(service, download.key, download.bytes, opts.root);
|
|
1183
|
+
for (const staged of mirrors)
|
|
1184
|
+
publishMirror(service, opts.root, staged);
|
|
1031
1185
|
adopt(service, opts.root, units, observed);
|
|
1032
1186
|
const report = observeResources(service, observed, { ...(opts.root !== undefined ? { root: opts.root } : {}), complete });
|
|
1033
1187
|
return unread.length ? { ...report, unread } : report;
|
package/dist/src/derived.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type DerivedOperation = {
|
|
|
13
13
|
headers?: Record<string, string | null>;
|
|
14
14
|
/** the status the vendor answers success with (201 for a create, 204 for an empty answer) */
|
|
15
15
|
successStatus?: number;
|
|
16
|
-
/** how the spec says the request body is encoded (
|
|
16
|
+
/** how the spec says the request body is encoded (the derivation: json, form, multipart, none) */
|
|
17
17
|
bodyEncoding?: string;
|
|
18
18
|
/** the success is documented with no content (a setting's update answered 200 and nothing else) */
|
|
19
19
|
emptySuccess?: boolean;
|
package/dist/src/events.d.ts
CHANGED
|
@@ -164,6 +164,8 @@ export declare function eventTypeOf(decl: Pick<EventsDecl, 'types' | 'known'>, o
|
|
|
164
164
|
/** Whether an endpoint's list of event types takes a type: every type when the list is empty, `*` or `<family>.*`
|
|
165
165
|
* its patterns, else the type itself. */
|
|
166
166
|
export declare function takesEvent(list: unknown, type: string): boolean;
|
|
167
|
+
/** Await already-scheduled first dispatches and consume their retained persistence failures. */
|
|
168
|
+
export declare function settleEventDeliveries(root: string): Promise<void>;
|
|
167
169
|
/** Who watches signed packets before dispatch (a walk reading each as its receiver would). This is not evidence of
|
|
168
170
|
* transport or acceptance. Set and restored by the walk, as its World store is; unset, nothing watches. */
|
|
169
171
|
export type DeliveryObserver = (delivery: EventDelivery & {
|
|
@@ -179,13 +181,14 @@ export type ApplicationAnswer = {
|
|
|
179
181
|
body: string;
|
|
180
182
|
missed?: 'timeout' | 'unreachable';
|
|
181
183
|
};
|
|
182
|
-
/** Who answers
|
|
184
|
+
/** Who answers a vendor's requests to the application before the World's application route does, given to a
|
|
183
185
|
* pack's fetch (`PackFetchOptions.application`) and belonging to that fetch alone: an answer it gives is the
|
|
184
186
|
* application's; `'unreachable'` is a question nobody answers (a walk's life, a sealed World, takes no other); undefined
|
|
185
187
|
* is a question it does not take, which goes on as any other does: the application route, the World's egress rule, the
|
|
186
188
|
* network. A walk's stand-in answers every question (its life's answers, else unreachable); a hosted World's router
|
|
187
189
|
* answers only what it can answer truthfully (WorldDoors.askInWorld) and leaves the rest. `bodyError` models a
|
|
188
|
-
* disconnect after the response headers; a caller needing the complete body receives no complete answer
|
|
190
|
+
* disconnect after the response headers; a caller needing the complete body receives no complete answer, while an
|
|
191
|
+
* event dispatch acknowledges the response status without reading its body. */
|
|
189
192
|
export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{
|
|
190
193
|
status: number;
|
|
191
194
|
headers?: Record<string, string>;
|
|
@@ -198,4 +201,4 @@ export type ApplicationStandIn = (url: string, init: RequestInit) => Promise<{
|
|
|
198
201
|
export declare function askApplication(url: string, init: RequestInit, within: number, standIn?: ApplicationStandIn): Promise<ApplicationAnswer>;
|
|
199
202
|
/** Every event a write sends, rendered, stored when the vendor keeps its events, signed, delivered to each endpoint
|
|
200
203
|
* that takes it, and recorded. */
|
|
201
|
-
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
|
|
204
|
+
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>>, application?: ApplicationStandIn): Promise<EventDelivery[]>;
|