@lapxo/head 0.1.0 → 0.1.4

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 (87) hide show
  1. package/README.md +24 -0
  2. package/TARGET.bound +517 -54
  3. package/dist/src/access/contract.d.ts +2 -0
  4. package/dist/src/access/obtain.d.ts +7 -1
  5. package/dist/src/access/obtain.js +6 -6
  6. package/dist/src/adopt/adopt.js +5 -2
  7. package/dist/src/compile/pin.js +5 -5
  8. package/dist/src/core/act.d.ts +1 -0
  9. package/dist/src/core/act.js +1 -0
  10. package/dist/src/core/answer.d.ts +18 -4
  11. package/dist/src/core/answer.js +7 -4
  12. package/dist/src/core/lock.d.ts +2 -0
  13. package/dist/src/core/lock.js +2 -0
  14. package/dist/src/core/package.d.ts +54 -0
  15. package/dist/src/core/package.js +65 -0
  16. package/dist/src/core/read.d.ts +3 -0
  17. package/dist/src/core/read.js +8 -4
  18. package/dist/src/core/surface.d.ts +1 -40
  19. package/dist/src/core/surface.js +4 -29
  20. package/dist/src/core/trust.d.ts +7 -0
  21. package/dist/src/core/trust.js +19 -0
  22. package/dist/src/flows/call.d.ts +1 -3
  23. package/dist/src/flows/call.js +3 -6
  24. package/dist/src/flows/device.js +1 -6
  25. package/dist/src/flows/read.js +24 -11
  26. package/dist/src/flows/world.js +3 -3
  27. package/dist/src/index.js +45 -21
  28. package/dist/src/onboard/first.d.ts +9 -0
  29. package/dist/src/onboard/first.js +38 -0
  30. package/dist/src/onboard/owed.d.ts +10 -0
  31. package/dist/src/onboard/owed.js +28 -0
  32. package/dist/src/onboard/site.d.ts +12 -0
  33. package/dist/src/onboard/site.js +22 -0
  34. package/dist/src/places/admit.js +1 -1
  35. package/dist/src/places/found.d.ts +1 -0
  36. package/dist/src/places/found.js +8 -7
  37. package/dist/src/ports/access/authorizer.d.ts +10 -0
  38. package/dist/src/ports/access/authorizer.js +34 -0
  39. package/dist/src/ports/access/flow.js +13 -3
  40. package/dist/src/ports/access/person.d.ts +3 -2
  41. package/dist/src/ports/access/person.js +12 -5
  42. package/dist/src/ports/adapters/request.d.ts +3 -0
  43. package/dist/src/ports/adapters/request.js +5 -3
  44. package/dist/src/ports/bound.d.ts +12 -2
  45. package/dist/src/ports/bound.js +30 -13
  46. package/dist/src/ports/documents.d.ts +34 -0
  47. package/dist/src/ports/documents.js +64 -0
  48. package/dist/src/ports/keychain.d.ts +2 -1
  49. package/dist/src/ports/keychain.js +3 -2
  50. package/dist/src/ports/place.d.ts +1 -0
  51. package/dist/src/ports/place.js +7 -1
  52. package/dist/src/ports/process.d.ts +9 -0
  53. package/dist/src/ports/process.js +5 -3
  54. package/dist/src/ports/spare.d.ts +1 -0
  55. package/dist/src/ports/spare.js +33 -0
  56. package/dist/src/ports/surface.d.ts +130 -0
  57. package/dist/src/ports/surface.js +50 -0
  58. package/dist/src/ports/warm.d.ts +19 -0
  59. package/dist/src/ports/warm.js +46 -0
  60. package/dist/src/signer.js +1 -1
  61. package/dist/src/sources/access.d.ts +10 -1
  62. package/dist/src/sources/access.js +15 -10
  63. package/dist/src/sources/connect.d.ts +13 -2
  64. package/dist/src/sources/connect.js +69 -38
  65. package/dist/src/sources/refused.d.ts +7 -0
  66. package/dist/src/sources/refused.js +16 -0
  67. package/dist/src/tools/ask.d.ts +1 -0
  68. package/dist/src/tools/ask.js +1 -0
  69. package/dist/src/tools/card.d.ts +1 -1
  70. package/dist/src/tools/card.js +5 -4
  71. package/dist/src/tools/do.d.ts +1 -0
  72. package/dist/src/tools/do.js +1 -0
  73. package/dist/src/tools/look.d.ts +5 -4
  74. package/dist/src/tools/look.js +15 -13
  75. package/dist/src/tools/places.d.ts +7 -0
  76. package/dist/src/tools/places.js +32 -0
  77. package/dist/src/tools/share.d.ts +1 -0
  78. package/dist/src/tools/share.js +1 -0
  79. package/dist/src/verbs/ask.d.ts +11 -0
  80. package/dist/src/verbs/ask.js +35 -0
  81. package/dist/src/verbs/card.d.ts +11 -0
  82. package/dist/src/verbs/card.js +16 -0
  83. package/dist/src/verbs/do.d.ts +8 -0
  84. package/dist/src/verbs/do.js +28 -0
  85. package/package.json +6 -5
  86. /package/dist/src/{core → ports}/keys.d.ts +0 -0
  87. /package/dist/src/{core → ports}/keys.js +0 -0
@@ -6,8 +6,9 @@ import { open } from "./page.js";
6
6
  /**
7
7
  * The person at this device, asked through the richest channel the client declares, in the order head's lock lines name:
8
8
  * the client's own form; head's one-time page, sent by the client as an address; the same page opened by this device's
9
- * browser. A secret goes only through the page — never a form the model could see — and a yes only through what the
10
- * client can ask, else it is the person's to sign. A visit to a provider comes back to such a page.
9
+ * browser. A secret goes only through the page — never a form the model could see — and a yes is the person's answer on
10
+ * any of them, never the model's: where none reaches the person, nothing was asked and the lines are theirs to sign.
11
+ * A visit to a provider comes back to such a page.
11
12
  */
12
13
  export const personOf = (lock, client) => {
13
14
  const wait = Number(valueOf(lock, 'access/wait').split('..')[1]);
@@ -18,9 +19,15 @@ export const personOf = (lock, client) => {
18
19
  const paged = async (title, fields, how) => offered(fill(aboutOf(lock, fields.some((f) => f.secret) ? 'said/enter' : 'said/answer'), { title }), await open(title, fields, wait), how);
19
20
  const form = async (message, properties) => ((got) => (got.action === 'accept' ? Object.fromEntries(Object.entries(got.content ?? {}).map(([k, v]) => [k, String(v)])) : undefined))(await client.elicit({ message, requestedSchema: { type: 'object', properties, required: Object.keys(properties) } }));
20
21
  return {
21
- consent: async (message, word) => ((how) => (how === undefined ? Promise.resolve('none')
22
- : (how === 'form' ? form(message, { yes: { type: 'boolean', title: word } }) : paged(message, [{ name: 'yes', label: word, secret: false, options: [{ value: 'true', label: word }, { value: 'false', label: 'No' }] }], how))
23
- .then((got) => (got?.['yes'] === 'true' ? 'yes' : 'no'))))(channel('ask/consent-channels')),
22
+ consent: async (message, word) => {
23
+ const how = channel('ask/consent-channels'), said = (got) => (got?.['yes'] === 'true' ? 'yes' : 'no');
24
+ if (how === undefined)
25
+ return 'none';
26
+ if (how === 'form')
27
+ return said(await form(message, { yes: { type: 'boolean', title: word } }));
28
+ const page = await open(message, [{ name: 'yes', label: word, secret: false, options: [{ value: 'true', label: word }, { value: 'false', label: 'No' }] }], wait);
29
+ return (await offer(fill(aboutOf(lock, 'said/answer'), { title: message }), page.url, how)) ? said(await page.held) : (page.close(), 'none');
30
+ },
24
31
  enter: async (title, fields) => paged(title, fields, channel('ask/secret-channels')),
25
32
  choose: async (message, choices) => ((how) => (how === 'form'
26
33
  ? form(message, Object.fromEntries(choices.map((c) => [c.name, { type: 'string', title: c.label ?? c.name, ...(c.options ? { oneOf: c.options.map((o) => ({ const: o.value, title: o.label })) } : {}) }])))
@@ -6,6 +6,8 @@ export type Fetched = {
6
6
  readonly status: number;
7
7
  readonly bytes: Uint8Array;
8
8
  readonly type: string;
9
+ readonly location?: string;
10
+ readonly authenticate?: string;
9
11
  } | {
10
12
  readonly kind: 'over';
11
13
  } | {
@@ -21,6 +23,7 @@ export type Prepared = {
21
23
  readonly refused: string;
22
24
  };
23
25
  export declare const published: (url: string, digest: string, most: number, wait: number) => Promise<Uint8Array | undefined>;
26
+ export declare const fetched: (url: string, attachment: Attachment | undefined, accept: string | undefined, most: number, wait: number) => Promise<Fetched>;
24
27
  export declare const get: (url: string, wait: number) => Promise<{
25
28
  readonly bytes: Uint8Array;
26
29
  readonly type: string;
@@ -17,16 +17,18 @@ const requestCapped = async (method, url, headers, body, most, wait, redirect =
17
17
  break;
18
18
  chunks.push(chunk);
19
19
  }
20
- return size > most ? { kind: 'over' } : { kind: 'got', status: response.status, bytes: Buffer.concat(chunks), type: response.headers.get('content-type') ?? '' };
20
+ return size > most ? { kind: 'over' } : { kind: 'got', status: response.status, bytes: Buffer.concat(chunks), type: response.headers.get('content-type') ?? '', ...((at) => (at ? { location: at } : {}))(response.headers.get('location')), ...((asks) => (asks ? { authenticate: asks } : {}))(response.headers.get('www-authenticate')) };
21
21
  }
22
22
  catch {
23
23
  return { kind: 'failed' };
24
24
  }
25
25
  };
26
26
  const loopback = new Set(['127.0.0.1', 'localhost', '[::1]']);
27
- export const published = async (url, digest, most, wait) => (URL.canParse(url) && new URL(url).protocol === 'https:'
27
+ export const published = async (url, digest, most, wait) => (URL.canParse(url) && ((at) => at.protocol === 'https:' || loopback.has(at.hostname))(new URL(url))
28
28
  ? ((got) => (got.kind === 'got' && got.status < 300 && `sha256:${createHash('sha256').update(got.bytes).digest('hex')}` === digest ? got.bytes : undefined))(await requestCapped('GET', url, {}, undefined, most, wait, 'follow')) : undefined);
29
- export const get = async (url, wait) => (URL.canParse(url) && new URL(url).protocol === 'https:' ? ((got) => (got.kind === 'got' && got.status < 300 ? got : undefined))(await requestCapped('GET', url, {}, undefined, 16_777_216, wait)) : undefined);
29
+ export const fetched = async (url, attachment, accept, most, wait) => (!URL.canParse(url) || !((at) => at.protocol === 'https:' || loopback.has(at.hostname))(new URL(url)) ? { kind: 'failed' }
30
+ : requestCapped('GET', ((extra) => (extra.size ? `${url}${url.includes('?') ? '&' : '?'}${extra}` : url))(new URLSearchParams(Object.entries(attachment?.query ?? {}))), { ...(attachment?.headers ?? {}), ...(accept ? { accept } : {}) }, undefined, most, wait));
31
+ export const get = async (url, wait) => (URL.canParse(url) && ((at) => at.protocol === 'https:' || loopback.has(at.hostname))(new URL(url)) ? ((got) => (got.kind === 'got' && got.status < 300 ? got : undefined))(await requestCapped('GET', url, {}, undefined, 16_777_216, wait)) : undefined);
30
32
  /** A body posted to an address a declaration names — as a form, or as JSON where the declaration's protocol asks it — as one request under the same rules: encrypted unless it is this device, capped, never redirected. */
31
33
  export const post = async (url, body, most, wait, as = 'form') => {
32
34
  if (!URL.canParse(url) || !((at) => at.protocol === 'https:' || loopback.has(at.hostname))(new URL(url)))
@@ -1,28 +1,36 @@
1
1
  import type { Said } from '../core/lock.ts';
2
2
  import type { Outcome } from '../core/answer.ts';
3
+ import { cooled } from './warm.ts';
3
4
  import type { Printed } from './process.ts';
4
5
  export type { Printed };
5
6
  export declare const limits: {
6
7
  wait: number;
7
8
  most: number;
8
9
  };
9
- /** bound run as a process in a folder, beside head and never blocking it, within the wait and the bytes head's lock gives it and stopped when the asker cancels. */
10
+ /** bound run as a process in a folder, beside head and never blocking it, within the wait and the bytes head's lock gives it and stopped when the asker cancels: one started ahead when head holds one, else one started now. */
10
11
  export declare const run: (name: string, cwd: string, args: readonly string[], env?: Readonly<Record<string, string>>, signal?: AbortSignal) => Promise<Printed | undefined>;
12
+ /** bound started ahead of the first ask of a call, as many times as head's lock keeps it started; resolved once each is ready. */
13
+ export declare const warm: (name: string) => Promise<unknown>;
14
+ export { cooled };
11
15
  export type Folded = {
12
16
  readonly lines: readonly Said[];
13
17
  } | {
14
18
  readonly refused: string;
15
19
  };
16
20
  /**
17
- * The lines a place stands on, from bound's public views alone: its folded normative standing (the lines view), and from
21
+ * The lines a place stands on, from bound's public views alone: its folded normative standing (the lines view) — its
22
+ * policy, all that a reader of its pins, keys and views needs, read alone where nothing else is — and from
18
23
  * its authenticated evidence what this device's own keys signed. Receipts are history, each kept as it was made; any other
19
24
  * line of a device key, the person's own attestation, stands only while it is the one line of its scope and measure,
20
25
  * unwithdrawn and beside no line of the place's own: anything else needs a fold bound does not publish, and is refused by
21
26
  * name. A place whose land prints no act result is refused before anything lands in it. Any refusal of bound is no standing.
22
27
  */
28
+ export declare const policyAt: (name: string, place: string, signal?: AbortSignal) => Promise<Folded>;
23
29
  export declare const standingAt: (name: string, place: string, devicesOf: (lines: readonly Said[]) => readonly string[], isHistory: (line: Said) => boolean, signal?: AbortSignal) => Promise<Folded>;
24
30
  export declare const sign: (name: string, place: string, lot: readonly string[], key: string, signer: string, env: Readonly<Record<string, string>>, signal?: AbortSignal) => Promise<Outcome>;
25
31
  /** A lot signed and landed in one process, its result read from what bound printed of the committed act: the records it admitted, never the receipt beside them; a place that declares no act result is refused by name. */
32
+ /** A place's receipts rendered beside its lock, so bound alone checks the place: nothing when they closed, else why they did not. */
33
+ export declare const closing: (name: string, place: string, signal?: AbortSignal) => Promise<string | undefined>;
26
34
  export declare const landAct: (name: string, place: string, lot: readonly string[], key: string, signer: string, env: Readonly<Record<string, string>>, signal?: AbortSignal) => Promise<Outcome>;
27
35
  export declare const land: (name: string, place: string, signed: readonly string[], signal?: AbortSignal) => Promise<Outcome>;
28
36
  export declare const told: (name: string, place: string, signed: readonly string[], view: string, signal?: AbortSignal) => Promise<readonly string[]>;
@@ -47,5 +55,7 @@ export declare const words: {
47
55
  evidence: string;
48
56
  act: string;
49
57
  profile: string;
58
+ receipts: string;
59
+ refusal: string;
50
60
  };
51
61
  export declare const settledView: (name: string, place: string, view: string, signal?: AbortSignal) => Promise<Folded>;
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { parse } from '@lapxo/topos/wire';
6
6
  import { spawned } from "./process.js";
7
+ import { cooled, kept, warmed } from "./warm.js";
7
8
  const binOf = (name) => {
8
9
  try {
9
10
  const manifest = createRequire(import.meta.url).resolve(join(name, 'package.json'));
@@ -15,24 +16,38 @@ const binOf = (name) => {
15
16
  }
16
17
  };
17
18
  export const limits = { wait: 0, most: 0 };
18
- /** bound run as a process in a folder, beside head and never blocking it, within the wait and the bytes head's lock gives it and stopped when the asker cancels. */
19
- export const run = async (name, cwd, args, env = {}, signal) => ((bin) => (bin === undefined ? undefined
20
- : spawned(process.execPath, [bin, ...args], { cwd, env: { ...process.env, ...env } }, { wait: limits.wait, most: limits.most || Number.MAX_SAFE_INTEGER, ...(signal ? { signal } : {}) })))(binOf(name));
19
+ /** bound run as a process in a folder, beside head and never blocking it, within the wait and the bytes head's lock gives it and stopped when the asker cancels: one started ahead when head holds one, else one started now. */
20
+ export const run = async (name, cwd, args, env = {}, signal) => ((bin, at, bounds) => (bin === undefined ? undefined
21
+ : (warmed(bin, args, at, bounds) ?? spawned(process.execPath, [bin, ...args], at, bounds)).then(apart)))(binOf(name), { cwd, env: { ...process.env, ...env } }, { wait: limits.wait, most: limits.most || Number.MAX_SAFE_INTEGER, ...(signal ? { signal } : {}) });
22
+ /** The record bound prints after a refusal's diagnostic, read apart from it: what bound printed is kept whole, the record beside it, and why bound refused is the diagnostic's last line, never the record. */
23
+ const recordOf = (line) => { try {
24
+ return ((got) => (got?.profile === words.refusal ? got : undefined))(JSON.parse(line));
25
+ }
26
+ catch {
27
+ return undefined;
28
+ } };
29
+ const isRecord = (line) => line.startsWith('{') && recordOf(line) !== undefined;
30
+ const apart = (printed) => ((record) => (record === undefined ? printed : { ...printed, refusal: recordOf(record) }))([...printed.err].reverse().find(isRecord));
31
+ const why = (printed) => printed?.err.filter((line) => !isRecord(line)).at(-1);
32
+ /** bound started ahead of the first ask of a call, as many times as head's lock keeps it started; resolved once each is ready. */
33
+ export const warm = (name) => ((bin) => (bin === undefined ? Promise.resolve() : kept(bin)))(binOf(name));
34
+ export { cooled };
21
35
  const facts = (out) => out.flatMap((line) => ((got) => (got.kind === 'fact' ? [got.value.fields] : []))(parse(line)));
22
- const readOf = (printed) => (printed === undefined || printed.code !== 0 ? { refused: printed?.err.at(-1) ?? '' } : { lines: facts(printed.out) });
36
+ const readOf = (printed) => (printed === undefined || printed.code !== 0 ? { refused: why(printed) ?? '' } : { lines: facts(printed.out) });
23
37
  /**
24
- * The lines a place stands on, from bound's public views alone: its folded normative standing (the lines view), and from
38
+ * The lines a place stands on, from bound's public views alone: its folded normative standing (the lines view) — its
39
+ * policy, all that a reader of its pins, keys and views needs, read alone where nothing else is — and from
25
40
  * its authenticated evidence what this device's own keys signed. Receipts are history, each kept as it was made; any other
26
41
  * line of a device key, the person's own attestation, stands only while it is the one line of its scope and measure,
27
42
  * unwithdrawn and beside no line of the place's own: anything else needs a fold bound does not publish, and is refused by
28
43
  * name. A place whose land prints no act result is refused before anything lands in it. Any refusal of bound is no standing.
29
44
  */
45
+ export const policyAt = async (name, place, signal) => ((folded) => ('refused' in folded || folded.lines.some((line) => line.scope === words.profile) ? folded
46
+ : { refused: `REFUSE·act the place declares no ${words.profile}: land would print nothing of what it admits, so head lands nothing there; its root can land the local-act@1 profile and view/act` }))(readOf(await run(name, place, [words.read, words.view, words.standing], {}, signal)));
30
47
  export const standingAt = async (name, place, devicesOf, isHistory, signal) => {
31
- const folded = readOf(await run(name, place, [words.read, words.view, words.standing], {}, signal));
48
+ const folded = await policyAt(name, place, signal);
32
49
  if ('refused' in folded)
33
50
  return folded;
34
- if (!folded.lines.some((line) => line.scope === words.profile))
35
- return { refused: `REFUSE·act the place declares no ${words.profile}: land would print nothing of what it admits, so head lands nothing there; its root can land the local-act@1 profile and view/act` };
36
51
  const evidence = readOf(await run(name, place, [words.read, words.view, words.evidence], {}, signal));
37
52
  if ('refused' in evidence)
38
53
  return evidence;
@@ -43,16 +58,18 @@ export const standingAt = async (name, place, devicesOf, isHistory, signal) => {
43
58
  : { refused: `REFUSE·standing ${unfolded.by} attests ${key(unfolded)} more than once, withdrawn, or beside the place's own line: folding a device key's lines is bound's, and bound publishes no view of it` };
44
59
  };
45
60
  const lotFile = (lines) => ((at) => { writeFileSync(at, `${lines.join('\n')}\n`); return at; })(join(mkdtempSync(join(tmpdir(), 'head-lot-')), 'lot.bound'));
46
- export const sign = async (name, place, lot, key, signer, env, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: got.out.filter((line) => parse(line).kind === 'fact') } : { refused: got?.err.at(-1) ?? '' }))(await run(name, place, [words.sign, lotFile(lot), words.key, key, words.signer, signer], env, signal));
61
+ export const sign = async (name, place, lot, key, signer, env, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: got.out.filter((line) => parse(line).kind === 'fact') } : { refused: why(got) ?? '' }))(await run(name, place, [words.sign, lotFile(lot), words.key, key, words.signer, signer], env, signal));
47
62
  /** A lot signed and landed in one process, its result read from what bound printed of the committed act: the records it admitted, never the receipt beside them; a place that declares no act result is refused by name. */
63
+ /** A place's receipts rendered beside its lock, so bound alone checks the place: nothing when they closed, else why they did not. */
64
+ export const closing = async (name, place, signal) => ((got) => (got !== undefined && got.code === 0 ? undefined : why(got) ?? 'nothing printed'))(await run(name, place, [words.read, words.view, words.receipts], {}, signal));
48
65
  export const landAct = async (name, place, lot, key, signer, env, signal) => ((got) => {
49
66
  if (got === undefined || got.code !== 0)
50
- return { refused: got?.err.at(-1) ?? '' };
67
+ return { refused: why(got) ?? '' };
51
68
  const at = got.out.findIndex((line) => line.startsWith(words.act));
52
69
  return at < 0 ? { refused: `REFUSE·act bound printed no act result, and the place declares ${words.profile}: what was admitted is bound's to say` }
53
70
  : { lines: got.out.slice(at + 1).filter((line) => parse(line).kind === 'fact').slice(0, -1) };
54
71
  })(await run(name, place, [words.land, lotFile(lot), words.key, key, words.signer, signer], env, signal));
55
- export const land = async (name, place, signed, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: signed } : { refused: got?.err.at(-1) ?? '' }))(await run(name, place, [words.land, lotFile(signed)], {}, signal));
72
+ export const land = async (name, place, signed, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: signed } : { refused: why(got) ?? '' }))(await run(name, place, [words.land, lotFile(signed)], {}, signal));
56
73
  export const told = async (name, place, signed, view, signal) => ((got) => [...(got?.out ?? []), ...(got?.err ?? [])])(await run(name, place, [words.read, words.view, view, lotFile(signed)], {}, signal));
57
74
  export const pinnedAt = async (name, place, file, signal) => {
58
75
  const printed = await run(name, place, [words.read, file], {}, signal);
@@ -63,6 +80,6 @@ export const pinnedAt = async (name, place, file, signal) => {
63
80
  const bytes = printed.out.filter((line) => line.startsWith(words.topos)).map((line) => `${line.slice(words.topos.length)}\n`).join('');
64
81
  return pin === undefined || !bytes ? undefined : { pin, bytes, lines: read.lines };
65
82
  };
66
- export const founded = async (name, place, key, signer, env, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: got.out } : { refused: got?.err.at(-1) ?? '' }))(await run(name, place, [words.land, words.key, key, words.signer, signer], env, signal));
67
- export const words = { read: '', view: '', sign: '', land: '', key: '', signer: '', pin: '', topos: '', standing: '', evidence: '', act: '', profile: '' };
83
+ export const founded = async (name, place, key, signer, env, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: got.out } : { refused: why(got) ?? '' }))(await run(name, place, [words.land, words.key, key, words.signer, signer], env, signal));
84
+ export const words = { read: '', view: '', sign: '', land: '', key: '', signer: '', pin: '', topos: '', standing: '', evidence: '', act: '', profile: '', receipts: '', refusal: '' };
68
85
  export const settledView = async (name, place, view, signal) => readOf(await run(name, place, [words.read, words.view, view], {}, signal));
@@ -0,0 +1,34 @@
1
+ import type { Attachment } from '../access/contract.ts';
2
+ import type { Context } from '../core/answer.ts';
3
+ import type { Said } from '../core/lock.ts';
4
+ import type { Call } from '../core/read.ts';
5
+ export type Read = {
6
+ readonly bytes: Uint8Array;
7
+ readonly kind: string;
8
+ };
9
+ export declare const kindOf: (at: string, type?: string) => string;
10
+ /**
11
+ * A document the person names: a file inside the place or a folder the client shares, or an encrypted address read with
12
+ * nothing attached — signed or expiring as it may be — and, when its digest is named, followed wherever it moves to the
13
+ * bytes that digest names, and only to them. Anything else is not read. Its kind is named, never read for: its file's
14
+ * extension, its address's, or the media type it was served as; bytes that say none are octet-stream.
15
+ */
16
+ export declare const documentOf: (context: Context, place: string, at: string, named?: string) => Promise<Read | undefined>;
17
+ /** A document read with the access a source the place connected holds: sent to that source's own origin alone, never redirected with it; a move is followed with nothing attached, only to the bytes its named digest names. */
18
+ export declare const fetchedAs: (context: Context, at: string, via: string, got: {
19
+ readonly plan: Call;
20
+ readonly attachment: Attachment | undefined;
21
+ }, named: string, accept: string | undefined) => Promise<Read | {
22
+ readonly said: readonly string[];
23
+ }>;
24
+ /**
25
+ * Every byte a package's lines name, by the path each names: read where a line of the same scope says — an encrypted
26
+ * address, signed or expiring, read with nothing attached and only to the bytes its digest names — or else beside the
27
+ * package's lock, in its folder or at its address. A path outside the package is never read. And what a place laid
28
+ * for a source it connected — its document and the overlay beside it — by the path the place keeps them at. And where
29
+ * an origin says its documents are: the catalog it publishes at its well-known address (RFC 9727), then the paths head's
30
+ * lock names, each once.
31
+ */
32
+ export declare const besideOf: (context: Context, document: string, lines: readonly Said[]) => Promise<ReadonlyMap<string, Uint8Array | undefined>>;
33
+ export declare const describedAt: (context: Context, origin: string) => Promise<readonly string[]>;
34
+ export declare const laidOf: (context: Context, place: string, source: string) => readonly (readonly [string, Uint8Array])[];
@@ -0,0 +1,64 @@
1
+ import { dirname, extname, join } from 'node:path';
2
+ import { within } from "../core/answer.js";
3
+ import { members, valueOf } from "../core/lock.js";
4
+ import { sayIn } from "../core/surface.js";
5
+ const limits = (context) => ['adapter/request/wait', 'adapter/request/document-bytes'].map((k) => Number(valueOf(context.lock, k).split('..')[1]));
6
+ export const kindOf = (at, type = '') => (extname(URL.canParse(at) ? new URL(at).pathname : at).slice(1)
7
+ || (type.split(';')[0].split('/')[1] ?? '').split('+').at(-1).replace(/^x-/, '') || 'octet-stream').toLowerCase();
8
+ /**
9
+ * A document the person names: a file inside the place or a folder the client shares, or an encrypted address read with
10
+ * nothing attached — signed or expiring as it may be — and, when its digest is named, followed wherever it moves to the
11
+ * bytes that digest names, and only to them. Anything else is not read. Its kind is named, never read for: its file's
12
+ * extension, its address's, or the media type it was served as; bytes that say none are octet-stream.
13
+ */
14
+ export const documentOf = async (context, place, at, named = '') => {
15
+ const [wait, most] = limits(context), transport = URL.canParse(at) ? await context.adapter(at) : undefined;
16
+ if (URL.canParse(at))
17
+ return named ? ((bytes) => bytes && { bytes, kind: kindOf(at) })(await transport?.published(at, named, most, wait)) : ((got) => got && { bytes: got.bytes, kind: kindOf(at, got.type) })(await transport?.get(at, wait));
18
+ return ((bytes) => bytes && { bytes, kind: kindOf(at) })((await within(context, place, at)) ? context.files.take(at) : undefined);
19
+ };
20
+ /** A document read with the access a source the place connected holds: sent to that source's own origin alone, never redirected with it; a move is followed with nothing attached, only to the bytes its named digest names. */
21
+ export const fetchedAs = async (context, at, via, got, named, accept) => {
22
+ const [say, [wait, most]] = [sayIn(context.lock), limits(context)];
23
+ if (!URL.canParse(at))
24
+ return { said: [say('document-unread', { document: at })] };
25
+ if (new URL(at).origin !== new URL(got.plan.origin).origin)
26
+ return { said: [say('fetch-cross-origin', { document: at, via })] };
27
+ const transport = await context.adapter(at), answer = transport ? await transport.fetched(at, got.attachment, accept, most, wait) : undefined;
28
+ if (answer?.kind !== 'got')
29
+ return { said: [say('document-unread', { document: at })] };
30
+ if (answer.status >= 300 && answer.status < 400 && answer.location !== undefined) {
31
+ if (!named)
32
+ return { said: [say('fetch-redirect', { document: at })] };
33
+ return ((bytes) => (bytes ? { bytes, kind: kindOf(at) } : { said: [say('document-unread', { document: at })] }))(await transport.published(new URL(answer.location, at).href, named, most, wait));
34
+ }
35
+ return answer.status < 300 ? { bytes: answer.bytes, kind: kindOf(at, answer.type) } : { said: [say('fetch-status', { document: at, via, status: String(answer.status) })] };
36
+ };
37
+ /**
38
+ * Every byte a package's lines name, by the path each names: read where a line of the same scope says — an encrypted
39
+ * address, signed or expiring, read with nothing attached and only to the bytes its digest names — or else beside the
40
+ * package's lock, in its folder or at its address. A path outside the package is never read. And what a place laid
41
+ * for a source it connected — its document and the overlay beside it — by the path the place keeps them at. And where
42
+ * an origin says its documents are: the catalog it publishes at its well-known address (RFC 9727), then the paths head's
43
+ * lock names, each once.
44
+ */
45
+ export const besideOf = async (context, document, lines) => {
46
+ const [wait, most] = limits(context), read = new Map();
47
+ for (const l of lines.filter((one) => one.needs && one.measure === 'digest' && !one.needs.split('/').includes('..') && !one.needs.startsWith('/'))) {
48
+ const at = lines.find((o) => o.scope === l.scope && o.measure === 'id')?.value ?? (URL.canParse(document) ? new URL(l.needs, document).href : join(dirname(document), l.needs));
49
+ read.set(l.needs, URL.canParse(at) ? await (await context.adapter(at))?.published(at, l.value ?? '', most, wait) : context.files.take(at));
50
+ }
51
+ return read;
52
+ };
53
+ export const describedAt = async (context, origin) => {
54
+ const [wait] = limits(context), catalog = await (await context.adapter(origin))?.get(new URL(valueOf(context.lock, 'use/catalog'), origin).href, wait), rel = valueOf(context.lock, 'use/catalog-rel');
55
+ const listed = (() => { try {
56
+ return JSON.parse(new TextDecoder().decode(catalog?.bytes ?? new Uint8Array())).linkset?.flatMap((l) => (l[rel] ?? []).flatMap((d) => d.href ?? [])) ?? [];
57
+ }
58
+ catch {
59
+ return [];
60
+ } })();
61
+ return [...new Set([...listed, ...members(valueOf(context.lock, 'use/documents'))].flatMap((at) => (URL.canParse(at, origin) ? [new URL(at, origin).href] : [])))];
62
+ };
63
+ export const laidOf = (context, place, source) => ((at) => context.files.list(place, at).filter((f) => new RegExp(`^${source}\\.(overlay\\.)?[^.]+$`).test(f))
64
+ .map((f) => [`${at}/${f}`, context.files.take(join(place, at, f)) ?? new Uint8Array()]))(valueOf(context.lock, 'place/connected'));
@@ -1,7 +1,8 @@
1
1
  export interface KeyStore {
2
2
  readonly read: (account: string) => string | undefined;
3
3
  readonly write: (account: string, secret: string) => void;
4
+ readonly made?: (account: string) => string | undefined;
4
5
  }
5
6
  export declare const plain: (word: string) => string;
6
- /** The secret store this host keeps head's keys in, by the name its lock gives the platform's: one adapter each, every secret crossing a command's stdin and stdout only. */
7
+ /** The secret store this host keeps head's keys in, by the name its lock gives the platform's: one adapter each, every secret crossing a command's stdin and stdout only; a key it is asked for by its account is made there once, and only its public half handed on. */
7
8
  export declare const keychain: (service: string, kind: string) => KeyStore;
@@ -1,11 +1,12 @@
1
1
  import { store as credentialManager } from "./stores/credential-manager.js";
2
2
  import { store as keychainOfMac } from "./stores/keychain.js";
3
3
  import { store as secretTool } from "./stores/secret-tool.js";
4
+ import { newKey, publicOf } from "./keys.js";
4
5
  export const plain = (word) => { if (!/^[\w.@/+=-]+$/.test(word))
5
6
  throw Error('REFUSE·keychain a name holds more than word characters'); return word; };
6
- /** The secret store this host keeps head's keys in, by the name its lock gives the platform's: one adapter each, every secret crossing a command's stdin and stdout only. */
7
+ /** The secret store this host keeps head's keys in, by the name its lock gives the platform's: one adapter each, every secret crossing a command's stdin and stdout only; a key it is asked for by its account is made there once, and only its public half handed on. */
7
8
  export const keychain = (service, kind) => ((made) => {
8
9
  if (made === undefined)
9
10
  throw Error(`REFUSE·keychain ${kind} is no store head carries`);
10
- return made(service);
11
+ return ((store) => ({ ...store, made: (account) => ((secret) => secret && publicOf(secret))(store.read(account) ?? ((key) => { store.write(account, key.secret); return store.read(account) === key.secret ? key.secret : undefined; })(newKey())) }))(made(service));
11
12
  })({ keychain: keychainOfMac, 'secret-tool': secretTool, 'credential-manager': credentialManager }[kind]);
@@ -8,3 +8,4 @@ export declare const layAt: (place: string, rel: string, bytes: Uint8Array) => v
8
8
  export declare const takeAt: (path: string) => Uint8Array | undefined;
9
9
  export declare const hasAt: (place: string, rel: string) => boolean;
10
10
  export declare const dropAt: (place: string, rel: string) => void;
11
+ export declare const listAt: (place: string, rel: string) => readonly string[];
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join, resolve, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { LOCK } from '@lapxo/topos/wire';
@@ -24,3 +24,9 @@ catch {
24
24
  } };
25
25
  export const hasAt = (place, rel) => existsSync(resolve(place, rel));
26
26
  export const dropAt = (place, rel) => rmSync(inside(place, rel), { force: true });
27
+ export const listAt = (place, rel) => { try {
28
+ return readdirSync(inside(place, rel));
29
+ }
30
+ catch {
31
+ return [];
32
+ } };
@@ -1,13 +1,20 @@
1
+ import type { ChildProcess } from 'node:child_process';
1
2
  export interface Printed {
2
3
  readonly code: number;
3
4
  readonly out: readonly string[];
4
5
  readonly err: readonly string[];
6
+ readonly refusal?: Readonly<Record<string, string>>;
5
7
  }
6
8
  export interface Bounds {
7
9
  readonly wait: number;
8
10
  readonly most: number;
9
11
  readonly signal?: AbortSignal;
10
12
  }
13
+ /** A process head starts in a group of its own, so stopping it stops everything it started. */
14
+ export declare const grouped: (command: string, args: readonly string[], at: {
15
+ readonly cwd?: string;
16
+ readonly env?: NodeJS.ProcessEnv;
17
+ }, pipes?: number) => ChildProcess;
11
18
  /**
12
19
  * A process run beside head, never blocking it: what it printed, out and err, line by line, and its exit, which is
13
20
  * transport and never the verdict. Its whole group is stopped when the wait runs out, when it prints more bytes than it
@@ -18,3 +25,5 @@ export declare const spawned: (command: string, args: readonly string[], at: {
18
25
  readonly env?: NodeJS.ProcessEnv;
19
26
  readonly input?: string;
20
27
  }, bounds: Bounds) => Promise<Printed>;
28
+ /** What a process head already started prints from now on, within the same bounds. */
29
+ export declare const watched: (child: ChildProcess, bounds: Bounds) => Promise<Printed>;
@@ -2,13 +2,16 @@ import { spawn } from 'node:child_process';
2
2
  const running = new Set();
3
3
  process.once('exit', () => { for (const stop of running)
4
4
  stop(); });
5
+ /** A process head starts in a group of its own, so stopping it stops everything it started. */
6
+ export const grouped = (command, args, at, pipes = 3) => spawn(command, [...args], { cwd: at.cwd, env: at.env, stdio: Array(pipes).fill('pipe'), detached: process.platform !== 'win32', windowsHide: true });
5
7
  /**
6
8
  * A process run beside head, never blocking it: what it printed, out and err, line by line, and its exit, which is
7
9
  * transport and never the verdict. Its whole group is stopped when the wait runs out, when it prints more bytes than it
8
10
  * may, when the asker cancels or when head exits; the exit is then -1 and the cause is err's last line.
9
11
  */
10
- export const spawned = (command, args, at, bounds) => new Promise((resolve) => {
11
- const child = spawn(command, [...args], { cwd: at.cwd, env: at.env, stdio: ['pipe', 'pipe', 'pipe'], detached: process.platform !== 'win32', windowsHide: true });
12
+ export const spawned = (command, args, at, bounds) => ((child) => { child.stdin.on('error', () => undefined).end(at.input ?? ''); return watched(child, bounds); })(grouped(command, args, at));
13
+ /** What a process head already started prints from now on, within the same bounds. */
14
+ export const watched = (child, bounds) => new Promise((resolve) => {
12
15
  const [out, err] = [[], []];
13
16
  let [printed, cause, done] = [0, undefined, false];
14
17
  const kill = () => { try {
@@ -41,7 +44,6 @@ export const spawned = (command, args, at, bounds) => new Promise((resolve) => {
41
44
  child.stderr.on('data', take(err));
42
45
  child.on('error', (error) => { cause ??= error.message; finish(-1); });
43
46
  child.on('close', (code) => finish(code ?? -1));
44
- child.stdin.on('error', () => undefined).end(at.input ?? '');
45
47
  if (signal.aborted)
46
48
  cancel();
47
49
  else
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
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 { readFileSync } from 'node:fs';
10
+ import { Socket } from 'node:net';
11
+ import { pathToFileURL } from 'node:url';
12
+ /**
13
+ * A process started before it is asked: it loads what the program it will run imports, says it is ready on its fourth
14
+ * pipe, and waits there for one ask, the folder, the arguments and the variables a process started then would have been
15
+ * given. It takes them as its own, closes that pipe and runs the program once, as that process would have. An ask that
16
+ * never comes ends it.
17
+ */
18
+ const [program] = process.argv.slice(2);
19
+ const url = pathToFileURL(program), pipe = new Socket({ fd: 3, readable: true, writable: true });
20
+ const asked = new Promise((resolve) => { let text = ''; pipe.setEncoding('utf8').on('data', (chunk) => { text += chunk; }).on('error', () => undefined).on('close', () => resolve(text)); });
21
+ await Promise.all([...readFileSync(program, 'utf8').matchAll(/^import [^'"]*['"](\.{1,2}\/[^'"]+)['"];?$/gm)].map(([, path]) => import(__rewriteRelativeImportExtension(new URL(path, url).href)).catch(() => undefined)));
22
+ pipe.write('ready\n');
23
+ const text = await asked;
24
+ if (!text)
25
+ process.exit(0);
26
+ const { cwd, args, env } = JSON.parse(text);
27
+ for (const name of Object.keys(process.env))
28
+ if (!(name in env))
29
+ delete process.env[name];
30
+ Object.assign(process.env, env);
31
+ process.chdir(cwd);
32
+ process.argv = [process.argv[0], program, ...args];
33
+ await import(__rewriteRelativeImportExtension(url.href));
@@ -0,0 +1,130 @@
1
+ import type { Said } from '../core/lock.ts';
2
+ export interface Field {
3
+ readonly name: string;
4
+ readonly about: string;
5
+ readonly type: string;
6
+ readonly required: boolean;
7
+ }
8
+ export interface Tool {
9
+ readonly name: string;
10
+ readonly about: string;
11
+ readonly fields: readonly Field[];
12
+ readonly hints: readonly string[];
13
+ }
14
+ export interface Resource {
15
+ readonly name: string;
16
+ readonly about: string;
17
+ readonly template: string;
18
+ readonly mime: string;
19
+ }
20
+ export interface Prompt {
21
+ readonly name: string;
22
+ readonly about: string;
23
+ readonly text: string;
24
+ readonly line: string;
25
+ readonly fields: readonly Field[];
26
+ }
27
+ export interface Surface {
28
+ readonly name: string;
29
+ readonly version: string;
30
+ readonly tools: readonly Tool[];
31
+ readonly catalogue: readonly Tool[];
32
+ readonly resources: readonly Resource[];
33
+ readonly prompts: readonly Prompt[];
34
+ }
35
+ /**
36
+ * Everything a client is offered, read off the lock alone, each with the words a model sees: the tools surface/tools
37
+ * names, out of the catalogue the tool lines are (every one of them where it names none), its resources and prompts.
38
+ */
39
+ export declare const surfaceOf: (lock: readonly Said[]) => Surface;
40
+ export declare const refusedOn: (lock: readonly Said[], platform: string) => string | undefined;
41
+ /**
42
+ * The fields a URI gives a resource template, or none when it is not one of its URIs: {name} matches one path step,
43
+ * {?a,b} names query fields any of which may be absent, in any order, each decoded.
44
+ */
45
+ export declare const matchTemplate: (template: string, uri: string) => Readonly<Record<string, string>> | undefined;
46
+ export interface Cell {
47
+ readonly code: string;
48
+ readonly state: string;
49
+ readonly next: {
50
+ readonly tool: string;
51
+ readonly args: Readonly<Record<string, string>>;
52
+ } | {
53
+ readonly none: string;
54
+ };
55
+ readonly fields: Readonly<Record<string, string>>;
56
+ readonly head?: string;
57
+ }
58
+ /** A said code as the cell head answers with: its state and next from the lock, the next's arguments read from the cell's own fields; a bound refusal a field carries keeps bound's code, head's own beside it. */
59
+ export declare const cellOf: (lock: readonly Said[], key: string, given: Readonly<Record<string, string | undefined>>) => Cell;
60
+ export declare const cellLine: (cell: Cell) => string;
61
+ /** An answer as cells and the lines bound printed beside them, read back from what head said: the shape every tool result has. */
62
+ export declare const shapeOf: (answer: readonly string[]) => {
63
+ readonly cells: readonly Cell[];
64
+ readonly lines: readonly string[];
65
+ };
66
+ /** The one output schema every tool declares: cells in the states the lock names, and the lines bound printed. */
67
+ export declare const shapeSchema: (lock: readonly Said[]) => {
68
+ type: "object";
69
+ required: string[];
70
+ properties: {
71
+ cells: {
72
+ type: string;
73
+ items: {
74
+ type: string;
75
+ required: string[];
76
+ properties: {
77
+ code: {
78
+ type: string;
79
+ };
80
+ state: {
81
+ enum: string[];
82
+ };
83
+ head: {
84
+ type: string;
85
+ };
86
+ next: {
87
+ oneOf: ({
88
+ type: string;
89
+ required: string[];
90
+ properties: {
91
+ tool: {
92
+ type: string;
93
+ };
94
+ args: {
95
+ type: string;
96
+ additionalProperties: {
97
+ type: string;
98
+ };
99
+ };
100
+ none?: undefined;
101
+ };
102
+ } | {
103
+ type: string;
104
+ required: string[];
105
+ properties: {
106
+ none: {
107
+ type: string;
108
+ };
109
+ tool?: undefined;
110
+ args?: undefined;
111
+ };
112
+ })[];
113
+ };
114
+ fields: {
115
+ type: string;
116
+ additionalProperties: {
117
+ type: string;
118
+ };
119
+ };
120
+ };
121
+ };
122
+ };
123
+ lines: {
124
+ type: string;
125
+ items: {
126
+ type: string;
127
+ };
128
+ };
129
+ };
130
+ };