@lapxo/head 0.1.6 → 0.2.0

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 (68) hide show
  1. package/README.md +39 -14
  2. package/TARGET.bound +301 -118
  3. package/dist/src/access/contract.d.ts +0 -2
  4. package/dist/src/access/obtain.js +3 -3
  5. package/dist/src/ahead/budget.d.ts +4 -0
  6. package/dist/src/ahead/budget.js +8 -0
  7. package/dist/src/ahead/read.d.ts +2 -2
  8. package/dist/src/ahead/read.js +2 -4
  9. package/dist/src/claims/claim.d.ts +1 -0
  10. package/dist/src/claims/claim.js +15 -15
  11. package/dist/src/claims/view.d.ts +3 -0
  12. package/dist/src/claims/view.js +11 -0
  13. package/dist/src/compile/pin.d.ts +7 -0
  14. package/dist/src/compile/pin.js +6 -7
  15. package/dist/src/core/answer.d.ts +3 -0
  16. package/dist/src/core/answer.js +1 -0
  17. package/dist/src/core/device.d.ts +2 -2
  18. package/dist/src/core/device.js +5 -5
  19. package/dist/src/core/lock.d.ts +1 -0
  20. package/dist/src/core/lock.js +2 -1
  21. package/dist/src/core/package.js +3 -1
  22. package/dist/src/core/read.js +1 -2
  23. package/dist/src/core/worlds.js +2 -3
  24. package/dist/src/doors/guard.d.ts +18 -0
  25. package/dist/src/doors/guard.js +63 -0
  26. package/dist/src/doors/head.d.ts +60 -0
  27. package/dist/src/doors/head.js +141 -0
  28. package/dist/src/doors/http.d.ts +3 -0
  29. package/dist/src/doors/http.js +34 -0
  30. package/dist/src/doors/lines.d.ts +3 -0
  31. package/dist/src/doors/lines.js +21 -0
  32. package/dist/src/flows/device.d.ts +3 -0
  33. package/dist/src/flows/device.js +5 -7
  34. package/dist/src/flows/plan.d.ts +2 -0
  35. package/dist/src/flows/plan.js +22 -11
  36. package/dist/src/flows/read.js +4 -1
  37. package/dist/src/index.js +17 -116
  38. package/dist/src/links/birth.d.ts +6 -0
  39. package/dist/src/links/birth.js +48 -0
  40. package/dist/src/links/inspect.d.ts +1 -1
  41. package/dist/src/links/inspect.js +28 -19
  42. package/dist/src/links/link.d.ts +2 -2
  43. package/dist/src/links/link.js +7 -10
  44. package/dist/src/places/found.js +1 -1
  45. package/dist/src/ports/bound.d.ts +9 -11
  46. package/dist/src/ports/bound.js +37 -33
  47. package/dist/src/ports/host.d.ts +53 -0
  48. package/dist/src/ports/host.js +157 -0
  49. package/dist/src/ports/keychain.d.ts +14 -2
  50. package/dist/src/ports/keychain.js +19 -7
  51. package/dist/src/ports/scratch.d.ts +4 -0
  52. package/dist/src/ports/scratch.js +10 -0
  53. package/dist/src/signer.js +8 -7
  54. package/dist/src/sources/connect.js +3 -2
  55. package/dist/src/tools/card.js +4 -6
  56. package/dist/src/use/named.js +3 -3
  57. package/dist/src/verbs/ask.js +2 -1
  58. package/dist/src/verbs/card.js +2 -2
  59. package/dist/src/walk/context.js +3 -6
  60. package/package.json +3 -3
  61. package/dist/src/ports/reading.d.ts +0 -8
  62. package/dist/src/ports/reading.js +0 -14
  63. package/dist/src/ports/signers.d.ts +0 -6
  64. package/dist/src/ports/signers.js +0 -14
  65. package/dist/src/ports/spare.d.ts +0 -1
  66. package/dist/src/ports/spare.js +0 -33
  67. package/dist/src/ports/warm.d.ts +0 -19
  68. package/dist/src/ports/warm.js +0 -45
@@ -0,0 +1,141 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { basename, join } from 'node:path';
10
+ import { answerOf, placeOf } from "../core/answer.js";
11
+ import { LOCK, parse } from '@lapxo/topos/wire';
12
+ import { aboutOf, members, valueOf } from "../core/lock.js";
13
+ import { sayIn } from "../core/surface.js";
14
+ import { matchTemplate, refusedOn, shapeOf, surfaceOf } from "../ports/surface.js";
15
+ import { birth, closing, founded, land, landAct, limits, pinnedAt, policyAt, run, settledView, sign, standingAt, told, words } from "../ports/bound.js";
16
+ import { configureHost, authorityAt } from "../ports/host.js";
17
+ import { held, keep } from "../ports/cache.js";
18
+ import { dropAt, hasAt, layAt, listAt, takeAt } from "../ports/place.js";
19
+ import * as documents from "../ports/documents.js";
20
+ import { keychain, portOf } from "../ports/keychain.js";
21
+ import { deviceOf, idIn, signingOf } from "../core/device.js";
22
+ import { personOf } from "../ports/access/person.js";
23
+ import { readAhead } from "../ahead/read.js";
24
+ import { cardAt } from "../verbs/card.js";
25
+ import { digestOf, guardOf, refusalsOf } from "./guard.js";
26
+ export const askedOf = (raw) => Object.fromEntries(Object.entries(raw ?? {}).map(([k, v]) => [k, v === undefined ? undefined : typeof v === 'object' ? JSON.stringify(v) : String(v)]));
27
+ /** head behind a door: its lock read, every call answered one at a time and each answer's next read ahead, the same for every door. */
28
+ export const headOf = (lock, root, ext, door) => {
29
+ const surface = surfaceOf(lock), runs = valueOf(lock, 'runs'), refused = refusedOn(lock, process.platform);
30
+ Object.assign(words, Object.fromEntries(['read', 'view', 'sign', 'land', 'key', 'signer', 'pin', 'topos', 'standing', 'evidence', 'act', 'profile', 'receipts', 'refusal', 'identity'].map((w) => [w, valueOf(lock, `bound/${w}`)])));
31
+ Object.assign(limits, { wait: Number(valueOf(lock, 'bound/wait').split('..')[1]), most: Number(valueOf(lock, 'bound/printed').split('..')[1]) });
32
+ Object.assign(birth, { at: valueOf(lock, 'founding/person-at'), file: valueOf(lock, 'founding/birth-file') });
33
+ configureHost(lock, limits);
34
+ /** One call at a time, each after the one before it and never on a timer, so one asked while another lands sees its receipt; a place's reads go to its fold host, which outlives the call and never holds a signer. */
35
+ let [last, ahead] = [Promise.resolve(), new AbortController()];
36
+ const turn = (act) => ((next) => { last = next.catch(() => undefined); return next; })(last.then(act));
37
+ /** What head refuses through this door, written as every door writes it; a door that reaches for a signing key is one such refusal, named by the lock's line for that key and never by what the door asked. */
38
+ const [refusals, keyRefused] = [refusalsOf(lock, { write: (place, lot) => write(place, lot), now: () => Date.now() }, door.name), shapeOf([sayIn(lock)('signing-key-refused', {})]).cells[0].code];
39
+ for (const end of ['SIGTERM', 'SIGINT'])
40
+ process.once(end, () => void last.then(refusals.settle).finally(() => process.exit(0)));
41
+ /** The person, asked as the client lets head ask; every wait on them is counted, so a time ceiling measures head's work and never the person's. */
42
+ let waited = 0;
43
+ const timed = (ask) => async (...args) => { const t = Date.now(); try {
44
+ return await ask(...args);
45
+ }
46
+ finally {
47
+ waited += Date.now() - t;
48
+ } };
49
+ const asked = personOf(lock, door);
50
+ const person = { consent: timed(asked.consent), enter: timed(asked.enter), choose: timed(asked.choose), visit: timed(asked.visit) };
51
+ /** head is its home: the folder of its index of places, and the keychain service its keys live under, one per home, so two heads on one keychain never share a key. A place's root is told to bound by the key id its lock names, its secret kept there under the place's account. */
52
+ const home = process.env[valueOf(lock, 'home/env')] || join(process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '', valueOf(lock, 'home/folder'));
53
+ const service = `${deviceOf(lock).service}/${basename(home).replace(/[^\w.-]+/g, '-')}`;
54
+ const store = valueOf(lock, `platform/${process.platform}/store`);
55
+ const keys = keychain(service, store, signingOf(lock));
56
+ const devices = new Map();
57
+ const device = (role) => devices.get(role) ?? ((key) => (key === undefined ? undefined : (devices.set(role, key), key)))(keys.held?.(deviceOf(lock, role).account));
58
+ const asRoot = (account) => ((device, key) => [key, device.signer, portOf(service, account, store, limits.wait)])(deviceOf(lock), valueOf(lock, 'founding/root').split('/')[1]);
59
+ const asRole = (role, lines) => ((one, id) => [id, one.signer, portOf(service, one.account, store, limits.wait)])(deviceOf(lock, role), idIn(lines, device(role)) ?? deviceOf(lock, role).account);
60
+ /** What a call is answered with, one call at a time as when bound blocked head: every bound process it starts is stopped when the client cancels it, and a call cancelled while it waits its turn is never begun. */
61
+ const contextOf = (signal) => {
62
+ /** One call's folds of a place, kept until something lands in it or is laid there; the places its root acted in, whose receipts close once, when the call ends; and those where anything but a read's receipts landed, as bound printed it. */
63
+ const [folds, touched, moved] = [new Map(), new Set(), new Set()], forget = (place) => { for (const k of [...folds.keys()])
64
+ if (k.endsWith(`\0${place}`))
65
+ folds.delete(k); };
66
+ const read = (at) => at?.startsWith(`${valueOf(lock, 'receipt/at')}:`) === true;
67
+ const once = (key, place, fold) => (folds.get(`${key}\0${place}`) ?? ((p) => (folds.set(`${key}\0${place}`, p), p))(fold()));
68
+ const acted = (place, root, done) => done.then((got) => { if ('lines' in got && !got.lines.every((l) => ((f) => f.kind === 'fact' && read(f.value.fields['at']))(parse(l))))
69
+ moved.add(place); return got; }).finally(() => { forget(place); if (root)
70
+ touched.add(place); });
71
+ const covered = async (place, lot, then) => {
72
+ const checked = await authorityAt(place, valueOf(lock, 'founding/root').split('/')[1], 'record-coverage', lot, Date.now(), signal);
73
+ return 'refused' in checked ? { refused: checked.refused } : checked.decision !== 'allow' ? { refused: `REFUSE·authority-preflight/${checked.decision} ${checked.needs.join('|')}` } : then();
74
+ };
75
+ return {
76
+ lock,
77
+ bound: (cwd, args) => run(runs, cwd, args, {}, signal),
78
+ standing: (place) => once('standing', place, () => standingAt(runs, place, (lines) => ['head', 'person'].flatMap((role) => ((id) => (id === undefined ? [] : [id]))(idIn(lines, device(role)))), (line) => read(line.at), signal)),
79
+ authority: (place, key, kind, records, presented) => authorityAt(place, key, kind, records, Date.now(), signal, presented),
80
+ policy: (place) => once('policy', place, () => policyAt(runs, place, signal)),
81
+ world: (place, view) => once(`world/${view}`, place, () => settledView(runs, place, view, signal)),
82
+ adapter: async (origin) => ((name) => (!/^[a-z-]+$/.test(name) ? undefined : import(__rewriteRelativeImportExtension(new URL(`../ports/adapters/${name}${ext}`, import.meta.url).href))))(URL.canParse(origin) ? valueOf(lock, `adapter/${new URL(origin).protocol.slice(0, -1)}`) : ''),
83
+ files: { lay: (place, rel, bytes) => { forget(place); layAt(place, rel, bytes); }, drop: (place, rel) => { forget(place); dropAt(place, rel); }, has: hasAt, list: listAt, read: documents, take: takeAt, own: (rel) => takeAt(join(root, rel)) },
84
+ home: () => home,
85
+ personal: () => process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '',
86
+ found: (place, account) => acted(place, true, founded(runs, place, ...asRoot(account), signal)),
87
+ signRoot: (place, lot, account) => covered(place, lot, () => sign(runs, place, lot, ...asRoot(account), signal)),
88
+ landRoot: (place, lot, account) => acted(place, true, covered(place, lot, () => landAct(runs, place, lot, ...asRoot(account), signal))),
89
+ moved: (place) => moved.has(place),
90
+ settle: async () => { const red = []; for (const place of touched) {
91
+ const why = await closing(runs, place, signal);
92
+ if (why !== undefined)
93
+ red.push(sayIn(lock)('receipts-open', { place, why }));
94
+ } touched.clear(); return red; },
95
+ pinned: (place, file) => pinnedAt(runs, place, file, signal),
96
+ access: async (kind) => (kind !== valueOf(lock, 'access/adapter') || !/^[a-z0-9-]+$/.test(kind) ? undefined : import(__rewriteRelativeImportExtension(new URL(`../ports/access/${kind}${ext}`, import.meta.url).href)).then((m) => m.adapter)),
97
+ person,
98
+ cache: { keep, held },
99
+ signAs: (place, lot, role, lines) => sign(runs, place, lot, ...asRole(role, lines), signal),
100
+ landAs: (place, lot, role, lines) => acted(place, false, landAct(runs, place, lot, ...asRole(role, lines), signal)),
101
+ land: (place, signed) => acted(place, true, land(runs, place, signed, signal)),
102
+ told: (place, signed) => told(runs, place, signed, words.standing, signal),
103
+ elicit: (message) => person.consent(message, aboutOf(lock, 'said/elicit/yes')),
104
+ now: () => Date.now(),
105
+ machine: () => Date.now() - waited,
106
+ begun: Date.now() - waited,
107
+ keys,
108
+ device,
109
+ roots: door.roots, client: door.client,
110
+ };
111
+ };
112
+ const call = (name, args, signal) => (ahead.abort(), turn(async () => {
113
+ const tool = surface.tools.find((one) => one.name === name), place = typeof args?.['place'] === 'string' ? args['place'] : '', keyed = (e) => ((key) => { if (key === undefined || !lock.some((l) => l.scope === key))
114
+ throw e; return [sayIn(lock)('signing-key-refused', { key, place })]; })(/^REFUSE·keychain (\S+) names a signing key/.exec(e.message)?.[1]);
115
+ if (tool === undefined)
116
+ return undefined;
117
+ const context = contextOf(signal), answer = refused !== undefined ? [refused] : signal.aborted ? [] : [...await (await answerOf('tools', tool.name))(askedOf(args), context).catch(keyed), ...await context.settle()];
118
+ void ((early) => turn(() => readAhead(shapeOf(answer).cells, contextOf(), early.signal)))(ahead = new AbortController());
119
+ return answer;
120
+ }).then(async (answer) => { if (answer !== undefined && shapeOf(answer).cells.some((c) => c.code === keyRefused))
121
+ await refusals.write(typeof args?.['place'] === 'string' ? args['place'] : await placed(), name, digestOf(name, args ?? {}), keyRefused); return answer; }));
122
+ const resource = (uri, signal) => turn(async () => {
123
+ const found = surface.resources.flatMap((r) => ((vars) => (vars === undefined ? [] : [{ r, vars }]))(matchTemplate(r.template, uri)))[0];
124
+ if (found === undefined)
125
+ return undefined;
126
+ if (refused !== undefined)
127
+ return { mime: found.r.mime, lines: [refused] };
128
+ return { mime: found.r.mime, lines: await (await answerOf('resources', found.r.name))(askedOf(found.vars), contextOf(signal)) };
129
+ });
130
+ /** A place's card now, as do reads it; whether head's index names a place yet, and the one it names; lines head's own key lands in a place, on its standing. */
131
+ const [cardOf, placed] = [(place) => turn(() => cardAt(contextOf(), place)), () => turn(() => placeOf({}, contextOf()))];
132
+ const hasPlace = () => turn(async () => ((c) => (!c.files.has(home, LOCK) ? false : c.policy(home).then((held) => !('refused' in held) && held.lines.some((l) => l.scope?.startsWith(`${valueOf(lock, 'home/family')}/`) && l.value !== 'withdraw'))))(contextOf()));
133
+ const write = (place, lot) => turn(async () => ((c) => c.standing(place).then((held) => ('refused' in held ? held : c.landAs(place, lot, 'head', held.lines))))(contextOf()));
134
+ return { surface, call, resource, idle: () => last, cardOf, placed, hasPlace, write, refusals, now: () => contextOf().now() };
135
+ };
136
+ /** A door that shows no form: its verbs, each answered by its tool through the guard every such door asks, a verb that is no tool carried as its carrier's action; undefined for a verb it lacks. */
137
+ export const plainOf = (lock, root, ext, name) => {
138
+ const [head, verbs, carrier] = [headOf(lock, root, ext, { can: () => ({ url: false, form: false }), elicit: async () => ({ action: 'decline' }), roots: async () => [], client: () => undefined, name }), members(valueOf(lock, 'doors/verbs')), valueOf(lock, 'doors/carrier')];
139
+ const guard = guardOf(lock, head, (verb, args) => ((tool) => head.call(tool ? verb : carrier, tool ? args : { ...args, action: verb }, new AbortController().signal))(head.surface.tools.some((t) => t.name === verb)));
140
+ return { head, verbs, settle: guard.settle, asked: async (verb, args) => (verbs.includes(verb) ? guard.call(verb, args) : undefined) };
141
+ };
@@ -0,0 +1,3 @@
1
+ import { type Said } from '../core/lock.ts';
2
+ /** The http door, on this device's loopback alone: one POST a verb, its body the args, its answer the cells every door gives; a Host not its own, or any Origin, is refused by name. It ends when its input does. */
3
+ export declare const open: (lock: readonly Said[], root: string, ext: string, name: string) => Promise<void>;
@@ -0,0 +1,34 @@
1
+ import { createServer } from 'node:http';
2
+ import { valueOf } from "../core/lock.js";
3
+ import { sayIn } from "../core/surface.js";
4
+ import { shapeOf } from "../ports/surface.js";
5
+ import { plainOf } from "./head.js";
6
+ /** The http door, on this device's loopback alone: one POST a verb, its body the args, its answer the cells every door gives; a Host not its own, or any Origin, is refused by name. It ends when its input does. */
7
+ export const open = (lock, root, ext, name) => new Promise((done) => {
8
+ const [loopback, asked, say] = [valueOf(lock, 'doors/loopback'), process.argv[4] ?? '0', sayIn(lock)];
9
+ if (!/^\d+$/.test(asked)) {
10
+ process.stdout.write(`${JSON.stringify(shapeOf([say('door-bind', { asked, loopback })]))}\n`);
11
+ process.exitCode = 1;
12
+ done();
13
+ return;
14
+ }
15
+ const door = plainOf(lock, root, ext, name);
16
+ const server = createServer((request, out) => {
17
+ const [own, answer] = [`${loopback}:${server.address().port}`, (status, lines) => out.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify(shapeOf(lines)))];
18
+ if (request.headers.origin !== undefined || request.headers.host !== own)
19
+ return void answer(403, [say(request.headers.origin !== undefined ? 'door-origin' : 'door-host', { origin: request.headers.origin ?? '', host: request.headers.host ?? '', own })]);
20
+ let body = '';
21
+ request.on('data', (chunk) => { body += chunk; }).on('end', async () => {
22
+ const args = ((t) => { try {
23
+ return JSON.parse(t || '{}');
24
+ }
25
+ catch {
26
+ return undefined;
27
+ } })(body);
28
+ const lines = request.method === 'POST' && args !== null && typeof args === 'object' && !Array.isArray(args) ? await door.asked((request.url ?? '').slice(1), args) : undefined;
29
+ answer(lines === undefined ? 400 : 200, lines ?? [say('door-unasked', { verbs: door.verbs.join(', ') })]);
30
+ });
31
+ });
32
+ server.listen(Number(asked), loopback, () => process.stdout.write(`${JSON.stringify({ port: server.address().port })}\n`));
33
+ process.stdin.on('end', () => server.close(() => void door.settle().then(door.head.idle).then(() => done()))).resume();
34
+ });
@@ -0,0 +1,3 @@
1
+ import type { Said } from '../core/lock.ts';
2
+ /** The lines door: one JSON request a line in, its answer one JSON line out, the cells as every door gives them. */
3
+ export declare const open: (lock: readonly Said[], root: string, ext: string, name: string) => Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { sayIn } from "../core/surface.js";
3
+ import { shapeOf } from "../ports/surface.js";
4
+ import { plainOf } from "./head.js";
5
+ /** The lines door: one JSON request a line in, its answer one JSON line out, the cells as every door gives them. */
6
+ export const open = (lock, root, ext, name) => new Promise((done) => {
7
+ const [door, say] = [plainOf(lock, root, ext, name), sayIn(lock)];
8
+ const answered = (id, answer) => process.stdout.write(`${JSON.stringify({ id, ...shapeOf(answer ?? [say('door-unread', { verbs: door.verbs.join(', ') })]) })}\n`);
9
+ let pending = Promise.resolve();
10
+ createInterface({ input: process.stdin }).on('line', (text) => {
11
+ pending = pending.then(async () => {
12
+ const asked = ((t) => { try {
13
+ return JSON.parse(t);
14
+ }
15
+ catch {
16
+ return {};
17
+ } })(text);
18
+ answered(asked.id, await door.asked(asked.verb ?? '', asked.args));
19
+ });
20
+ }).on('close', () => void pending.then(door.settle).then(door.head.idle).then(() => done()));
21
+ });
@@ -17,6 +17,9 @@ export declare const rootIn: (context: Context, place: string) => {
17
17
  * lock as its parent has nothing signed here: what the parent would cut is bound's to judge.
18
18
  */
19
19
  type By = {
20
+ readonly key: string;
21
+ readonly root: boolean;
22
+ readonly records: (lot: readonly string[]) => readonly string[];
20
23
  readonly sign: (lot: readonly string[]) => Promise<Outcome>;
21
24
  readonly land: (lot: readonly string[]) => Promise<Outcome>;
22
25
  };
@@ -7,19 +7,17 @@ import { adoptsIn } from "../adopt/adopt.js";
7
7
  /** A key of this device by its role: read from the keychain, or made there once; only its public half is ever handed on. */
8
8
  export const deviceKey = (context, role) => context.keys.made?.(deviceOf(context.lock, role).account);
9
9
  /** The keychain account of a place's root, when this keychain holds it — who configures the place is here; else what head says of that. */
10
- export const rootIn = (context, place) => ((account) => (context.keys.read(account) === undefined ? { said: [sayIn(context.lock)('no-root', { place })] } : { account }))(fill(valueOf(context.lock, 'founding/root-account'), { place: placeNameOf(place) }));
10
+ export const rootIn = (context, place) => ((account) => (context.keys.held?.(account) === undefined ? { said: [sayIn(context.lock)('no-root', { place })] } : { account }))(fill(valueOf(context.lock, 'founding/root-account'), { place: placeNameOf(place) }));
11
11
  export const widening = (context, place, lines, lot) => {
12
12
  const parent = adoptsIn(context.lock, lines);
13
13
  if (parent !== undefined)
14
14
  return { said: [sayIn(context.lock)('parent-lock', { place, parent })] };
15
15
  const [root, witness, field] = [rootIn(context, place), idIn(lines, context.device('person')), valueOf(context.lock, 'widening/witness')];
16
- const wire = lines.filter((l) => l.scope === 'wire/fields');
16
+ const wire = lines.filter((l) => l.scope === 'wire/fields'), admits = wire.some((l) => members(l.value ?? '').includes(field));
17
17
  const witnessed = (signed) => signed.map((line) => ((got) => (got.kind === 'fact' ? canonical({ ...got.value.fields, [field]: witness }) : line))(parse(line)));
18
18
  if ('account' in root && witness !== undefined) {
19
- if (wire.some((l) => members(l.value ?? '').includes(field)))
20
- return { sign: (signed) => context.signRoot(place, witnessed(signed), root.account), land: (signed) => context.landRoot(place, witnessed(signed), root.account) };
21
- if (lot.every((line) => / scope=wire\/fields /.test(line)))
22
- return { sign: (signed) => context.signRoot(place, signed, root.account), land: (signed) => context.landRoot(place, signed, root.account) };
19
+ if (admits || lot.every((line) => / scope=wire\/fields /.test(line)))
20
+ return { key: valueOf(context.lock, 'founding/root').split('/')[1], root: true, records: (signed) => admits ? witnessed(signed) : signed, sign: (signed) => context.signRoot(place, admits ? witnessed(signed) : signed, root.account), land: (signed) => context.landRoot(place, admits ? witnessed(signed) : signed, root.account) };
23
21
  const [named, bare] = [[...new Set([...wire.flatMap((l) => members(l.value ?? '')), field])].join('|'), (l) => Object.fromEntries(Object.entries(l).filter(([k]) => !['sig', 'epoch'].includes(k)))];
24
22
  return { said: [sayIn(context.lock)('unwitnessed', { place }), ...wire.map((l) => canonical({ ...bare(l), by: 'target', value: 'withdraw' })),
25
23
  canonical({ at: `policy:head/wire/${new Date(context.now()).toISOString()}`, by: 'target', form: 'alphabet', measure: 'id', role: 'reads', scope: 'wire/fields', value: named })] };
@@ -28,5 +26,5 @@ export const widening = (context, place, lines, lot) => {
28
26
  if (stopped)
29
27
  return { said: stopped };
30
28
  const again = lot.flatMap((line) => ((got) => (got.kind === 'fact' && lines.some((l) => l.scope === got.value.fields.scope && l.measure === got.value.fields.measure) ? [got.value.fields.scope ?? ''] : []))(parse(line)))[0];
31
- return again !== undefined ? { said: [sayIn(context.lock)('attested-again', { place, scope: again })] } : { sign: (signed) => context.signAs(place, signed, 'person', lines), land: (signed) => context.landAs(place, signed, 'person', lines) };
29
+ return again !== undefined ? { said: [sayIn(context.lock)('attested-again', { place, scope: again })] } : { key: witness, root: false, records: (signed) => signed, sign: (signed) => context.signAs(place, signed, 'person', lines), land: (signed) => context.landAs(place, signed, 'person', lines) };
32
30
  };
@@ -1,4 +1,6 @@
1
1
  import type { Asked, Context } from '../core/answer.ts';
2
+ /** Bound observes exact configuration records; Q4 domain checks remain in Head's existing card. */
3
+ export declare const authorityFor: (context: Context, place: string, key: string, kind: string, records: readonly string[], observe?: boolean, presented?: string) => Promise<readonly string[] | undefined>;
2
4
  /**
3
5
  * A proposal the person signs: drafted lines shown to the person with the place and the base they are made against; on
4
6
  * their yes to exactly those, signed by their key; what bound prints folding it as if landed comes back as it printed it. Nothing lands; the lot is kept by its digest.
@@ -2,6 +2,14 @@ import { baseOf, inPlace, standingOr } from "../core/answer.js";
2
2
  import { sayIn } from "../core/surface.js";
3
3
  import { widening } from "./device.js";
4
4
  import { parse } from '@lapxo/topos/wire';
5
+ const moved = async (context, place, base) => ((now) => ('said' in now ? now.said : baseOf(now.lines) !== base ? [sayIn(context.lock)('base-moved', { base })] : undefined))(await standingOr(context, place));
6
+ /** Bound observes exact configuration records; Q4 domain checks remain in Head's existing card. */
7
+ export const authorityFor = async (context, place, key, kind, records, observe = false, presented) => {
8
+ const result = await context.authority(place, key, kind, records, presented), say = sayIn(context.lock);
9
+ if ('refused' in result)
10
+ return [say('authority-refused', { place, key, kind, why: result.refused, code: result.code })];
11
+ return observe || result.decision !== 'allow' ? [say(observe ? 'authority-card' : result.decision === 'deny' ? 'authority-refused' : 'authority-needed', { place, key, kind, decision: result.decision, authority: result.authority, request: result.request, needs: result.needs.join('|'), why: result.decision })] : undefined;
12
+ };
5
13
  /**
6
14
  * A proposal the person signs: drafted lines shown to the person with the place and the base they are made against; on
7
15
  * their yes to exactly those, signed by their key; what bound prints folding it as if landed comes back as it printed it. Nothing lands; the lot is kept by its digest.
@@ -18,6 +26,9 @@ export const planLot = (asked, context) => inPlace(asked, context, async (place)
18
26
  const by = widening(context, place, held.lines, lot);
19
27
  if ('said' in by)
20
28
  return by.said;
29
+ const initial = by.root ? await authorityFor(context, place, by.key, 'record-coverage', by.records(lot)) : undefined;
30
+ if (initial)
31
+ return initial;
21
32
  const consent = await context.elicit(say('elicit', { count: String(lot.length), place, base, lines: lot.join('\n') }));
22
33
  if (consent === 'none')
23
34
  return [say('sign-yourself', { place }), ...lot];
@@ -26,12 +37,10 @@ export const planLot = (asked, context) => inPlace(asked, context, async (place)
26
37
  const signed = await by.sign(lot);
27
38
  if ('refused' in signed)
28
39
  return [signed.refused];
29
- const now = await standingOr(context, place);
30
- if ('said' in now)
31
- return now.said;
32
- if (baseOf(now.lines) !== base)
33
- return [say('base-moved', { base })];
34
- const kept = context.cache.keep(Buffer.from(JSON.stringify({ place, base, signed: signed.lines })));
40
+ const stale = await moved(context, place, base);
41
+ if (stale)
42
+ return stale;
43
+ const kept = context.cache.keep(Buffer.from(JSON.stringify({ place, base, signed: signed.lines, root: by.root })));
35
44
  return [say('planned', { lot: kept, base }), ...(await context.told(place, signed.lines))];
36
45
  });
37
46
  /** A planned lot landed by its digest, only while the place stands on the base it was planned against. */
@@ -41,11 +50,13 @@ export const landLot = async (asked, context) => {
41
50
  if (bytes === undefined)
42
51
  return [say('no-lot', { lot: asked['lot'] ?? '' })];
43
52
  const kept = JSON.parse(Buffer.from(bytes).toString('utf8'));
44
- const now = await standingOr(context, kept.place);
45
- if ('said' in now)
46
- return now.said;
47
- if (baseOf(now.lines) !== kept.base)
48
- return [say('base-moved', { base: kept.base })];
53
+ const stale = await moved(context, kept.place, kept.base);
54
+ if (stale)
55
+ return stale;
56
+ const author = parse(kept.signed[0] ?? '');
57
+ const checked = kept.root ? await authorityFor(context, kept.place, author.kind === 'fact' ? author.value.fields.by ?? '' : '', 'admission', kept.signed) : undefined;
58
+ if (checked)
59
+ return checked;
49
60
  const landed = await context.land(kept.place, kept.signed);
50
61
  return 'refused' in landed ? [landed.refused] : [say('landed', { count: String(landed.lines.length) }), ...landed.lines];
51
62
  };
@@ -9,7 +9,7 @@ import { principalOf } from "../access/obtain.js";
9
9
  import { jsonOf } from "../access/contract.js";
10
10
  import { refusedAccess } from "../sources/refused.js";
11
11
  import { hand, keptOf } from "../kept/kept.js";
12
- import { markOf, overOf } from "../ahead/budget.js";
12
+ import { calledIn, markOf, overOf } from "../ahead/budget.js";
13
13
  /** An intent an earlier act left open, closed by the read that looked at what it would have changed: a line naming that read-s digest, signed by head. */
14
14
  const closeWith = async (intent, source, digest, { word, lines, lands }, context) => {
15
15
  const say = sayIn(context.lock);
@@ -72,6 +72,9 @@ export const readSource = (asked, context) => called(asked, context, planRead, a
72
72
  const again = was?.kind === 'again' ? say(`kept-${was.why}`, { source, operation, at: was.at }) : [];
73
73
  if (overOf(lines, [family, at, word('ahead/mark')], source, context.now(), most('read/rate'), Number(word('ahead/share').split('..')[1]), asked))
74
74
  return asked['ahead'] === 'true' ? [] : say('rate', { source });
75
+ const calls = lines.find((l) => l.scope === `${word('calls/family')}/${source}`), made = calls && calledIn(lines, [family, at, word('ahead/mark')], source, calls.measure ?? '', context.now());
76
+ if (calls && (made === undefined || made >= most(calls.scope)))
77
+ return asked['ahead'] === 'true' ? [] : say(made === undefined ? 'calls-unwindowed' : 'calls-owed', { source, calls: String(most(calls.scope)), window: calls.measure });
75
78
  const attached = await attach();
76
79
  if ('said' in attached)
77
80
  return attached.said;
package/dist/src/index.js CHANGED
@@ -7,144 +7,45 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
7
7
  }
8
8
  return path;
9
9
  };
10
- import { basename, join } from 'node:path';
11
10
  import { fileURLToPath } from 'node:url';
12
11
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
12
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
13
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
15
- import { answerOf } from "./core/answer.js";
16
- import { parse } from '@lapxo/topos/wire';
17
- import { aboutOf, valueOf } from "./core/lock.js";
18
- import { fill, sayIn } from "./core/surface.js";
19
- import { matchTemplate, refusedOn, shapeOf, shapeSchema, surfaceOf } from "./ports/surface.js";
20
- import { birth, closing, cooled, founded, land, landAct, limits, pinnedAt, policyAt, run, settledView, sign, standingAt, told, warm, words } from "./ports/bound.js";
21
- import { warmth } from "./ports/warm.js";
22
- import { held, keep } from "./ports/cache.js";
23
- import { registryFor } from "./ports/signers.js";
24
- import { dropAt, folderOf, hasAt, layAt, listAt, lockAt, takeAt } from "./ports/place.js";
25
- import * as documents from "./ports/documents.js";
26
- import { keychain } from "./ports/keychain.js";
27
- import { algorithmOf, deviceOf, idIn } from "./core/device.js";
28
- import { publicOf } from "./ports/keys.js";
29
- import { personOf } from "./ports/access/person.js";
30
- import { readAhead } from "./ahead/read.js";
14
+ import { aboutOf, members, valueOf } from "./core/lock.js";
15
+ import { fill } from "./core/surface.js";
16
+ import { shapeOf, shapeSchema } from "./ports/surface.js";
17
+ import { folderOf, lockAt } from "./ports/place.js";
18
+ import { askedOf, headOf } from "./doors/head.js";
31
19
  const [ext, root] = ((self) => [self.slice(self.lastIndexOf('.')), fileURLToPath(new URL(self.endsWith('.ts') ? '..' : '../..', self))])(import.meta.url);
32
20
  const lock = lockAt(root);
33
- if (process.argv[2] !== valueOf(lock, 'cli/run')) {
21
+ const [door, doors] = [process.argv[3], members(valueOf(lock, 'cli/run/doors'))];
22
+ if (process.argv[2] !== valueOf(lock, 'cli/run') || (door !== undefined && !doors.includes(door))) {
34
23
  process.stdout.write(`${aboutOf(lock, 'cli/help')}\n`);
35
24
  process.exit(0);
36
25
  }
37
- const surface = surfaceOf(lock);
38
- const runs = valueOf(lock, 'runs');
39
- Object.assign(words, Object.fromEntries(['read', 'view', 'sign', 'land', 'key', 'signer', 'pin', 'topos', 'standing', 'evidence', 'act', 'profile', 'receipts', 'refusal', 'identity'].map((w) => [w, valueOf(lock, `bound/${w}`)])));
40
- Object.assign(limits, { wait: Number(valueOf(lock, 'bound/wait').split('..')[1]), most: Number(valueOf(lock, 'bound/printed').split('..')[1]) });
41
- Object.assign(birth, { at: valueOf(lock, 'founding/person-at'), file: valueOf(lock, 'founding/birth-file') });
42
- Object.assign(warmth, { spares: Number(valueOf(lock, 'bound/spares').split('..')[1]), boot: valueOf(lock, 'bound/spare-env').split('|') });
43
- /** One call at a time, each after the one before it and never on a timer, so one asked while another lands sees its receipt; bound started ahead for it, and nothing of it kept once it is answered. */
44
- let [last, ahead] = [Promise.resolve(), new AbortController()];
45
- const turn = (act) => ((next) => { last = next.catch(() => undefined); return next; })(last.then(() => { void warm(runs); return act().finally(cooled); }));
46
- for (const end of ['SIGTERM', 'SIGINT'])
47
- process.once(end, () => void last.finally(() => process.exit(0)));
26
+ if (door !== undefined && door !== doors[0]) {
27
+ await (await import(__rewriteRelativeImportExtension(new URL(`./doors/${door}${ext}`, import.meta.url).href))).open(lock, root, ext, door);
28
+ process.exit();
29
+ }
48
30
  const offered = (n) => (n ? { listChanged: false } : undefined);
31
+ const head = headOf(lock, root, ext, {
32
+ can: () => ((e) => ({ url: Boolean(e?.url), form: e !== undefined && (e.form !== undefined || Object.keys(e).length === 0) }))(server.getClientCapabilities()?.elicitation), elicit: (params) => server.elicitInput(params, { timeout: Number(valueOf(lock, 'access/wait').split('..')[1]) }),
33
+ roots: async () => (server.getClientCapabilities()?.roots ? (await server.listRoots()).roots.flatMap((root) => folderOf(root.uri) ?? []) : []), client: () => ((c) => (c ? `${c.name}@${c.version}` : undefined))(server.getClientVersion()), name: doors[0],
34
+ }), surface = head.surface;
49
35
  const server = new Server({ name: surface.name, version: surface.version }, { capabilities: { tools: offered(surface.tools.length), resources: offered(surface.resources.length), prompts: offered(surface.prompts.length) }, instructions: aboutOf(lock, 'prose/en/instructions') });
50
- /** The person, asked as the client lets head ask; every wait on them is counted, so a time ceiling measures head's work and never the person's. */
51
- let waited = 0;
52
- const timed = (ask) => async (...args) => { const t = Date.now(); try {
53
- return await ask(...args);
54
- }
55
- finally {
56
- waited += Date.now() - t;
57
- } };
58
- const asked = personOf(lock, { can: () => ((e) => ({ url: Boolean(e?.url), form: e !== undefined && (e.form !== undefined || Object.keys(e).length === 0) }))(server.getClientCapabilities()?.elicitation), elicit: (params) => server.elicitInput(params, { timeout: Number(valueOf(lock, 'access/wait').split('..')[1]) }) });
59
- const person = { consent: timed(asked.consent), enter: timed(asked.enter), choose: timed(asked.choose), visit: timed(asked.visit) };
60
- /** head is its home: the folder of its index of places, and the keychain service its keys live under, one per home, so two heads on one keychain never share a key. A place's root is told to bound by the key id its lock names, its secret kept there under the place's account. */
61
- const home = process.env[valueOf(lock, 'home/env')] || join(process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '', valueOf(lock, 'home/folder'));
62
- const service = `${deviceOf(lock).service}/${basename(home).replace(/[^\w.-]+/g, '-')}`;
63
- const store = valueOf(lock, `platform/${process.platform}/store`);
64
- const keys = keychain(service, store);
65
- const devices = new Map();
66
- const device = (role) => devices.get(role) ?? ((secret) => (secret === undefined ? undefined : ((key) => { devices.set(role, key); return key; })(publicOf(secret))))(keys.read(deviceOf(lock, role).account));
67
- const asRoot = (account) => ((device, key) => [key, device.signer, { BOUND_SIGNERS: registryFor(device.signer, service, account, key, valueOf(lock, 'founding/algorithm'), store) }])(deviceOf(lock), valueOf(lock, 'founding/root').split('/')[1]);
68
- /** What a call is answered with, one call at a time as when bound blocked head: every bound process it starts is stopped when the client cancels it, and a call cancelled while it waits its turn is never begun. */
69
- const contextOf = (signal) => {
70
- /** One call's folds of a place, kept until something lands in it or is laid there; the places its root acted in, whose receipts close once, when the call ends; and those where anything but a read's receipts landed, as bound printed it. */
71
- const [folds, touched, moved] = [new Map(), new Set(), new Set()], forget = (place) => { for (const k of [...folds.keys()])
72
- if (k.endsWith(`\0${place}`))
73
- folds.delete(k); };
74
- const read = (at) => at?.startsWith(`${valueOf(lock, 'receipt/at')}:`) === true;
75
- const once = (key, place, fold) => (folds.get(`${key}\0${place}`) ?? ((p) => (folds.set(`${key}\0${place}`, p), p))(fold()));
76
- const acted = (place, root, done) => done.then((got) => { if ('lines' in got && !got.lines.every((l) => ((f) => f.kind === 'fact' && read(f.value.fields['at']))(parse(l))))
77
- moved.add(place); return got; }).finally(() => { forget(place); if (root)
78
- touched.add(place); });
79
- const signRoot = (place, lot, account) => sign(runs, place, lot, ...asRoot(account), signal);
80
- const signAs = (place, lot, role, lines) => ((one, id) => sign(runs, place, lot, id, one.signer, { BOUND_SIGNERS: registryFor(one.signer, service, one.account, id, algorithmOf(lines), store) }, signal))(deviceOf(lock, role), idIn(lines, device(role)) ?? deviceOf(lock, role).account);
81
- return {
82
- lock,
83
- bound: (cwd, args) => run(runs, cwd, args, {}, signal),
84
- standing: (place) => once('standing', place, () => standingAt(runs, place, (lines) => ['head', 'person'].flatMap((role) => ((id) => (id === undefined ? [] : [id]))(idIn(lines, device(role)))), (line) => read(line.at), signal)),
85
- policy: (place) => once('policy', place, () => policyAt(runs, place, signal)),
86
- world: (place, view) => once(`world/${view}`, place, () => settledView(runs, place, view, signal)),
87
- adapter: async (origin) => ((name) => (!/^[a-z-]+$/.test(name) ? undefined : import(__rewriteRelativeImportExtension(new URL(`./ports/adapters/${name}${ext}`, import.meta.url).href))))(URL.canParse(origin) ? valueOf(lock, `adapter/${new URL(origin).protocol.slice(0, -1)}`) : ''),
88
- files: { lay: (place, rel, bytes) => { forget(place); layAt(place, rel, bytes); }, drop: (place, rel) => { forget(place); dropAt(place, rel); }, has: hasAt, list: listAt, read: documents, take: takeAt, own: (rel) => takeAt(join(root, rel)) },
89
- home: () => home,
90
- personal: () => process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '',
91
- found: (place, account) => acted(place, true, founded(runs, place, ...asRoot(account), signal)),
92
- signRoot,
93
- landRoot: (place, lot, account) => acted(place, true, landAct(runs, place, lot, ...asRoot(account), signal)),
94
- moved: (place) => moved.has(place),
95
- settle: async () => { const red = []; for (const place of touched) {
96
- const why = await closing(runs, place, signal);
97
- if (why !== undefined)
98
- red.push(sayIn(lock)('receipts-open', { place, why }));
99
- } touched.clear(); return red; },
100
- pinned: (place, file) => pinnedAt(runs, place, file, signal),
101
- access: async (kind) => (kind !== valueOf(lock, 'access/adapter') || !/^[a-z0-9-]+$/.test(kind) ? undefined : import(__rewriteRelativeImportExtension(new URL(`./ports/access/${kind}${ext}`, import.meta.url).href)).then((m) => m.adapter)),
102
- person,
103
- cache: { keep, held },
104
- signAs,
105
- landAs: (place, lot, role, lines) => acted(place, false, ((one, id) => landAct(runs, place, lot, id, one.signer, { BOUND_SIGNERS: registryFor(one.signer, service, one.account, id, algorithmOf(lines), store) }, signal))(deviceOf(lock, role), idIn(lines, device(role)) ?? deviceOf(lock, role).account)),
106
- land: (place, signed) => acted(place, true, land(runs, place, signed, signal)),
107
- told: (place, signed) => told(runs, place, signed, words.standing, signal),
108
- elicit: (message) => person.consent(message, aboutOf(lock, 'said/elicit/yes')),
109
- now: () => Date.now(),
110
- machine: () => Date.now() - waited,
111
- begun: Date.now() - waited,
112
- keys,
113
- device,
114
- roots: async () => (server.getClientCapabilities()?.roots ? (await server.listRoots()).roots.flatMap((root) => folderOf(root.uri) ?? []) : []), client: () => ((c) => (c ? `${c.name}@${c.version}` : undefined))(server.getClientVersion()),
115
- };
116
- };
117
- const askedOf = (raw) => Object.fromEntries(Object.entries(raw ?? {}).map(([k, v]) => [k, v === undefined ? undefined : typeof v === 'object' ? JSON.stringify(v) : String(v)]));
118
36
  const schemaOf = (fields) => ({ type: 'object',
119
37
  properties: Object.fromEntries(fields.map((f) => [f.name, { type: f.type, description: f.about }])), required: fields.filter((f) => f.required).map((f) => f.name) });
120
38
  if (surface.tools.length)
121
39
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: surface.tools.map((tool) => ({ name: tool.name, description: tool.about,
122
40
  inputSchema: schemaOf([...tool.fields]), outputSchema: shapeSchema(lock), annotations: Object.fromEntries(tool.hints.map((hint) => [`${hint}Hint`, true])) })) }));
123
41
  if (surface.tools.length)
124
- server.setRequestHandler(CallToolRequestSchema, async (request, extra) => (ahead.abort(), turn(async () => {
125
- const tool = surface.tools.find((one) => one.name === request.params.name);
126
- if (tool === undefined)
127
- return { isError: true, content: [] };
128
- const refused = refusedOn(lock, process.platform);
129
- const context = contextOf(extra.signal), answer = refused !== undefined ? [refused] : extra.signal.aborted ? [] : [...await (await answerOf('tools', tool.name))(askedOf(request.params.arguments), context), ...await context.settle()];
130
- void ((early) => turn(() => readAhead(shapeOf(answer).cells, contextOf(), early.signal)))(ahead = new AbortController());
131
- return { content: [{ type: 'text', text: answer.join('\n') }], structuredContent: shapeOf(answer) };
132
- })));
42
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => ((answer) => (answer === undefined ? { isError: true, content: [] } : { content: [{ type: 'text', text: answer.join('\n') }], structuredContent: shapeOf(answer) }))(await head.call(request.params.name, request.params.arguments, extra.signal)));
133
43
  if (surface.resources.length)
134
44
  server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
135
45
  if (surface.resources.length)
136
46
  server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({ resourceTemplates: surface.resources.map((r) => ({ name: r.name, uriTemplate: r.template, description: r.about, mimeType: r.mime })) }));
137
47
  if (surface.resources.length)
138
- server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => turn(async () => {
139
- const found = surface.resources.flatMap((r) => ((vars) => (vars === undefined ? [] : [{ r, vars }]))(matchTemplate(r.template, request.params.uri)))[0];
140
- if (found === undefined)
141
- return { contents: [] };
142
- const refused = refusedOn(lock, process.platform);
143
- if (refused !== undefined)
144
- return { contents: [{ uri: request.params.uri, mimeType: found.r.mime, text: refused }] };
145
- const lines = await (await answerOf('resources', found.r.name))(askedOf(found.vars), contextOf(extra.signal));
146
- return { contents: [{ uri: request.params.uri, mimeType: found.r.mime, text: lines.join('\n') }] };
147
- }));
48
+ server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => ((got) => ({ contents: got === undefined ? [] : [{ uri: request.params.uri, mimeType: got.mime, text: got.lines.join('\n') }] }))(await head.resource(request.params.uri, extra.signal)));
148
49
  if (surface.prompts.length)
149
50
  server.setRequestHandler(ListPromptsRequestSchema, () => ({ prompts: surface.prompts.map((p) => ({ name: p.name, description: p.about, arguments: p.fields.map((f) => ({ name: f.name, description: f.about, required: f.required })) })) }));
150
51
  if (surface.prompts.length)
@@ -0,0 +1,6 @@
1
+ import { type Asked, type Context } from '../core/answer.ts';
2
+ import { type Said } from '../core/lock.ts';
3
+ /** A separate public birth artifact. The source package, authority and personal birth journal are not its payload. */
4
+ export declare const sharedBirth: (context: Context, to: string, standing: readonly Said[]) => readonly string[];
5
+ /** Inline or client-shared local birth bytes are inspected only; no transport, signer, file write or admission is acquired. */
6
+ export declare const receivedBirth: (asked: Asked, context: Context) => Promise<readonly string[] | undefined>;
@@ -0,0 +1,48 @@
1
+ import { basename, join } from 'node:path';
2
+ import { canonical, parse } from '@lapxo/topos/wire';
3
+ import { PORTABLE_BIRTH_PROFILE, decodePortableBirth, encodePortableBirth, inspectPortableBirth } from '@lapxo/topos/contract';
4
+ import { semanticStandingBytes } from '@lapxo/topos/standing';
5
+ import { inStanding } from "../core/answer.js";
6
+ import { members, valueOf } from "../core/lock.js";
7
+ import { sayIn } from "../core/surface.js";
8
+ import { digestIn } from "../core/worlds.js";
9
+ const budget = (context) => ({ bytes: Number(valueOf(context.lock, 'bound/printed').split('..')[1]), items: Number(valueOf(context.lock, 'share/birth-items').split('..')[1]) });
10
+ const refused = (context, error) => [sayIn(context.lock)('birth-refused', { why: error instanceof Error ? error.message : String(error) })];
11
+ /** A separate public birth artifact. The source package, authority and personal birth journal are not its payload. */
12
+ export const sharedBirth = (context, to, standing) => {
13
+ const say = sayIn(context.lock), scopes = members(valueOf(context.lock, 'share/birth-scopes')), file = valueOf(context.lock, 'share/birth-file');
14
+ const declarations = standing.filter(line => scopes.includes(line.scope ?? '')).map(({ by: _by, sig: _sig, epoch: _epoch, ...line }) => canonical(line));
15
+ if (!declarations.length)
16
+ return [say('birth-empty')];
17
+ try {
18
+ const record = encodePortableBirth({ origin: digestIn(context.lock)(semanticStandingBytes(standing.map(line => canonical(line)))), declarations }, budget(context)), bytes = new TextEncoder().encode(record);
19
+ context.files.lay(to, file, bytes);
20
+ return [say('birth-shared', { document: join(to, file), digest: digestIn(context.lock)(bytes), record })];
21
+ }
22
+ catch (error) {
23
+ return refused(context, error);
24
+ }
25
+ };
26
+ /** Inline or client-shared local birth bytes are inspected only; no transport, signer, file write or admission is acquired. */
27
+ export const receivedBirth = async (asked, context) => {
28
+ const document = asked['document'] ?? '', inline = document.startsWith(PORTABLE_BIRTH_PROFILE.split('/')[0] + '/');
29
+ if (!inline && !(document.startsWith('/') && basename(document) === valueOf(context.lock, 'share/birth-file')))
30
+ return undefined;
31
+ return inStanding(asked, context, async (place, standing) => {
32
+ const say = sayIn(context.lock), bytes = inline ? new TextEncoder().encode(document) : (await context.files.read.documentOf(context, place, document))?.bytes;
33
+ if (!bytes)
34
+ return [say('birth-needs', { document })];
35
+ if (asked['digest'] && digestIn(context.lock)(bytes) !== asked['digest'])
36
+ return [say('document-digest', { document: inline ? PORTABLE_BIRTH_PROFILE : document, digest: asked['digest'], got: digestIn(context.lock)(bytes) })];
37
+ try {
38
+ const pack = decodePortableBirth(new TextDecoder().decode(bytes), budget(context)), allowed = members(valueOf(context.lock, 'share/birth-scopes'));
39
+ if (pack.declarations.some(line => ((p) => p.kind !== 'fact' || !allowed.includes(p.value.fields.scope ?? ''))(parse(line))))
40
+ return [say('birth-refused', { why: 'declaration outside the public birth selection' })];
41
+ const inspected = inspectPortableBirth(pack, [...new Set(standing.flatMap(line => line.scope ?? []))], budget(context));
42
+ return [say('birth-inspected', { inspection: inspected.status, collisions: inspected.collisions.join(', ') || 'none', origin: pack.origin, declarations: pack.declarations.join('\n') })];
43
+ }
44
+ catch (error) {
45
+ return refused(context, error);
46
+ }
47
+ }, context.policy);
48
+ };
@@ -1,6 +1,6 @@
1
1
  import { type Asked, type Context } from '../core/answer.ts';
2
2
  /**
3
- * A #bound: link connected: inspected by the instrument in the person's place, never authenticated — its bytes are the
3
+ * A legacy or located reference connected: inspected by the instrument in the person's place, never authenticated — its bytes are the
4
4
  * link's, and nothing shows who shared it — and what it names is shown; nothing is connected or granted, since a package
5
5
  * is received by the digest its sharer sent apart. A place that admits no content transport is handed the lines that admit
6
6
  * one, to sign; bytes that do not arrive are owed by the link until they do; bytes that are not what it names are refused.