@volter/world-core 3.0.164 → 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.
Files changed (55) hide show
  1. package/dist/generated/pack-facts.json +44 -3660
  2. package/dist/pack-facts.cjs +4 -4
  3. package/dist/src/browser.d.ts +4 -0
  4. package/dist/src/browser.js +10 -0
  5. package/dist/src/derived-core.d.ts +45 -5
  6. package/dist/src/derived-core.js +53 -8
  7. package/dist/src/derived-real.js +144 -2
  8. package/dist/src/derived.d.ts +1 -1
  9. package/dist/src/executor.d.ts +2 -2
  10. package/dist/src/executor.js +13 -0
  11. package/dist/src/git/index.d.ts +1 -0
  12. package/dist/src/git/index.js +1 -0
  13. package/dist/src/git/smart-http-client.d.ts +28 -0
  14. package/dist/src/git/smart-http-client.js +95 -0
  15. package/dist/src/git/smart-http.d.ts +3 -0
  16. package/dist/src/git/smart-http.js +9 -0
  17. package/dist/src/head.d.ts +16 -1
  18. package/dist/src/head.js +52 -7
  19. package/dist/src/html.d.ts +29 -0
  20. package/dist/src/html.js +64 -0
  21. package/dist/src/index.d.ts +3 -1
  22. package/dist/src/index.js +3 -0
  23. package/dist/src/pack-fetch.js +41 -2
  24. package/dist/src/pack-paths.d.ts +2 -3
  25. package/dist/src/pack-paths.js +3 -13
  26. package/dist/src/packRegistry.d.ts +17 -1
  27. package/dist/src/runtime.d.ts +2 -0
  28. package/dist/src/runtime.js +3 -0
  29. package/dist/src/serve-http.d.ts +1 -1
  30. package/dist/src/serve-http.js +11 -2
  31. package/dist/src/smtp-server.d.ts +78 -0
  32. package/dist/src/smtp-server.js +160 -0
  33. package/dist/src/smtp.d.ts +29 -11
  34. package/dist/src/smtp.js +221 -52
  35. package/generated/pack-facts.json +44 -3660
  36. package/pack-facts.cjs +4 -4
  37. package/package.json +4 -2
  38. package/src/browser.ts +10 -0
  39. package/src/derived-core.ts +68 -12
  40. package/src/derived-real.ts +121 -2
  41. package/src/derived.ts +2 -2
  42. package/src/executor.ts +13 -2
  43. package/src/git/index.ts +1 -0
  44. package/src/git/smart-http-client.ts +99 -0
  45. package/src/git/smart-http.ts +7 -0
  46. package/src/head.ts +37 -6
  47. package/src/html.ts +72 -0
  48. package/src/index.ts +4 -1
  49. package/src/pack-fetch.ts +35 -2
  50. package/src/pack-paths.ts +3 -11
  51. package/src/packRegistry.ts +9 -1
  52. package/src/runtime.ts +3 -0
  53. package/src/serve-http.ts +11 -3
  54. package/src/smtp-server.ts +166 -0
  55. package/src/smtp.ts +199 -53
@@ -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
- // this kernel was built with (generated/pack-facts.json, compiled from the packs of its checkout), overlaid by the facts
3
- // of the packs the World resolved from installed packages. Each released pack carries its own
4
- // (`generated/pack-facts.json`), and the World's instance file (VOLTER_WORLD_INSTANCE, which every World process already
5
- // has) names them as `packFacts`. A pack's own facts replace the built-in ones for its vendor. Plain CommonJS, no
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');
@@ -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>;
@@ -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 SmtpRoute } from './smtp.js';
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?: {
@@ -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
- * (scripts/life-coverage.ts); serving never depends on it. */
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 sends a person, over SMTP to the route its settings name (smtp.ts), under the World's egress rule;
1004
- * throws when it cannot be delivered. */
1005
- mail(route: SmtpRoute, mail: Mail, headers?: Record<string, string>): Promise<void>;
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>;
@@ -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. */
@@ -903,6 +941,17 @@ function recordedInput(m, call, merged) {
903
941
  const scopeOfCall = new WeakMap();
904
942
  /** The intact copy contextFor already keeps for core(), before a handler can consume its request. */
905
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
+ }
906
955
  async function writeDetailed(m, call, resource, id, fields, operation, params, root, occurredAt, checks) {
907
956
  const before = stateBeforeWrite(m, resource, id, root);
908
957
  const prior = counted(m, resource) ? stored(m, resource, root).find((r) => r.id === id) : undefined;
@@ -911,7 +960,7 @@ async function writeDetailed(m, call, resource, id, fields, operation, params, r
911
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
912
961
  inVendorMove()
913
962
  ? { operation, subjectType: storedType(m, resource), subjectId: id, fields, input: { clock: true }, occurredAt, actor: { kind: 'system' } }
914
- : { 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);
915
964
  observeStateWrite(m, result, before, root, checks);
916
965
  return finishWrite(m, call, resource, row, result, operation, params, root, occurredAt, prior && { ...prior });
917
966
  }
@@ -1574,13 +1623,9 @@ export async function contextFor(m, call, scope) {
1574
1623
  ask: (url, init, within) => askApplication(url, init, within, scope.application),
1575
1624
  worldEnv: (name) => worldEnvValue(name),
1576
1625
  asVendor: (fn) => runAsVendorMove(fn),
1577
- git: (name) => {
1578
- if (!/^[A-Za-z0-9_.-]+(\/[A-Za-z0-9_.-]+)*$/.test(name) || name.split('/').some((seg) => seg === '.' || seg === '..'))
1579
- throw new Error(`semantics: ${name} is not a git repository name`);
1580
- const plane = `${worldPaths(m.service, root).dir}/git${accountScope(m) && accountScope(m) !== 'default' ? `/accounts/${encodeURIComponent(accountScope(m))}` : ''}`;
1581
- return { store: new GitObjectStore(getActiveBlobStore(), `${plane}/repos/${name}`), refs: new GitRefs(getActiveWorldStore(), `${plane}/repos/${name}`) };
1582
- },
1583
- 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),
1584
1629
  now: () => now(m, at),
1585
1630
  render: (row, resource) => {
1586
1631
  const raw = rawResources.has(row);
@@ -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`).
@@ -1038,8 +1168,20 @@ function refreshOf(units, service) {
1038
1168
  }
1039
1169
  }
1040
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
+ }
1041
1181
  for (const download of downloads)
1042
1182
  await writeResourceBlob(service, download.key, download.bytes, opts.root);
1183
+ for (const staged of mirrors)
1184
+ publishMirror(service, opts.root, staged);
1043
1185
  adopt(service, opts.root, units, observed);
1044
1186
  const report = observeResources(service, observed, { ...(opts.root !== undefined ? { root: opts.root } : {}), complete });
1045
1187
  return unread.length ? { ...report, unread } : report;
@@ -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 (derive-pack: json, form, multipart, none) */
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;
@@ -132,8 +132,8 @@ export declare function validateRemoteOrigin(origin: string, opts?: {
132
132
  /** `vendorHosts`: the pack's declared vendor host rules (its descriptor's `hosts`). An absolute https URL
133
133
  * on one of them — an exact `host` rule whose `pathPattern` the path matches — is a second host of the
134
134
  * same vendor that takes the credential (LinkedIn's image uploads on www.linkedin.com/dms-uploads,
135
- * Instagram's rupload host); it is sent credentialed like an anchored call. Any other absolute URL is
136
- * refused, as before. */
135
+ * Instagram's rupload host); it is sent credentialed like an anchored call, as HTTP Basic when a rule naming
136
+ * it names a `basic` user. Any other absolute URL is refused. */
137
137
  /** Where a credential is kept (the head's and refresh's root: head.ts rootCustody): `key` names the credential stably
138
138
  * across rotations and anew when another is sealed in its place (the token cache is keyed by it, not by the secret's value), `open` reads it as sealed now, `seal` keeps a rotated one. */
139
139
  /** `unsealed`: where a rotation the seal failed to keep is recorded durably (the field and the cause, never a value), since
@@ -224,6 +224,8 @@ function direct(origin, credential, auth, vendorHosts, now = () => new Date()) {
224
224
  let target;
225
225
  // the vendor's own URL, where a loopback root stands in for it: what a signature is made over
226
226
  let vendorUrl;
227
+ // the user a host that takes the token as HTTP Basic's password names it under (HostRule.basic)
228
+ let basicUser;
227
229
  if (/^https:\/\//i.test(path) && vendorHosts?.length) {
228
230
  // a second host the pack declares for this vendor, on a path its rule names: validated as the root is
229
231
  target = new URL(path);
@@ -235,6 +237,7 @@ function direct(origin, credential, auth, vendorHosts, now = () => new Date()) {
235
237
  const declared = vendorHosts.some((r) => !r.exclude && named(r)) && !vendorHosts.some((r) => r.exclude && named(r));
236
238
  if (!declared)
237
239
  throw new Error(`remote execute: ${target.host}${target.pathname.slice(0, 64)} is not a host this vendor's pack declares for this path`);
240
+ basicUser = vendorHosts.find((r) => !r.exclude && r.basic !== undefined && named(r))?.basic;
238
241
  // A loopback root is the vendor substitute. Preserve the vendor Host while retaining
239
242
  // the already-validated root's mounted path; an absolute URL never escapes that root.
240
243
  if (isLoopbackHost(origin.hostname)) {
@@ -258,6 +261,16 @@ function direct(origin, credential, auth, vendorHosts, now = () => new Date()) {
258
261
  else
259
262
  for (const [name, value] of Object.entries(credential.headers))
260
263
  outbound.set(name, value);
264
+ // A HOST THAT TAKES THE TOKEN AS HTTP BASIC: the sealed authorization header's credentials, after its scheme, are
265
+ // Basic's password under the rule's user; one sealed as Basic goes as it is, and none sealed refuses before egress
266
+ if (issued === undefined && basicUser !== undefined) {
267
+ const sealed = outbound.get('authorization') ?? '';
268
+ const token = /^\S+\s+(\S.*)$/.exec(sealed)?.[1];
269
+ if (token === undefined)
270
+ throw new Error(`remote execute: ${target.host} takes the credential as HTTP Basic, and the sealed credential carries no authorization header`);
271
+ if (!/^basic\s/i.test(sealed))
272
+ outbound.set('authorization', `Basic ${btoa(`${basicUser}:${token}`)}`);
273
+ }
261
274
  // the vendor's host, kept over any the credential carries (a loopback twin routes by it, as the World's proxy's)
262
275
  if (vendorUrl) {
263
276
  outbound.set('host', vendorUrl.host);
@@ -6,3 +6,4 @@ export * from './smart-http.js';
6
6
  export * from './history.js';
7
7
  export * from './lfs.js';
8
8
  export * from './format.js';
9
+ export * from './smart-http-client.js';
@@ -6,3 +6,4 @@ export * from "./smart-http.js";
6
6
  export * from "./history.js";
7
7
  export * from "./lfs.js";
8
8
  export * from "./format.js";
9
+ export * from "./smart-http-client.js";
@@ -0,0 +1,28 @@
1
+ import { type GitObjectStore } from './objects.js';
2
+ import { type Service } from './smart-http.js';
3
+ /** One exchange: `GET info/refs?service=<service>` when there is no body, `POST <service>` when there is. It answers
4
+ * the response's bytes, and throws when the server did not answer 200. */
5
+ export type SmartHttpTransport = (request: {
6
+ service: Service;
7
+ body?: Uint8Array;
8
+ }) => Promise<Uint8Array>;
9
+ /** What a server advertises: its refs (peeled `^{}` lines dropped), the ref its HEAD names, and its capabilities. */
10
+ export type RemoteRefs = {
11
+ refs: Map<string, string>;
12
+ head: string | null;
13
+ caps: string[];
14
+ };
15
+ /** `GET info/refs?service=<service>`, read. */
16
+ export declare function lsRemote(transport: SmartHttpTransport, service: Service): Promise<RemoteRefs>;
17
+ /** `POST git-upload-pack` with `done`: every object `wants` reach and `haves` do not, read into `store`. */
18
+ export declare function fetchPack(transport: SmartHttpTransport, store: GitObjectStore, wants: string[], haves: string[]): Promise<void>;
19
+ /** One ref update a push asks for: `old` is the server's value as advertised (ZERO_SHA to create), `new` the object
20
+ * to point it at (ZERO_SHA to delete). */
21
+ export type PushCommand = {
22
+ name: string;
23
+ old: string;
24
+ new: string;
25
+ };
26
+ /** `POST git-receive-pack`: the commands and the pack of what they need beyond `haves`, atomic when `atomic` (the
27
+ * server offered it). Answers each command's outcome by ref name: null when the server took it, else its reason. */
28
+ export declare function sendPack(transport: SmartHttpTransport, store: GitObjectStore, commands: PushCommand[], haves: string[], atomic?: boolean): Promise<Map<string, string | null>>;
@@ -0,0 +1,95 @@
1
+ // THE CLIENT HALF OF SMART HTTP (gitprotocol-http, protocol v0): what `git fetch` and `git push` send to a server,
2
+ // over a transport the caller supplies. The library opens no socket, holds no credential and names no host; a root's
3
+ // refresh and deploy reach the vendor's git through it over the kernel executor (derived-real.ts, ADR 0031). It asks
4
+ // only for what it reads: no side-band (the pack arrives bare), no multi_ack (one answer), no thin pack (every delta
5
+ // base is in the pack or the store), ofs-delta (readPack resolves both delta kinds), and on a push report-status, with
6
+ // atomic when the server offers it.
7
+ import { concat } from "./objects.js";
8
+ import { readPack } from "./pack.js";
9
+ import { FLUSH, ZERO_SHA, packObjects, pktLine, readPktLines } from "./smart-http.js";
10
+ const DEC = new TextDecoder();
11
+ const SHA = /^[0-9a-f]{40}$/;
12
+ /** `GET info/refs?service=<service>`, read. */
13
+ export async function lsRemote(transport, service) {
14
+ let { lines, rest } = readPktLines(await transport({ service }));
15
+ if (lines[0]?.startsWith('# service='))
16
+ ({ lines, rest } = readPktLines(rest));
17
+ const refs = new Map();
18
+ let head = null;
19
+ let caps = [];
20
+ for (const raw of lines) {
21
+ const [line, offered] = raw.replace(/\n$/, '').split('\0');
22
+ if (line.startsWith('ERR '))
23
+ throw new Error(`git ${service}: ${line.slice(4)}`);
24
+ if (offered !== undefined) {
25
+ caps = offered.split(' ').filter(Boolean);
26
+ const symref = caps.find((c) => c.startsWith('symref=HEAD:'));
27
+ if (symref)
28
+ head = symref.slice('symref=HEAD:'.length);
29
+ }
30
+ const sha = line.slice(0, 40);
31
+ const name = line.slice(41);
32
+ if (!SHA.test(sha) || sha === ZERO_SHA || name === 'HEAD' || name.endsWith('^{}'))
33
+ continue;
34
+ refs.set(name, sha);
35
+ }
36
+ return { refs, head, caps };
37
+ }
38
+ const isPack = (bytes, at) => DEC.decode(bytes.subarray(at, at + 4)) === 'PACK';
39
+ /** `POST git-upload-pack` with `done`: every object `wants` reach and `haves` do not, read into `store`. */
40
+ export async function fetchPack(transport, store, wants, haves) {
41
+ if (wants.length === 0)
42
+ return;
43
+ const answer = await transport({
44
+ service: 'git-upload-pack',
45
+ body: concat(pktLine(`want ${wants[0]} ofs-delta no-progress agent=volter-twin\n`), ...wants.slice(1).map((w) => pktLine(`want ${w}\n`)), FLUSH, ...haves.map((h) => pktLine(`have ${h}\n`)), pktLine('done\n')),
46
+ });
47
+ // the negotiation's ACK / NAK lines, then the pack
48
+ let at = 0;
49
+ while (at + 4 <= answer.length && !isPack(answer, at)) {
50
+ const len = parseInt(DEC.decode(answer.subarray(at, at + 4)), 16);
51
+ if (Number.isNaN(len))
52
+ break;
53
+ if (len < 4) {
54
+ at += 4;
55
+ continue;
56
+ }
57
+ const line = DEC.decode(answer.subarray(at + 4, at + len));
58
+ if (line.startsWith('ERR '))
59
+ throw new Error(`git upload-pack: ${line.slice(4).trim()}`);
60
+ at += len;
61
+ }
62
+ if (!isPack(answer, at))
63
+ throw new Error('git upload-pack: the server answered no pack');
64
+ await readPack(answer.subarray(at), store);
65
+ }
66
+ /** `POST git-receive-pack`: the commands and the pack of what they need beyond `haves`, atomic when `atomic` (the
67
+ * server offered it). Answers each command's outcome by ref name: null when the server took it, else its reason. */
68
+ export async function sendPack(transport, store, commands, haves, atomic = false) {
69
+ const updates = commands.filter((c) => c.new !== ZERO_SHA).map((c) => c.new);
70
+ const caps = `report-status${atomic ? ' atomic' : ''} agent=volter-twin`;
71
+ const answer = await transport({
72
+ service: 'git-receive-pack',
73
+ body: concat(...commands.map((c, i) => pktLine(`${c.old} ${c.new} ${c.name}${i === 0 ? `\0${caps}` : ''}\n`)), FLUSH,
74
+ // a push of deletions alone sends no pack (gitprotocol-pack, "Pushing data to a server")
75
+ ...(updates.length ? [await packObjects(store, updates, haves)] : [])),
76
+ });
77
+ const report = readPktLines(answer).lines.map((l) => l.replace(/\n$/, ''));
78
+ const failure = report.find((l) => l.startsWith('ERR '));
79
+ if (failure)
80
+ throw new Error(`git receive-pack: ${failure.slice(4)}`);
81
+ const unpack = report.find((l) => l.startsWith('unpack '))?.slice('unpack '.length);
82
+ if (unpack === undefined)
83
+ throw new Error('git receive-pack: the server sent no report');
84
+ const out = new Map(commands.map((c) => [c.name, unpack === 'ok' ? 'the server reported nothing for it' : `unpacker error: ${unpack}`]));
85
+ for (const l of report) {
86
+ if (l.startsWith('ok '))
87
+ out.set(l.slice(3), null);
88
+ else if (l.startsWith('ng ')) {
89
+ const rest = l.slice(3);
90
+ const sp = rest.indexOf(' ');
91
+ out.set(sp === -1 ? rest : rest.slice(0, sp), sp === -1 ? 'refused' : rest.slice(sp + 1));
92
+ }
93
+ }
94
+ return out;
95
+ }
@@ -16,6 +16,9 @@ export declare function readAllPktLines(bytes: Uint8Array): {
16
16
  export type Service = 'git-upload-pack' | 'git-receive-pack';
17
17
  /** `GET info/refs?service=<service>`: the ref advertisement, HEAD first as a symref. */
18
18
  export declare function advertisement(service: Service, refs: GitRefs): Uint8Array;
19
+ /** The pack of every object `wants` reach that the `haves` the store holds do not: what a push sends (a client's
20
+ * send-pack, smart-http-client.ts). */
21
+ export declare function packObjects(store: GitObjectStore, wants: string[], haves: string[]): Promise<Uint8Array>;
19
22
  /** `POST git-upload-pack`: wants and haves in, NAK and (once `done`) the pack out. A request with `deepen <n>` is shallow:
20
23
  * its answer opens with the shallow-update section (`shallow <sha>` for each boundary commit, `unshallow <sha>` for a
21
24
  * commit the client held shallow whose parents it now receives, then a flush), and its pack stops at the boundary. */
@@ -105,6 +105,15 @@ async function closure(store, roots, seen) {
105
105
  return out;
106
106
  }
107
107
  async function reachable(store, roots) { const seen = new Set(); await closure(store, roots, seen); return seen; }
108
+ /** The pack of every object `wants` reach that the `haves` the store holds do not: what a push sends (a client's
109
+ * send-pack, smart-http-client.ts). */
110
+ export async function packObjects(store, wants, haves) {
111
+ const known = [];
112
+ for (const h of haves)
113
+ if (await store.has(h))
114
+ known.push(h);
115
+ return writePack(await closure(store, wants, await reachable(store, known)));
116
+ }
108
117
  /**
109
118
  * A shallow request's commits (gitprotocol-pack, "Shallow requests"): every commit within `depth` of a want (a want is
110
119
  * depth 1), and the boundary, the commits whose parents the client will not have. The client's own shallow commits it