@lapxo/head 0.1.5 → 0.1.7

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 (71) hide show
  1. package/README.md +6 -4
  2. package/TARGET.bound +245 -121
  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/answers/answered.d.ts +4 -0
  8. package/dist/src/answers/answered.js +22 -0
  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 +8 -13
  15. package/dist/src/core/answer.d.ts +1 -0
  16. package/dist/src/core/answer.js +1 -0
  17. package/dist/src/core/device.d.ts +3 -6
  18. package/dist/src/core/device.js +7 -10
  19. package/dist/src/core/lock.d.ts +1 -0
  20. package/dist/src/core/lock.js +2 -1
  21. package/dist/src/core/read.js +1 -2
  22. package/dist/src/core/worlds.js +2 -3
  23. package/dist/src/doors/guard.d.ts +18 -0
  24. package/dist/src/doors/guard.js +63 -0
  25. package/dist/src/doors/head.d.ts +60 -0
  26. package/dist/src/doors/head.js +136 -0
  27. package/dist/src/doors/http.d.ts +3 -0
  28. package/dist/src/doors/http.js +34 -0
  29. package/dist/src/doors/lines.d.ts +3 -0
  30. package/dist/src/doors/lines.js +21 -0
  31. package/dist/src/flows/act.d.ts +1 -5
  32. package/dist/src/flows/act.js +1 -5
  33. package/dist/src/flows/call.d.ts +2 -9
  34. package/dist/src/flows/call.js +12 -12
  35. package/dist/src/flows/device.js +4 -6
  36. package/dist/src/flows/plan.js +7 -10
  37. package/dist/src/flows/read.d.ts +1 -7
  38. package/dist/src/flows/read.js +6 -9
  39. package/dist/src/index.js +20 -114
  40. package/dist/src/links/inspect.d.ts +1 -0
  41. package/dist/src/links/inspect.js +9 -8
  42. package/dist/src/onboard/first.js +1 -1
  43. package/dist/src/places/admit.js +1 -1
  44. package/dist/src/places/found.js +6 -10
  45. package/dist/src/ports/adapters/request.d.ts +1 -0
  46. package/dist/src/ports/adapters/request.js +2 -2
  47. package/dist/src/ports/bound.d.ts +15 -18
  48. package/dist/src/ports/bound.js +33 -30
  49. package/dist/src/ports/host.d.ts +35 -0
  50. package/dist/src/ports/host.js +108 -0
  51. package/dist/src/ports/keychain.d.ts +14 -2
  52. package/dist/src/ports/keychain.js +19 -7
  53. package/dist/src/ports/scratch.d.ts +4 -0
  54. package/dist/src/ports/scratch.js +10 -0
  55. package/dist/src/signer.js +8 -7
  56. package/dist/src/tools/card.js +3 -6
  57. package/dist/src/tools/look.js +5 -5
  58. package/dist/src/use/named.d.ts +12 -0
  59. package/dist/src/use/named.js +25 -0
  60. package/dist/src/verbs/ask.d.ts +1 -9
  61. package/dist/src/verbs/ask.js +4 -11
  62. package/dist/src/verbs/card.d.ts +2 -2
  63. package/dist/src/verbs/card.js +3 -3
  64. package/dist/src/walk/context.js +3 -6
  65. package/package.json +3 -3
  66. package/dist/src/ports/signers.d.ts +0 -6
  67. package/dist/src/ports/signers.js +0 -14
  68. package/dist/src/ports/spare.d.ts +0 -1
  69. package/dist/src/ports/spare.js +0 -33
  70. package/dist/src/ports/warm.d.ts +0 -19
  71. package/dist/src/ports/warm.js +0 -45
@@ -4,5 +4,6 @@ import { type Asked, type Context } from '../core/answer.ts';
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.
7
+ * Asked why, it shows the transport it reads by: the profile's lines, laid at the place's birth by head, never the person's.
7
8
  */
8
9
  export declare const linked: (asked: Asked, context: Context) => Promise<readonly string[]>;
@@ -1,6 +1,6 @@
1
1
  import { decodeReference } from '@lapxo/bound/reference';
2
2
  import { canonical } from '@lapxo/topos/wire';
3
- import { inPlace, standingOr } from "../core/answer.js";
3
+ import { inStanding } from "../core/answer.js";
4
4
  import { members, valueOf } from "../core/lock.js";
5
5
  import { sayIn } from "../core/surface.js";
6
6
  import { linesOf } from "../core/worlds.js";
@@ -10,11 +10,12 @@ import { rootIn } from "../flows/device.js";
10
10
  * link's, and nothing shows who shared it — and what it names is shown; nothing is connected or granted, since a package
11
11
  * is received by the digest its sharer sent apart. A place that admits no content transport is handed the lines that admit
12
12
  * 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.
13
+ * Asked why, it shows the transport it reads by: the profile's lines, laid at the place's birth by head, never the person's.
13
14
  */
14
- export const linked = (asked, context) => inPlace(asked, context, async (place) => {
15
- const [say, word, link] = [sayIn(context.lock), (k) => valueOf(context.lock, k), asked['document'] ?? ''], [held, admits] = [await standingOr(context, place, context.policy), members(valueOf(context.lock, 'link/transport'))];
16
- if ('said' in held)
17
- return held.said;
15
+ export const linked = (asked, context) => inStanding(asked, context, async (place, standing) => {
16
+ const [say, word, link] = [sayIn(context.lock), (k) => valueOf(context.lock, k), asked['document'] ?? ''], admits = members(valueOf(context.lock, 'link/transport'));
17
+ if (asked['why'] === 'true')
18
+ return [...standing.filter((l) => admits.includes(l.scope ?? '') || l.scope?.startsWith('transport/')).map((l) => canonical(l)), say('why-transport', { place })];
18
19
  const lines = context.lock.filter((l) => admits.some((s) => l.scope === `birth/${s}`)).map(({ about: _about, scope, ...l }) => canonical({ ...l, scope: scope.slice('birth/'.length), at: 'policy:person/transport', by: 'target' }));
19
20
  let reference;
20
21
  try {
@@ -23,10 +24,10 @@ export const linked = (asked, context) => inPlace(asked, context, async (place)
23
24
  catch (refused) {
24
25
  return [say('link-refused', { link, why: refused instanceof Error ? refused.message : String(refused) })];
25
26
  }
26
- if (![admits[0], `transport/${new URL(reference.source).protocol.slice(0, -1)}`].every((s) => held.lines.some((l) => l.scope === s)))
27
+ if (![admits[0], `transport/${new URL(reference.source).protocol.slice(0, -1)}`].every((s) => standing.some((l) => l.scope === s)))
27
28
  return [say('transport-missing', { place, lines: lines.join('\n') })];
28
29
  const [printed, scope] = [await context.bound(place, [word('bound/read'), link]), `${word('place/needs')}/bytes/${reference.blob.slice(reference.algorithm.length + 1, reference.algorithm.length + 17)}`];
29
- const [owed, root, why] = [held.lines.find((l) => l.scope === scope && l.role === 'demands' && l.value === link), rootIn(context, place), printed?.err.filter((l) => !l.startsWith('{')).at(-1) ?? ''];
30
+ const [owed, root, why] = [standing.find((l) => l.scope === scope && l.role === 'demands' && l.value === link), rootIn(context, place), printed?.err.filter((l) => !l.startsWith('{')).at(-1) ?? ''];
30
31
  if ('said' in root)
31
32
  return root.said;
32
33
  if (printed?.code !== 0 && !why.includes(word('link/unarrived')))
@@ -38,4 +39,4 @@ export const linked = (asked, context) => inPlace(asked, context, async (place)
38
39
  const named = linesOf(new TextEncoder().encode(printed.out.filter((l) => l.startsWith('TOPOS ')).map((l) => l.slice('TOPOS '.length)).join('\n')));
39
40
  const sources = named.filter((l) => /^sources\/[^/]+$/.test(l.scope ?? '')).map((l) => l.scope.split('/')[1]);
40
41
  return [say('inspected', { source: reference.source, sources: sources.join(', ') || '—', digest: reference.digest }), say('sharer-unverified', {}), ...await settled(owed ? [canonical({ ...due, value: 'withdraw' })] : [])];
41
- });
42
+ }, context.policy);
@@ -28,7 +28,7 @@ export const firstRun = async (asked, context, then) => {
28
28
  const connected = await then(dir, true), unread = connected.some((l) => l.startsWith('said/head/document-unread ')), held = unread ? undefined : await context.policy(dir);
29
29
  if (held !== undefined && ('refused' in held || !held.lines.some((l) => l.scope === `uses/${source}`)))
30
30
  return [...born, ...connected];
31
- const line = (scope, measure, value, role = 'writes') => canonical({ at: 'policy:head/first-run', by: 'target', form: 'alphabet', measure, role, scope, value });
31
+ const line = (scope, measure, value, role = 'writes') => canonical({ at: 'policy:person/grant', by: 'target', form: 'alphabet', measure, role, scope, value });
32
32
  const owed = unread ? [line(`${word('place/needs')}/bytes/${source}`, 'id', document, 'demands')] : [];
33
33
  const landed = await context.landRoot(dir, [line(word('grant/family'), 'sources', source), line(word('grant/family'), 'until', until), ...owed], root.account);
34
34
  if ('refused' in landed)
@@ -26,7 +26,7 @@ export const admitted = (asked, context) => inPlace(asked, context, async (place
26
26
  const refused = await unless(context, say('elicit-admit', { name, place, coverage }));
27
27
  if (refused)
28
28
  return refused;
29
- const lot = admissionOf(context.lock, key, 'head', { id: name, coverage, at: `policy:head/admit/${new Date(context.now()).toISOString()}` });
29
+ const lot = admissionOf(context.lock, key, 'head', { id: name, coverage, at: `policy:person/admit/${new Date(context.now()).toISOString()}` });
30
30
  const landed = await context.landRoot(place, lot, root.account);
31
31
  return 'refused' in landed ? [say('not-landed', { why: landed.refused })] : [say('admitted', { name, place, coverage }), ...landed.lines];
32
32
  });
@@ -2,19 +2,15 @@ import { isAbsolute, basename } from 'node:path';
2
2
  import { canonical, LOCK } from '@lapxo/topos/wire';
3
3
  import { unless } from "../core/answer.js";
4
4
  import { admissionOf } from "../core/device.js";
5
- import { valueOf } from "../core/lock.js";
5
+ import { members, valueOf } from "../core/lock.js";
6
6
  import { deviceKey } from "../flows/device.js";
7
7
  import { walkable } from "../walk/context.js";
8
8
  import { fill, sayIn } from "../core/surface.js";
9
- /**
10
- * The lines a place is born with, every one drafted by target: its root, by the public half of a key kept only in the
11
- * keychain and the signer that reaches it; this device's keys admitted; and the prelude head's lock declares under
12
- * birth/ — the wire, the regions and views a world is read through, and the lifetime every reader is bounded by.
13
- */
14
- const birthOf = (lock, root, head, person, place) => [
15
- ...[['class', 'authorize'], ['coverage', '*'], ['signer', valueOf(lock, 'device/head/signer')], ['public-key', root]].map(([measure, value]) => canonical({ at: 'policy:head/birth', by: 'target', form: 'alphabet', measure: measure, role: 'writes', scope: valueOf(lock, 'founding/root'), value: value })),
9
+ /** Found the place from the declared prelude; personal lines retain the person's provenance. */
10
+ const birthOf = (lock, root, head, person, place, mine = `${valueOf(lock, 'founding/person-at')}birth`) => [
11
+ ...[['class', 'authorize'], ['coverage', '*'], ['signer', valueOf(lock, 'device/head/signer')], ['public-key', root]].map(([measure, value]) => canonical({ at: mine, by: 'target', form: 'alphabet', measure: measure, role: 'writes', scope: valueOf(lock, 'founding/root'), value: value })),
16
12
  ...admissionOf(lock, head, 'head'), ...admissionOf(lock, person, 'person'),
17
- ...lock.filter((l) => l.scope?.startsWith('birth/')).map(({ about: _about, scope, ...line }) => canonical({ ...line, value: fill(line.value ?? '', { place }), scope: scope.slice('birth/'.length), at: 'policy:head/birth', by: 'target' })),
13
+ ...lock.filter((l) => l.scope?.startsWith('birth/')).map(({ about: _about, scope, ...line }) => ((s) => canonical({ ...line, value: fill(line.value ?? '', { place }), scope: s, at: members(valueOf(lock, 'founding/person')).includes(s) ? mine : 'policy:head/birth', by: 'target' }))(scope.slice('birth/'.length))),
18
14
  ];
19
15
  /** A place founded in a folder: a root key made in the keychain under the place's name — never a file — the birth lines written, and the first land signed by that root. */
20
16
  const founded = async (context, dir, name) => {
@@ -22,7 +18,7 @@ const founded = async (context, dir, name) => {
22
18
  const [head, person] = [deviceKey(context, 'head'), deviceKey(context, 'person')];
23
19
  if (head === undefined || person === undefined)
24
20
  return { refused: 'no-keychain' };
25
- if (context.keys.read(account) !== undefined)
21
+ if (context.keys.held?.(account) !== undefined)
26
22
  return { refused: 'place-taken' };
27
23
  const root = context.keys.made?.(account);
28
24
  if (root === undefined)
@@ -12,6 +12,7 @@ export type Fetched = {
12
12
  readonly kind: 'over';
13
13
  } | {
14
14
  readonly kind: 'failed';
15
+ readonly why?: string;
15
16
  };
16
17
  export type Prepared = {
17
18
  readonly key: string;
@@ -19,8 +19,8 @@ const requestCapped = async (method, url, headers, body, most, wait, redirect =
19
19
  }
20
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
- catch {
23
- return { kind: 'failed' };
22
+ catch (error) {
23
+ return { kind: 'failed', why: String(error?.cause?.code ?? error?.name ?? '') };
24
24
  }
25
25
  };
26
26
  const loopback = new Set(['127.0.0.1', 'localhost', '[::1]']);
@@ -1,38 +1,35 @@
1
- import type { Said } from '../core/lock.ts';
1
+ import { type Said } from '../core/lock.ts';
2
2
  import type { Outcome } from '../core/answer.ts';
3
- import { cooled } from './warm.ts';
3
+ import type { portOf } from './keychain.ts';
4
4
  import type { Printed } from './process.ts';
5
5
  export type { Printed };
6
6
  export declare const limits: {
7
7
  wait: number;
8
8
  most: number;
9
9
  };
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
+ /** bound asked 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: a place or one of its views read through the place's fold host, else a process started now, as for a document, a lot told or a sign: a host is always handed its place first, and bound folds a document only when it is named first; what it read from the temporary folder is removed once it ends, however it ends. */
11
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 };
15
12
  export type Folded = {
16
13
  readonly lines: readonly Said[];
17
14
  } | {
18
15
  readonly refused: string;
19
16
  };
20
- /**
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
23
- * its authenticated evidence what this device's own keys signed. Receipts are history, each kept as it was made; any other
24
- * line of a device key, the person's own attestation, stands only while it is the one line of its scope and measure,
25
- * unwithdrawn and beside no line of the place's own: anything else needs a fold bound does not publish, and is refused by
26
- * name. A place whose land prints no act result is refused before anything lands in it. Any refusal of bound is no standing.
27
- */
17
+ /** Read normative standing plus authenticated device evidence; ambiguous device history refuses. */
28
18
  export declare const policyAt: (name: string, place: string, signal?: AbortSignal) => Promise<Folded>;
29
19
  export declare const standingAt: (name: string, place: string, devicesOf: (lines: readonly Said[]) => readonly string[], isHistory: (line: Said) => boolean, signal?: AbortSignal) => Promise<Folded>;
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>;
20
+ /** A sign or a land: in a host of its own for this one call, signed by head's signer through the port of the key it names, the lot handed in as the host's input. */
21
+ type Port = ReturnType<typeof portOf>;
22
+ export declare const sign: (_name: string, place: string, lot: readonly string[], key: string, signer: string, port: Port, signal?: AbortSignal) => Promise<Outcome>;
31
23
  /** 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
24
  /** A place's receipts rendered beside its lock, so bound alone checks the place: nothing when they closed, else why they did not. */
33
25
  export declare const closing: (name: string, place: string, signal?: AbortSignal) => Promise<string | undefined>;
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>;
35
- export declare const land: (name: string, place: string, signed: readonly string[], signal?: AbortSignal) => Promise<Outcome>;
26
+ /** A place's own lines in the person's words, beside its lock, the text they read and share: each line head lands at the person's at, drafted with no signature, gone with its withdraw. Bound reads the file as it reads the place, so it is written with the land, never while a fold runs. Set from head's lock. */
27
+ export declare const birth: {
28
+ at: string;
29
+ file: string;
30
+ }, birthed: (place: string, lot: readonly string[]) => void;
31
+ export declare const landAct: (_name: string, place: string, lot: readonly string[], key: string, signer: string, port: Port, signal?: AbortSignal) => Promise<Outcome>;
32
+ export declare const land: (_name: string, place: string, signed: readonly string[], signal?: AbortSignal) => Promise<Outcome>;
36
33
  export declare const told: (name: string, place: string, signed: readonly string[], view: string, signal?: AbortSignal) => Promise<readonly string[]>;
37
34
  export declare const pinnedAt: (name: string, place: string, file: string, signal?: AbortSignal) => Promise<{
38
35
  readonly pin: string;
@@ -41,7 +38,7 @@ export declare const pinnedAt: (name: string, place: string, file: string, signa
41
38
  } | {
42
39
  readonly refused: string;
43
40
  } | undefined>;
44
- export declare const founded: (name: string, place: string, key: string, signer: string, env: Readonly<Record<string, string>>, signal?: AbortSignal) => Promise<Outcome>;
41
+ export declare const founded: (_name: string, place: string, key: string, signer: string, port: Port, signal?: AbortSignal) => Promise<Outcome>;
45
42
  export declare const words: {
46
43
  read: string;
47
44
  view: string;
@@ -1,10 +1,11 @@
1
- import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
- import { tmpdir } from 'node:os';
4
3
  import { dirname, join } from 'node:path';
5
- import { parse } from '@lapxo/topos/wire';
4
+ import { canonical, LOCK, parse } from '@lapxo/topos/wire';
5
+ import { factsOf } from "../core/lock.js";
6
6
  import { spawned } from "./process.js";
7
- import { cooled, kept, warmed } from "./warm.js";
7
+ import { hosted, hosting, signedIn } from "./host.js";
8
+ import { cleared, scratchOf } from "./scratch.js";
8
9
  import { folded } from "../ahead/fold.js";
9
10
  import { readingOf } from "./reading.js";
10
11
  const binOf = (name) => {
@@ -18,9 +19,16 @@ const binOf = (name) => {
18
19
  }
19
20
  };
20
21
  export const limits = { wait: 0, most: 0 };
21
- /** 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. */
22
- export const run = async (name, cwd, args, env = {}, signal) => ((bin, at, bounds) => (bin === undefined ? undefined
23
- : (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
+ /** bound asked 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: a place or one of its views read through the place's fold host, else a process started now, as for a document, a lot told or a sign: a host is always handed its place first, and bound folds a document only when it is named first; what it read from the temporary folder is removed once it ends, however it ends. */
23
+ export const run = async (name, cwd, args, env = {}, signal) => {
24
+ try {
25
+ return await ((bin, at, bounds) => (bin === undefined ? undefined
26
+ : (args[0] === words.read && (args.length === 1 || (args.length === 3 && args[1] === words.view)) && hosting.installed && !Object.keys(env).length ? hosted(cwd, args, signal) : 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 } : {}) });
27
+ }
28
+ finally {
29
+ cleared([...args, ...Object.values(env)]);
30
+ }
31
+ };
24
32
  /** 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. */
25
33
  const recordOf = (line) => { try {
26
34
  return ((got) => (got?.profile === words.refusal ? got : undefined))(JSON.parse(line));
@@ -31,36 +39,24 @@ catch {
31
39
  const isRecord = (line) => line.startsWith('{') && recordOf(line) !== undefined;
32
40
  const apart = (printed) => ((record) => (record === undefined ? printed : { ...printed, refusal: recordOf(record) }))([...printed.err].reverse().find(isRecord));
33
41
  const why = (printed) => printed?.err.filter((line) => !isRecord(line)).at(-1);
34
- /** 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. */
35
- export const warm = (name) => ((bin) => (bin === undefined ? Promise.resolve() : kept(bin)))(binOf(name));
36
- export { cooled };
37
- const facts = (out) => out.flatMap((line) => ((got) => (got.kind === 'fact' ? [got.value.fields] : []))(parse(line)));
38
- const readOf = (printed) => (printed === undefined || printed.code !== 0 ? { refused: why(printed) ?? '' } : { lines: facts(printed.out) });
42
+ const readOf = (printed) => (printed === undefined || printed.code !== 0 ? { refused: why(printed) ?? '' } : { lines: factsOf(printed.out) });
39
43
  /** Native identities and expected-identity results come from the same public CLI contract. Without it, read fresh. */
40
44
  const nativeView = (name, place, view, signal) => {
41
45
  const read = async (expected) => {
42
46
  if (expected === undefined)
43
47
  return readOf(await run(name, place, [words.read, words.view, view], {}, signal));
44
48
  const result = readingOf(await run(name, place, [words.read, words.view, `${words.identity}@${view}@${expected}`], {}, signal), view, expected);
45
- return 'refused' in result ? result : { lines: facts(result.text.split('\n')) };
49
+ return 'refused' in result ? result : { lines: factsOf(result.text.split('\n')) };
46
50
  };
47
51
  return !words.identity ? read() : folded(`${name}/${view}`, place, async () => { const result = readingOf(await run(name, place, [words.read, words.view, `${words.identity}@${view}`], {}, signal), view); return 'refused' in result ? undefined : result.identity; }, read);
48
52
  };
49
- /**
50
- * The lines a place stands on, from bound's public views alone: its folded normative standing (the lines view) — its
51
- * policy, all that a reader of its pins, keys and views needs, read alone where nothing else is — and from
52
- * its authenticated evidence what this device's own keys signed. Receipts are history, each kept as it was made; any other
53
- * line of a device key, the person's own attestation, stands only while it is the one line of its scope and measure,
54
- * unwithdrawn and beside no line of the place's own: anything else needs a fold bound does not publish, and is refused by
55
- * name. A place whose land prints no act result is refused before anything lands in it. Any refusal of bound is no standing.
56
- */
53
+ /** Read normative standing plus authenticated device evidence; ambiguous device history refuses. */
57
54
  export const policyAt = async (name, place, signal) => ((folded) => ('refused' in folded || folded.lines.some((line) => line.scope === words.profile) ? folded
58
55
  : { 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` }))(await nativeView(name, place, words.standing, signal));
59
56
  export const standingAt = async (name, place, devicesOf, isHistory, signal) => {
60
- const folded = await policyAt(name, place, signal);
57
+ const [folded, evidence] = await Promise.all([policyAt(name, place, signal), nativeView(name, place, words.evidence, signal)]);
61
58
  if ('refused' in folded)
62
59
  return folded;
63
- const evidence = await nativeView(name, place, words.evidence, signal);
64
60
  if ('refused' in evidence)
65
61
  return evidence;
66
62
  const ids = new Set(devicesOf(folded.lines)), own = evidence.lines.filter((line) => ids.has(line.by ?? ''));
@@ -69,19 +65,26 @@ export const standingAt = async (name, place, devicesOf, isHistory, signal) => {
69
65
  return unfolded === undefined ? { lines: [...folded.lines, ...own] }
70
66
  : { 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` };
71
67
  };
72
- const lotFile = (lines) => ((at) => { writeFileSync(at, `${lines.join('\n')}\n`); return at; })(join(mkdtempSync(join(tmpdir(), 'head-lot-')), 'lot.bound'));
73
- 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));
68
+ const lotFile = (lines) => scratchOf('head-lot-', 'lot.bound', `${lines.join('\n')}\n`);
69
+ const through = (place, verb, lot, key, signer, port, signal) => signedIn(place, [verb, ...(key ? [words.key, key, words.signer, signer] : [])], lot, new Map(key && port ? [[key, { name: signer, sign: port }]] : []), key, signal).then(apart);
70
+ export const sign = async (_name, place, lot, key, signer, port, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: got.out.filter((line) => parse(line).kind === 'fact') } : { refused: why(got) ?? '' }))(await through(place, words.sign, lot, key, signer, port, signal));
74
71
  /** 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. */
75
72
  /** A place's receipts rendered beside its lock, so bound alone checks the place: nothing when they closed, else why they did not. */
76
73
  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));
77
- export const landAct = async (name, place, lot, key, signer, env, signal) => ((got) => {
74
+ /** A place's own lines in the person's words, beside its lock, the text they read and share: each line head lands at the person's at, drafted with no signature, gone with its withdraw. Bound reads the file as it reads the place, so it is written with the land, never while a fold runs. Set from head's lock. */
75
+ export const birth = { at: '', file: '' }, birthed = (place, lot) => {
76
+ const [file, mine] = [join(place, birth.file), factsOf(lot).filter((l) => birth.file && l['at']?.startsWith(birth.at))], bare = (l) => JSON.stringify(Object.entries(l).filter(([k]) => !['about', 'value', 'sig', 'epoch', 'by'].includes(k)).sort());
77
+ if (mine.length)
78
+ writeFileSync(file, [...(existsSync(file) ? factsOf(readFileSync(file, 'utf8').split('\n')) : []).filter((k) => !mine.some((m) => bare(m) === bare(k))), ...mine.filter((m) => m['value'] !== 'withdraw')].map(({ sig: _sig, epoch: _epoch, ...l }) => `${canonical({ ...l, by: 'target' })}\n`).join(''));
79
+ };
80
+ export const landAct = async (_name, place, lot, key, signer, port, signal) => ((got) => {
78
81
  if (got === undefined || got.code !== 0)
79
82
  return { refused: why(got) ?? '' };
80
83
  const at = got.out.findIndex((line) => line.startsWith(words.act));
81
84
  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` }
82
- : { lines: got.out.slice(at + 1).filter((line) => parse(line).kind === 'fact').slice(0, -1) };
83
- })(await run(name, place, [words.land, lotFile(lot), words.key, key, words.signer, signer], env, signal));
84
- 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));
85
+ : (birthed(place, lot), { lines: got.out.slice(at + 1).filter((line) => parse(line).kind === 'fact').slice(0, -1) });
86
+ })(await through(place, words.land, lot, key, signer, port, signal));
87
+ export const land = async (_name, place, signed, signal) => ((got) => (got !== undefined && got.code === 0 ? (birthed(place, signed), { lines: signed }) : { refused: why(got) ?? '' }))(await through(place, words.land, signed, undefined, undefined, undefined, signal));
85
88
  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));
86
89
  export const pinnedAt = async (name, place, file, signal) => {
87
90
  const printed = await run(name, place, [words.read, file], {}, signal);
@@ -92,6 +95,6 @@ export const pinnedAt = async (name, place, file, signal) => {
92
95
  const bytes = printed.out.filter((line) => line.startsWith(words.topos)).map((line) => `${line.slice(words.topos.length)}\n`).join('');
93
96
  return pin === undefined || !bytes ? undefined : { pin, bytes, lines: read.lines };
94
97
  };
95
- 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));
98
+ export const founded = async (_name, place, key, signer, port, signal) => ((got) => (got !== undefined && got.code === 0 ? (birthed(place, readFileSync(join(place, LOCK), 'utf8').split('\n')), { lines: got.out }) : { refused: why(got) ?? '' }))(await through(place, words.land, undefined, key, signer, port, signal));
96
99
  export const words = { read: '', view: '', sign: '', land: '', key: '', signer: '', pin: '', topos: '', standing: '', evidence: '', act: '', profile: '', receipts: '', refusal: '', identity: '' };
97
100
  export const settledView = async (name, place, view, signal) => readOf(await run(name, place, [words.read, words.view, view], {}, signal));
@@ -0,0 +1,35 @@
1
+ import type { NativeHostOptions } from '@lapxo/bound/host';
2
+ import type { Printed } from './process.ts';
3
+ import { type Said } from '../core/lock.ts';
4
+ /** What a place's fold host is held to, from head's lock: the installed instrument it must be, how long it stays idle and lives, and its limits. */
5
+ export declare const hosting: {
6
+ installed: string;
7
+ idle: number;
8
+ life: number;
9
+ wait: number;
10
+ bytes: number;
11
+ heap: number;
12
+ queued: number;
13
+ http: NativeHostOptions["http"];
14
+ };
15
+ /** Receiver permissions and work budgets are declared by head's own lock, never inferred from a redirect. */
16
+ export declare const configureHost: (lock: readonly Said[], bounds: {
17
+ readonly wait: number;
18
+ readonly most: number;
19
+ }) => void;
20
+ /** Every host closed, its scratch removed with it: whenever a caller asks, and when head ends, its scratch then removed at once, whatever its host is still doing. */
21
+ export declare const cooled: () => void;
22
+ /** The process a place's fold host runs as, while one is held for it. */
23
+ export declare const hostOf: (place: string) => number | undefined;
24
+ /**
25
+ * A read of a place's view through the place's own fold host, which only folds and never holds a signer: started on the first
26
+ * read, kept while it is asked within its idle and its life, the installed bound checked file by file before every call.
27
+ * A refusal, an error or a changed instrument closes it, and the next read starts another.
28
+ */
29
+ export declare const hosted: (place: string, args: readonly string[], signal?: AbortSignal, from?: string) => Promise<Printed>;
30
+ /**
31
+ * A sign or a land in a host of its own, started for this one call with the signers the lock names and closed once it
32
+ * answers: the signer never outlives the call it signed for. The place's fold host is closed first, a place having one
33
+ * host at a time; the lot goes in as the host's input, never a file of head's.
34
+ */
35
+ export declare const signedIn: (place: string, args: readonly string[], lot: readonly string[] | undefined, signers: NonNullable<NativeHostOptions["signers"]>, keyId?: string, signal?: AbortSignal, from?: string) => Promise<Printed>;
@@ -0,0 +1,108 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join, relative, sep } from 'node:path';
6
+ import { installedHostInstrument, nativeHostEnvironment, startNativeHost } from '@lapxo/bound/host';
7
+ import { members, valueOf } from "../core/lock.js";
8
+ /** What a place's fold host is held to, from head's lock: the installed instrument it must be, how long it stays idle and lives, and its limits. */
9
+ export const hosting = { installed: '', idle: 0, life: 0, wait: 0, bytes: 0, heap: 0, queued: 0, http: undefined };
10
+ /** Receiver permissions and work budgets are declared by head's own lock, never inferred from a redirect. */
11
+ export const configureHost = (lock, bounds) => { Object.assign(hosting, { installed: valueOf(lock, 'dep/bound/installed'), wait: bounds.wait, bytes: bounds.most, ...Object.fromEntries(['idle', 'life', 'heap', 'queued'].map((k) => [k, Number(valueOf(lock, `bound/host/${k}`).split('..')[1])])), http: { origins: members(valueOf(lock, 'bound/receiver/origins')), redirects: { maxRedirects: Number(valueOf(lock, 'bound/receiver/redirects').split('..')[1]), headerBytes: Number(valueOf(lock, 'bound/receiver/header-bytes').split('..')[1]) } } }); };
12
+ const root = dirname(createRequire(import.meta.url).resolve('@lapxo/bound/package.json'));
13
+ const walk = (at) => (statSync(at).isDirectory() ? readdirSync(at).flatMap((e) => walk(join(at, e))) : [at]);
14
+ /** Every file of the installed bound by its coordinate and digest: what its identity is computed from, and what is checked again before each call. */
15
+ const inventory = (at) => walk(at).map((f) => ({ coordinate: relative(at, f).split(sep).join('/'), digest: `sha256:${createHash('sha256').update(readFileSync(f)).digest('hex')}` })).sort((a, b) => (a.coordinate < b.coordinate ? -1 : 1));
16
+ /** The hosts held, those starting, those closing — a place gets its next host once its last one is gone — and the folders head made for their scratch, each removed once its host is. */
17
+ const [live, starting, closing, scratches] = [new Map(), new Map(), new Map(), new Set()];
18
+ const cleared = (dir) => { rmSync(dir, { recursive: true, force: true }); scratches.delete(dir); };
19
+ const shut = (at) => { const one = live.get(at); live.delete(at); if (one) {
20
+ clearTimeout(one.idle);
21
+ closing.set(at, one.host.close().catch(() => undefined).finally(() => { cleared(one.scratch); closing.delete(at); }));
22
+ } };
23
+ /** Every host closed, its scratch removed with it: whenever a caller asks, and when head ends, its scratch then removed at once, whatever its host is still doing. */
24
+ export const cooled = () => { for (const at of [...live.keys()])
25
+ shut(at); };
26
+ /** The process a place's fold host runs as, while one is held for it. */
27
+ export const hostOf = (place) => live.get(realpathSync(place))?.host.pid;
28
+ process.once('exit', () => { cooled(); for (const dir of [...scratches])
29
+ cleared(dir); });
30
+ /** A host started on a place, on the installed instrument the lock pins and in a scratch folder of head's own; with signers only for one call's sign or land. */
31
+ const opened = async (at, from, signers) => {
32
+ const members = inventory(from), instrument = installedHostInstrument({ directory: from, shape: 'package', members, exports: [['exports', './engine', 'default']], pins: [], limits: { members: 4096, bytes: 1 << 27, responseBytes: 1 << 22 } }, join(from, 'dist/cli/verb.js'));
33
+ const scratch = mkdtempSync(join(tmpdir(), 'head-host-'));
34
+ scratches.add(scratch);
35
+ const host = await startNativeHost({ place: at, instrument, expectedInstrument: hosting.installed, environment: nativeHostEnvironment({ ...process.env }), scratchParent: scratch, ...(hosting.http ? { http: hosting.http } : {}), ...(signers ? { signers } : {}), limits: { requestBytes: hosting.bytes, responseBytes: hosting.bytes, timeoutMs: hosting.wait, heapMiB: hosting.heap, queuedRequests: hosting.queued } }).catch((e) => { cleared(scratch); throw e; });
36
+ return { host, members: JSON.stringify(members), born: Date.now(), scratch, busy: 0 };
37
+ };
38
+ const started = async (at, from) => ((one) => (live.set(at, one), one))(await opened(at, from));
39
+ const lines = (text) => text.split('\n').filter(Boolean), refused = (why) => ({ code: -1, out: [], err: [why] });
40
+ /**
41
+ * A read of a place's view through the place's own fold host, which only folds and never holds a signer: started on the first
42
+ * read, kept while it is asked within its idle and its life, the installed bound checked file by file before every call.
43
+ * A refusal, an error or a changed instrument closes it, and the next read starts another.
44
+ */
45
+ export const hosted = async (place, args, signal, from = root) => {
46
+ if (args[0] !== 'fold')
47
+ return refused(`REFUSE·host a place's fold host only folds, and never holds a signer: ${args[0]} goes to a process of its own`);
48
+ const at = realpathSync(place), was = live.get(at);
49
+ if (was !== undefined && Date.now() - was.born > hosting.life * 1000)
50
+ shut(at);
51
+ let one;
52
+ try {
53
+ await closing.get(at);
54
+ one = live.get(at) ?? await (starting.get(at) ?? ((p) => (starting.set(at, p), p))(started(at, from).finally(() => starting.delete(at))));
55
+ }
56
+ catch (e) {
57
+ return refused(e.message);
58
+ }
59
+ if (JSON.stringify(inventory(from)) !== one.members) {
60
+ shut(at);
61
+ return refused(`REFUSE·host the installed bound changed after its host started, so it is no longer ${hosting.installed}: the host is closed, and nothing was read`);
62
+ }
63
+ clearTimeout(one.idle);
64
+ one.busy += 1;
65
+ try {
66
+ const got = await one.host.request({ verb: 'fold', args: args.slice(1) }, signal);
67
+ if (got.status !== 0)
68
+ shut(at);
69
+ return { code: got.status, out: lines(got.stdout), err: lines(got.stderr) };
70
+ }
71
+ catch (e) {
72
+ shut(at);
73
+ return refused(e.message);
74
+ }
75
+ finally {
76
+ one.busy -= 1;
77
+ if (one.busy === 0 && live.get(at) === one)
78
+ one.idle = setTimeout(() => shut(at), hosting.idle * 1000).unref();
79
+ }
80
+ };
81
+ /**
82
+ * A sign or a land in a host of its own, started for this one call with the signers the lock names and closed once it
83
+ * answers: the signer never outlives the call it signed for. The place's fold host is closed first, a place having one
84
+ * host at a time; the lot goes in as the host's input, never a file of head's.
85
+ */
86
+ export const signedIn = async (place, args, lot, signers, keyId, signal, from = root) => {
87
+ const at = realpathSync(place);
88
+ shut(at);
89
+ await closing.get(at);
90
+ let one;
91
+ try {
92
+ one = await opened(at, from, signers);
93
+ }
94
+ catch (e) {
95
+ return refused(e.message);
96
+ }
97
+ try {
98
+ const got = await one.host.request({ verb: args[0], args: [...(lot ? ['@input/lot.bound'] : []), ...args.slice(1)], ...(lot ? { inputs: [{ name: 'lot.bound', text: `${lot.join('\n')}\n` }] } : {}), ...(keyId ? { signer: keyId } : {}) }, signal);
99
+ return { code: got.status, out: lines(got.stdout), err: lines(got.stderr) };
100
+ }
101
+ catch (e) {
102
+ return refused(e.message);
103
+ }
104
+ finally {
105
+ await one.host.close().catch(() => undefined);
106
+ cleared(one.scratch);
107
+ }
108
+ };
@@ -2,7 +2,19 @@ export interface KeyStore {
2
2
  readonly read: (account: string) => string | undefined;
3
3
  readonly write: (account: string, secret: string) => void;
4
4
  readonly made?: (account: string) => string | undefined;
5
+ readonly held?: (account: string) => string | undefined;
5
6
  }
6
7
  export declare const plain: (word: string) => string;
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. */
8
- export declare const keychain: (service: string, kind: string) => KeyStore;
8
+ /** 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. Only head's signer opens it for a signing key. */
9
+ export declare const secretsOf: (service: string, kind: string) => KeyStore;
10
+ /** The store head's long process is handed: a signing account is refused by name before any command starts, and its key is made, or its public half read, in head's signer, never here; the secrets of access stay this process's, since it calls their APIs. */
11
+ export declare const keychain: (service: string, kind: string, signing: (account: string) => string | undefined) => KeyStore;
12
+ /** The signing port a host calls for one lot: head's signer started with the key's names alone and the lot on its stdin, within the wait the lock gives bound; what it prints are the signatures, read as the signing-lot contract reads them. */
13
+ export declare const portOf: (service: string, account: string, kind: string, wait: number) => (lot: {
14
+ readonly bytes: readonly string[];
15
+ readonly keyId: string;
16
+ readonly algorithm: string;
17
+ }, signal: AbortSignal) => Promise<readonly {
18
+ readonly keyId: string;
19
+ readonly signature: string;
20
+ }[]>;
@@ -1,12 +1,24 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { signerResponseOf } from '@lapxo/topos/wire';
4
+ import { spawned } from "./process.js";
1
5
  import { store as credentialManager } from "./stores/credential-manager.js";
2
6
  import { store as keychainOfMac } from "./stores/keychain.js";
3
7
  import { store as secretTool } from "./stores/secret-tool.js";
4
- import { newKey, publicOf } from "./keys.js";
5
8
  export const plain = (word) => { if (!/^[\w.@/+=-]+$/.test(word))
6
9
  throw Error('REFUSE·keychain a name holds more than word characters'); return word; };
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. */
8
- export const keychain = (service, kind) => ((made) => {
9
- if (made === undefined)
10
- throw Error(`REFUSE·keychain ${kind} is no store head carries`);
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));
12
- })({ keychain: keychainOfMac, 'secret-tool': secretTool, 'credential-manager': credentialManager }[kind]);
10
+ /** 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. Only head's signer opens it for a signing key. */
11
+ export const secretsOf = (service, kind) => ((made) => { if (made === undefined)
12
+ throw Error(`REFUSE·keychain ${kind} is no store head carries`); return made(service); })({ keychain: keychainOfMac, 'secret-tool': secretTool, 'credential-manager': credentialManager }[kind]);
13
+ const signer = fileURLToPath(new URL(`../signer${import.meta.url.slice(import.meta.url.lastIndexOf('.'))}`, import.meta.url)), half = (mode, service, account, kind) => { try {
14
+ return execFileSync(process.execPath, [signer, mode, service, plain(account), kind], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) || undefined;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ } };
19
+ /** The store head's long process is handed: a signing account is refused by name before any command starts, and its key is made, or its public half read, in head's signer, never here; the secrets of access stay this process's, since it calls their APIs. */
20
+ export const keychain = (service, kind, signing) => ((keys, guard) => ({ read: (account) => keys.read(guard(account)), write: (account, secret) => keys.write(guard(account), secret), made: (account) => half('make', service, account, kind), held: (account) => half('exists', service, account, kind) }))(secretsOf(service, kind), (account) => ((key) => { if (key !== undefined)
21
+ throw Error(`REFUSE·keychain ${key} names a signing key: only head's signer reads it`); return account; })(signing(account)));
22
+ /** The signing port a host calls for one lot: head's signer started with the key's names alone and the lot on its stdin, within the wait the lock gives bound; what it prints are the signatures, read as the signing-lot contract reads them. */
23
+ export const portOf = (service, account, kind, wait) => async (lot, signal) => ((got) => { if (got.code !== 0)
24
+ throw Error(`REFUSE·signer head's signer did not sign for ${lot.keyId}`); return signerResponseOf(`${got.out.join('\n')}\n`, lot); })(await spawned(process.execPath, [signer, service, plain(account), lot.keyId, lot.algorithm, kind], { input: `${lot.bytes.join('\n')}\n` }, { wait, most: 1 << 20, signal }));
@@ -0,0 +1,4 @@
1
+ /** A file one bound run reads, in a folder of its own under this device's temporary folder, kept only until that run ends. */
2
+ export declare const scratchOf: (prefix: string, name: string, text: string) => string;
3
+ /** The folders made for what a run read, removed once it ended, however it ended; any other path is left as it is. */
4
+ export declare const cleared: (paths: readonly string[]) => void;
@@ -0,0 +1,10 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ const made = new Set();
5
+ /** A file one bound run reads, in a folder of its own under this device's temporary folder, kept only until that run ends. */
6
+ export const scratchOf = (prefix, name, text) => ((dir) => { made.add(dir); writeFileSync(join(dir, name), text); return join(dir, name); })(mkdtempSync(join(tmpdir(), prefix)));
7
+ /** The folders made for what a run read, removed once it ended, however it ended; any other path is left as it is. */
8
+ export const cleared = (paths) => { for (const at of paths)
9
+ if (made.delete(dirname(at)))
10
+ rmSync(dirname(at), { recursive: true, force: true }); };
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from 'node:fs';
3
- import { signLot } from "./ports/keys.js";
4
- import { keychain } from "./ports/keychain.js";
5
- const [service = '', account = '', key = '', algorithm = '', store = 'keychain'] = process.argv.slice(2);
6
- const secret = keychain(service, store).read(account);
7
- if (secret === undefined)
8
- process.exit(1);
3
+ import { newKey, publicOf, signLot } from "./ports/keys.js";
4
+ import { secretsOf } from "./ports/keychain.js";
5
+ /** The one process that reads a signing secret, and it prints a public half at most: `make` keeps a key made once, `exists` says whether one is kept, else it signs the lot on stdin. */
6
+ const [first = '', ...rest] = process.argv.slice(2), half = first === 'make' || first === 'exists', [service = '', account = '', key = '', algorithm = '', store = 'keychain'] = half ? [rest[0], rest[1], '', '', rest[2]] : [first, ...rest], keys = secretsOf(service, store);
7
+ const kept = keys.read(account) ?? (first === 'make' ? ((made) => { keys.write(account, made.secret); return keys.read(account) === made.secret ? made.secret : undefined; })(newKey()) : undefined);
8
+ if (kept === undefined)
9
+ process.exit(half ? 3 : 1);
9
10
  try {
10
- process.stdout.write(signLot(readFileSync(0, 'utf8'), secret, key, algorithm));
11
+ process.stdout.write(half ? publicOf(kept) : signLot(readFileSync(0, 'utf8'), kept, key, algorithm));
11
12
  }
12
13
  catch {
13
14
  process.exit(1);
@@ -1,13 +1,10 @@
1
- import { inPlace, printedOr, standingOr } from "../core/answer.js";
1
+ import { inStanding, printedOr } from "../core/answer.js";
2
2
  import { members, valueOf } from "../core/lock.js";
3
3
  import { sayIn } from "../core/surface.js";
4
4
  /** What is open for a key: the cells view the place's standing declares, as bound folded it at the resolution asked, narrowed to the key; a refused fold is said, never read as no view. The standing is the one the card was read off, folded once a call. */
5
- export const answer = (asked, context) => inPlace(asked, context, async (place) => {
5
+ export const answer = (asked, context) => inStanding(asked, context, async (place, lines) => {
6
6
  const word = (scope) => valueOf(context.lock, `bound/${scope}`);
7
- const held = await standingOr(context, place);
8
- if ('said' in held)
9
- return held.said;
10
- const view = held.lines.find((f) => f.scope?.startsWith('view/') && !f.shape && members(f.value ?? '').includes(word('cells')));
7
+ const view = lines.find((f) => f.scope?.startsWith('view/') && !f.shape && members(f.value ?? '').includes(word('cells')));
11
8
  if (view === undefined)
12
9
  return [sayIn(context.lock)('no-cells', { place })];
13
10
  return printedOr(context, await context.bound(place, [word('read'), word('view'), `${view.scope.split('/')[1]}${asked['at'] ? `${word('at')}${asked['at']}` : ''}`, ...(asked['key'] ? [word('key'), asked['key']] : [])]));