@lapxo/head 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -14
- package/TARGET.bound +301 -118
- package/dist/src/access/contract.d.ts +0 -2
- package/dist/src/access/obtain.js +3 -3
- package/dist/src/ahead/budget.d.ts +4 -0
- package/dist/src/ahead/budget.js +8 -0
- package/dist/src/ahead/read.d.ts +2 -2
- package/dist/src/ahead/read.js +2 -4
- package/dist/src/claims/claim.d.ts +1 -0
- package/dist/src/claims/claim.js +15 -15
- package/dist/src/claims/view.d.ts +3 -0
- package/dist/src/claims/view.js +11 -0
- package/dist/src/compile/pin.d.ts +7 -0
- package/dist/src/compile/pin.js +6 -7
- package/dist/src/core/answer.d.ts +3 -0
- package/dist/src/core/answer.js +1 -0
- package/dist/src/core/device.d.ts +2 -2
- package/dist/src/core/device.js +5 -5
- package/dist/src/core/lock.d.ts +1 -0
- package/dist/src/core/lock.js +2 -1
- package/dist/src/core/package.js +3 -1
- package/dist/src/core/read.js +1 -2
- package/dist/src/core/worlds.js +2 -3
- package/dist/src/doors/guard.d.ts +18 -0
- package/dist/src/doors/guard.js +63 -0
- package/dist/src/doors/head.d.ts +60 -0
- package/dist/src/doors/head.js +141 -0
- package/dist/src/doors/http.d.ts +3 -0
- package/dist/src/doors/http.js +34 -0
- package/dist/src/doors/lines.d.ts +3 -0
- package/dist/src/doors/lines.js +21 -0
- package/dist/src/flows/device.d.ts +3 -0
- package/dist/src/flows/device.js +5 -7
- package/dist/src/flows/plan.d.ts +2 -0
- package/dist/src/flows/plan.js +22 -11
- package/dist/src/flows/read.js +4 -1
- package/dist/src/index.js +17 -116
- package/dist/src/links/birth.d.ts +6 -0
- package/dist/src/links/birth.js +48 -0
- package/dist/src/links/inspect.d.ts +1 -1
- package/dist/src/links/inspect.js +28 -19
- package/dist/src/links/link.d.ts +2 -2
- package/dist/src/links/link.js +7 -10
- package/dist/src/places/found.js +1 -1
- package/dist/src/ports/bound.d.ts +9 -11
- package/dist/src/ports/bound.js +37 -33
- package/dist/src/ports/host.d.ts +53 -0
- package/dist/src/ports/host.js +157 -0
- package/dist/src/ports/keychain.d.ts +14 -2
- package/dist/src/ports/keychain.js +19 -7
- package/dist/src/ports/scratch.d.ts +4 -0
- package/dist/src/ports/scratch.js +10 -0
- package/dist/src/signer.js +8 -7
- package/dist/src/sources/connect.js +3 -2
- package/dist/src/tools/card.js +4 -6
- package/dist/src/use/named.js +3 -3
- package/dist/src/verbs/ask.js +2 -1
- package/dist/src/verbs/card.js +2 -2
- package/dist/src/walk/context.js +3 -6
- package/package.json +3 -3
- package/dist/src/ports/reading.d.ts +0 -8
- package/dist/src/ports/reading.js +0 -14
- package/dist/src/ports/signers.d.ts +0 -6
- package/dist/src/ports/signers.js +0 -14
- package/dist/src/ports/spare.d.ts +0 -1
- package/dist/src/ports/spare.js +0 -33
- package/dist/src/ports/warm.d.ts +0 -19
- package/dist/src/ports/warm.js +0 -45
|
@@ -1,44 +1,53 @@
|
|
|
1
1
|
import { decodeReference } from '@lapxo/bound/reference';
|
|
2
|
+
import { decodeLocatedReference, locatedReferenceIdentity } from '@lapxo/topos/contract';
|
|
2
3
|
import { canonical } from '@lapxo/topos/wire';
|
|
3
|
-
import {
|
|
4
|
+
import { inStanding } from "../core/answer.js";
|
|
4
5
|
import { members, valueOf } from "../core/lock.js";
|
|
5
6
|
import { sayIn } from "../core/surface.js";
|
|
6
7
|
import { linesOf } from "../core/worlds.js";
|
|
7
8
|
import { rootIn } from "../flows/device.js";
|
|
8
9
|
/**
|
|
9
|
-
* A
|
|
10
|
+
* A legacy or located reference connected: inspected by the instrument in the person's place, never authenticated — its bytes are the
|
|
10
11
|
* link's, and nothing shows who shared it — and what it names is shown; nothing is connected or granted, since a package
|
|
11
12
|
* is received by the digest its sharer sent apart. A place that admits no content transport is handed the lines that admit
|
|
12
13
|
* 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
14
|
* 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.
|
|
14
15
|
*/
|
|
15
|
-
export const linked = (asked, context) =>
|
|
16
|
-
const [say, word, link] = [sayIn(context.lock), (k) => valueOf(context.lock, k), asked['document'] ?? ''],
|
|
17
|
-
if ('said' in held)
|
|
18
|
-
return held.said;
|
|
16
|
+
export const linked = (asked, context) => inStanding(asked, context, async (place, standing) => {
|
|
17
|
+
const [say, word, link] = [sayIn(context.lock), (k) => valueOf(context.lock, k), asked['document'] ?? ''], admits = members(valueOf(context.lock, 'link/transport'));
|
|
19
18
|
if (asked['why'] === 'true')
|
|
20
|
-
return [...
|
|
19
|
+
return [...standing.filter((l) => admits.includes(l.scope ?? '') || l.scope?.startsWith('transport/')).map((l) => canonical(l)), say('why-transport', { place })];
|
|
21
20
|
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' }));
|
|
22
21
|
let reference;
|
|
22
|
+
const legacy = link.startsWith(word('link/prefix'));
|
|
23
23
|
try {
|
|
24
|
-
reference = decodeReference(link);
|
|
24
|
+
reference = legacy ? (({ source, ...held }) => ({ ...held, location: source, 'location-class': 'holder' }))(decodeReference(link)) : decodeLocatedReference(link);
|
|
25
25
|
}
|
|
26
26
|
catch (refused) {
|
|
27
27
|
return [say('link-refused', { link, why: refused instanceof Error ? refused.message : String(refused) })];
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
const identity = legacy ? link : locatedReferenceIdentity(reference), source = reference.location;
|
|
30
|
+
if (!source || reference['location-class'] === 'viewer')
|
|
31
|
+
return [say('reference-needs', { identity, needs: 'bytes/holder-location' })];
|
|
32
|
+
if (![admits[0], `transport/${new URL(source).protocol.slice(0, -1)}`].every((s) => standing.some((l) => l.scope === s)))
|
|
30
33
|
return [say('transport-missing', { place, lines: lines.join('\n') })];
|
|
31
|
-
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)}`];
|
|
32
|
-
const [owed,
|
|
33
|
-
if ('said' in root)
|
|
34
|
-
return root.said;
|
|
34
|
+
const [printed, scope] = [await context.bound(place, [word('bound/read'), link]), `${word('place/needs')}/bytes/${reference.blob.slice(reference.algorithm.length + 1, legacy ? reference.algorithm.length + 17 : undefined)}`];
|
|
35
|
+
const [owed, why, absent] = [standing.find((l) => l.scope === scope && l.role === 'demands' && l.value === identity), printed?.err.filter((l) => !l.startsWith('{')).at(-1) ?? '', printed?.out.some((line) => line.startsWith('NEEDS '))];
|
|
35
36
|
if (printed?.code !== 0 && !why.includes(word('link/unarrived')))
|
|
36
37
|
return [say('link-refused', { link, why })];
|
|
37
|
-
const settled =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const settled = async (lot) => {
|
|
39
|
+
if (!lot.length)
|
|
40
|
+
return [];
|
|
41
|
+
const root = rootIn(context, place);
|
|
42
|
+
if ('said' in root)
|
|
43
|
+
return root.said;
|
|
44
|
+
const got = await context.landRoot(place, lot, root.account);
|
|
45
|
+
return 'refused' in got ? [say('not-landed', { why: got.refused })] : got.lines;
|
|
46
|
+
};
|
|
47
|
+
const due = { at: 'policy:head/link', by: 'target', form: 'alphabet', measure: 'id', role: 'demands', scope, value: identity };
|
|
48
|
+
if (printed?.code !== 0 || absent)
|
|
49
|
+
return [say('link-owed', { place, source }), ...await settled(owed ? [] : [canonical(due)])];
|
|
41
50
|
const named = linesOf(new TextEncoder().encode(printed.out.filter((l) => l.startsWith('TOPOS ')).map((l) => l.slice('TOPOS '.length)).join('\n')));
|
|
42
51
|
const sources = named.filter((l) => /^sources\/[^/]+$/.test(l.scope ?? '')).map((l) => l.scope.split('/')[1]);
|
|
43
|
-
return [say('inspected', { source
|
|
44
|
-
});
|
|
52
|
+
return [say('inspected', { source, sources: sources.join(', ') || '—', digest: reference.digest }), say('sharer-unverified', {}), ...await settled(owed ? [canonical({ ...due, value: 'withdraw' })] : [])];
|
|
53
|
+
}, context.policy);
|
package/dist/src/links/link.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Context } from '../core/answer.ts';
|
|
2
2
|
/**
|
|
3
3
|
* A package's link: its lines as one standing document, laid in its folder as it is to be published — the link names that
|
|
4
|
-
* document's bytes, never the package's files
|
|
5
|
-
*
|
|
4
|
+
* document's bytes, never the package's files. The separate located profile keeps retrieval apart from content identity.
|
|
5
|
+
* Without a location the reference still names its content; it does not invent a holder or transfer authority.
|
|
6
6
|
*/
|
|
7
7
|
export declare const linkOf: (context: Context, to: string, lines: readonly string[], urls: Readonly<Record<string, string>>) => readonly string[];
|
package/dist/src/links/link.js
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
2
|
+
import { encodeLocatedReference } from '@lapxo/topos/contract';
|
|
4
3
|
import { semanticStandingBytes } from '@lapxo/topos/standing';
|
|
5
4
|
import { members, valueOf } from "../core/lock.js";
|
|
6
5
|
import { sayIn } from "../core/surface.js";
|
|
7
|
-
|
|
6
|
+
import { digestIn } from "../core/worlds.js";
|
|
8
7
|
/**
|
|
9
8
|
* A package's link: its lines as one standing document, laid in its folder as it is to be published — the link names that
|
|
10
|
-
* document's bytes, never the package's files
|
|
11
|
-
*
|
|
9
|
+
* document's bytes, never the package's files. The separate located profile keeps retrieval apart from content identity.
|
|
10
|
+
* Without a location the reference still names its content; it does not invent a holder or transfer authority.
|
|
12
11
|
*/
|
|
13
12
|
export const linkOf = (context, to, lines, urls) => {
|
|
14
|
-
const say = sayIn(context.lock), [name, profile, [kind, algorithm]] = [valueOf(context.lock, 'link/document'), valueOf(context.lock, 'link/profile'), members(valueOf(context.lock, 'link/reference'))];
|
|
13
|
+
const say = sayIn(context.lock), digest = digestIn(context.lock), [name, profile, [kind, algorithm]] = [valueOf(context.lock, 'link/document'), valueOf(context.lock, 'link/profile'), members(valueOf(context.lock, 'link/reference'))];
|
|
15
14
|
const standing = semanticStandingBytes(lines), bytes = new TextEncoder().encode(JSON.stringify({ profile, standing })), at = urls[name], document = join(to, name);
|
|
16
15
|
context.files.lay(to, name, bytes);
|
|
17
|
-
if (at === undefined || !URL.canParse(at))
|
|
18
|
-
return [say('shared-unlinked', { document })];
|
|
19
16
|
try {
|
|
20
|
-
return [say('shared-link', { document, at, link:
|
|
17
|
+
return [say(at === undefined ? 'shared-unlinked' : 'shared-link', { document, at: at ?? '', link: encodeLocatedReference({ class: kind, algorithm, digest: digest(standing).slice(7), blob: digest(bytes), ...(at === undefined ? {} : { location: new URL(at).href, 'location-class': 'holder' }) }) })];
|
|
21
18
|
}
|
|
22
19
|
catch (refused) {
|
|
23
|
-
return [say('link-refused', { link:
|
|
20
|
+
return [say('link-refused', { link: name, why: refused instanceof Error ? refused.message : String(refused) })];
|
|
24
21
|
}
|
|
25
22
|
};
|
package/dist/src/places/found.js
CHANGED
|
@@ -18,7 +18,7 @@ const founded = async (context, dir, name) => {
|
|
|
18
18
|
const [head, person] = [deviceKey(context, 'head'), deviceKey(context, 'person')];
|
|
19
19
|
if (head === undefined || person === undefined)
|
|
20
20
|
return { refused: 'no-keychain' };
|
|
21
|
-
if (context.keys.
|
|
21
|
+
if (context.keys.held?.(account) !== undefined)
|
|
22
22
|
return { refused: 'place-taken' };
|
|
23
23
|
const root = context.keys.made?.(account);
|
|
24
24
|
if (root === undefined)
|
|
@@ -1,26 +1,24 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Said } from '../core/lock.ts';
|
|
2
2
|
import type { Outcome } from '../core/answer.ts';
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
/** Read normative standing plus authenticated device evidence; ambiguous device history refuses. */
|
|
21
17
|
export declare const policyAt: (name: string, place: string, signal?: AbortSignal) => Promise<Folded>;
|
|
22
18
|
export declare const standingAt: (name: string, place: string, devicesOf: (lines: readonly Said[]) => readonly string[], isHistory: (line: Said) => boolean, signal?: AbortSignal) => Promise<Folded>;
|
|
23
|
-
|
|
19
|
+
/** 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. */
|
|
20
|
+
type Port = ReturnType<typeof portOf>;
|
|
21
|
+
export declare const sign: (_name: string, place: string, lot: readonly string[], key: string, signer: string, port: Port, signal?: AbortSignal) => Promise<Outcome>;
|
|
24
22
|
/** 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. */
|
|
25
23
|
/** A place's receipts rendered beside its lock, so bound alone checks the place: nothing when they closed, else why they did not. */
|
|
26
24
|
export declare const closing: (name: string, place: string, signal?: AbortSignal) => Promise<string | undefined>;
|
|
@@ -29,8 +27,8 @@ export declare const birth: {
|
|
|
29
27
|
at: string;
|
|
30
28
|
file: string;
|
|
31
29
|
}, birthed: (place: string, lot: readonly string[]) => void;
|
|
32
|
-
export declare const landAct: (
|
|
33
|
-
export declare const land: (
|
|
30
|
+
export declare const landAct: (_name: string, place: string, lot: readonly string[], key: string, signer: string, port: Port, signal?: AbortSignal) => Promise<Outcome>;
|
|
31
|
+
export declare const land: (_name: string, place: string, signed: readonly string[], signal?: AbortSignal) => Promise<Outcome>;
|
|
34
32
|
export declare const told: (name: string, place: string, signed: readonly string[], view: string, signal?: AbortSignal) => Promise<readonly string[]>;
|
|
35
33
|
export declare const pinnedAt: (name: string, place: string, file: string, signal?: AbortSignal) => Promise<{
|
|
36
34
|
readonly pin: string;
|
|
@@ -39,7 +37,7 @@ export declare const pinnedAt: (name: string, place: string, file: string, signa
|
|
|
39
37
|
} | {
|
|
40
38
|
readonly refused: string;
|
|
41
39
|
} | undefined>;
|
|
42
|
-
export declare const founded: (
|
|
40
|
+
export declare const founded: (_name: string, place: string, key: string, signer: string, port: Port, signal?: AbortSignal) => Promise<Outcome>;
|
|
43
41
|
export declare const words: {
|
|
44
42
|
read: string;
|
|
45
43
|
view: string;
|
package/dist/src/ports/bound.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { existsSync,
|
|
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
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 {
|
|
7
|
+
import { capturedViews, hosted, hosting, signedIn } from "./host.js";
|
|
8
|
+
import { cleared, scratchOf } from "./scratch.js";
|
|
8
9
|
import { folded } from "../ahead/fold.js";
|
|
9
|
-
import { readingOf } from "./reading.js";
|
|
10
10
|
const binOf = (name) => {
|
|
11
11
|
try {
|
|
12
12
|
const manifest = createRequire(import.meta.url).resolve(join(name, 'package.json'));
|
|
@@ -18,9 +18,16 @@ const binOf = (name) => {
|
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
20
|
export const limits = { wait: 0, most: 0 };
|
|
21
|
-
/** bound
|
|
22
|
-
export const run = async (name, cwd, args, env = {}, signal) =>
|
|
23
|
-
|
|
21
|
+
/** 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. */
|
|
22
|
+
export const run = async (name, cwd, args, env = {}, signal) => {
|
|
23
|
+
try {
|
|
24
|
+
return await ((bin, at, bounds) => (bin === undefined ? undefined
|
|
25
|
+
: (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 } : {}) });
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
cleared([...args, ...Object.values(env)]);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
24
31
|
/** 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
32
|
const recordOf = (line) => { try {
|
|
26
33
|
return ((got) => (got?.profile === words.refusal ? got : undefined))(JSON.parse(line));
|
|
@@ -31,26 +38,22 @@ catch {
|
|
|
31
38
|
const isRecord = (line) => line.startsWith('{') && recordOf(line) !== undefined;
|
|
32
39
|
const apart = (printed) => ((record) => (record === undefined ? printed : { ...printed, refusal: recordOf(record) }))([...printed.err].reverse().find(isRecord));
|
|
33
40
|
const why = (printed) => printed?.err.filter((line) => !isRecord(line)).at(-1);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
47
|
-
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);
|
|
41
|
+
const readOf = (printed) => (printed === undefined || printed.code !== 0 ? { refused: why(printed) ?? '' } : { lines: factsOf(printed.out) });
|
|
42
|
+
/** Only the requested views are captured by Bound; every result binds its text to the actual current native identity. */
|
|
43
|
+
const nativeViews = async (name, place, views, signal) => {
|
|
44
|
+
const captured = capturedViews(place, views, signal);
|
|
45
|
+
if (captured?.kind === 'refuse' && captured.code === 'host/instrument-pin')
|
|
46
|
+
return views.map(() => ({ refused: captured.diagnostic }));
|
|
47
|
+
return Promise.all(views.map(async (view) => {
|
|
48
|
+
const result = captured?.kind === 'fact' ? captured.views.find((one) => one.view === view) : undefined;
|
|
49
|
+
const read = result ? await folded(`${name}/${view}`, place, async () => result.identity, async () => ({ lines: factsOf(result.text.split('\n')) })) : readOf(await run(name, place, [words.read, words.view, view], {}, signal));
|
|
50
|
+
return 'refused' in read || view !== words.standing || read.lines.some((line) => line.scope === words.profile) ? read
|
|
51
|
+
: { 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` };
|
|
52
|
+
}));
|
|
48
53
|
};
|
|
49
|
-
|
|
50
|
-
export const policyAt = async (name, place, signal) => ((folded) => ('refused' in folded || folded.lines.some((line) => line.scope === words.profile) ? folded
|
|
51
|
-
: { 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));
|
|
54
|
+
export const policyAt = async (name, place, signal) => (await nativeViews(name, place, [words.standing], signal))[0];
|
|
52
55
|
export const standingAt = async (name, place, devicesOf, isHistory, signal) => {
|
|
53
|
-
const [folded, evidence] = await
|
|
56
|
+
const [folded, evidence] = await nativeViews(name, place, [words.standing, words.evidence], signal);
|
|
54
57
|
if ('refused' in folded)
|
|
55
58
|
return folded;
|
|
56
59
|
if ('refused' in evidence)
|
|
@@ -61,25 +64,26 @@ export const standingAt = async (name, place, devicesOf, isHistory, signal) => {
|
|
|
61
64
|
return unfolded === undefined ? { lines: [...folded.lines, ...own] }
|
|
62
65
|
: { 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` };
|
|
63
66
|
};
|
|
64
|
-
const lotFile = (lines) => (
|
|
65
|
-
|
|
67
|
+
const lotFile = (lines) => scratchOf('head-lot-', 'lot.bound', `${lines.join('\n')}\n`);
|
|
68
|
+
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);
|
|
69
|
+
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));
|
|
66
70
|
/** 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. */
|
|
67
71
|
/** A place's receipts rendered beside its lock, so bound alone checks the place: nothing when they closed, else why they did not. */
|
|
68
72
|
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));
|
|
69
73
|
/** 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. */
|
|
70
74
|
export const birth = { at: '', file: '' }, birthed = (place, lot) => {
|
|
71
|
-
const [file, mine] = [join(place, birth.file),
|
|
75
|
+
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());
|
|
72
76
|
if (mine.length)
|
|
73
|
-
writeFileSync(file, [...(existsSync(file) ?
|
|
77
|
+
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(''));
|
|
74
78
|
};
|
|
75
|
-
export const landAct = async (
|
|
79
|
+
export const landAct = async (_name, place, lot, key, signer, port, signal) => ((got) => {
|
|
76
80
|
if (got === undefined || got.code !== 0)
|
|
77
81
|
return { refused: why(got) ?? '' };
|
|
78
82
|
const at = got.out.findIndex((line) => line.startsWith(words.act));
|
|
79
83
|
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` }
|
|
80
84
|
: (birthed(place, lot), { lines: got.out.slice(at + 1).filter((line) => parse(line).kind === 'fact').slice(0, -1) });
|
|
81
|
-
})(await
|
|
82
|
-
export const land = async (
|
|
85
|
+
})(await through(place, words.land, lot, key, signer, port, signal));
|
|
86
|
+
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));
|
|
83
87
|
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));
|
|
84
88
|
export const pinnedAt = async (name, place, file, signal) => {
|
|
85
89
|
const printed = await run(name, place, [words.read, file], {}, signal);
|
|
@@ -90,6 +94,6 @@ export const pinnedAt = async (name, place, file, signal) => {
|
|
|
90
94
|
const bytes = printed.out.filter((line) => line.startsWith(words.topos)).map((line) => `${line.slice(words.topos.length)}\n`).join('');
|
|
91
95
|
return pin === undefined || !bytes ? undefined : { pin, bytes, lines: read.lines };
|
|
92
96
|
};
|
|
93
|
-
export const founded = async (
|
|
97
|
+
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));
|
|
94
98
|
export const words = { read: '', view: '', sign: '', land: '', key: '', signer: '', pin: '', topos: '', standing: '', evidence: '', act: '', profile: '', receipts: '', refusal: '', identity: '' };
|
|
95
99
|
export const settledView = async (name, place, view, signal) => readOf(await run(name, place, [words.read, words.view, view], {}, signal));
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
authority: {
|
|
14
|
+
clock: string;
|
|
15
|
+
unit: string;
|
|
16
|
+
items: number;
|
|
17
|
+
};
|
|
18
|
+
http: NativeHostOptions["http"];
|
|
19
|
+
};
|
|
20
|
+
/** Receiver permissions and work budgets are declared by head's own lock, never inferred from a redirect. */
|
|
21
|
+
export declare const configureHost: (lock: readonly Said[], bounds: {
|
|
22
|
+
readonly wait: number;
|
|
23
|
+
readonly most: number;
|
|
24
|
+
}) => void;
|
|
25
|
+
/** 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. */
|
|
26
|
+
export declare const cooled: () => void;
|
|
27
|
+
/** The process a place's fold host runs as, while one is held for it. */
|
|
28
|
+
export declare const hostOf: (place: string) => number | undefined;
|
|
29
|
+
/** Native read-only snapshots use Bound's actual caller environment; their identities are not borrowed from a different CLI context. */
|
|
30
|
+
export declare const capturedViews: (place: string, views: readonly string[], signal?: AbortSignal, from?: string) => import("@lapxo/bound/host").NativeViewIdentitiesResult | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* 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
|
|
33
|
+
* read, kept while it is asked within its idle and its life, the installed bound checked file by file before every call.
|
|
34
|
+
* A refusal, an error or a changed instrument closes it, and the next read starts another.
|
|
35
|
+
*/
|
|
36
|
+
export declare const hosted: (place: string, args: readonly string[], signal?: AbortSignal, from?: string) => Promise<Printed>;
|
|
37
|
+
/**
|
|
38
|
+
* 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
|
|
39
|
+
* answers: the signer never outlives the call it signed for. The place's fold host is closed first, a place having one
|
|
40
|
+
* host at a time; the lot goes in as the host's input, never a file of head's.
|
|
41
|
+
*/
|
|
42
|
+
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>;
|
|
43
|
+
export type AuthorityObservation = {
|
|
44
|
+
readonly refused: string;
|
|
45
|
+
readonly code: string;
|
|
46
|
+
} | {
|
|
47
|
+
readonly authority: string;
|
|
48
|
+
readonly request: string;
|
|
49
|
+
readonly decision: 'allow' | 'ask' | 'deny' | 'open';
|
|
50
|
+
readonly needs: readonly string[];
|
|
51
|
+
};
|
|
52
|
+
/** Configuration-only observations at an explicit receiver clock; no signing, admission or API execution occurs here. */
|
|
53
|
+
export declare const authorityAt: (place: string, key: string, kind: string, records: readonly string[], now: number, signal?: AbortSignal, presented?: string) => Promise<AuthorityObservation>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync } 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, prepareNativeViewIdentities, prepareNativeAuthorityCard, answerNativeAuthorityCard, nativeAuthorityRequestIdentity, startNativeHost } from '@lapxo/bound/host';
|
|
7
|
+
import { decodeAuthorityCard } from '@lapxo/topos/contract';
|
|
8
|
+
import { members, valueOf } from "../core/lock.js";
|
|
9
|
+
/** 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. */
|
|
10
|
+
export const hosting = { installed: '', idle: 0, life: 0, wait: 0, bytes: 0, heap: 0, queued: 0, authority: { clock: '', unit: '', items: 0 }, http: undefined };
|
|
11
|
+
/** Receiver permissions and work budgets are declared by head's own lock, never inferred from a redirect. */
|
|
12
|
+
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])])), authority: { clock: valueOf(lock, 'bound/authority/clock'), unit: valueOf(lock, 'bound/authority/unit'), items: Number(valueOf(lock, 'bound/authority/items').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]) } } }); };
|
|
13
|
+
const root = dirname(createRequire(import.meta.url).resolve('@lapxo/bound/package.json'));
|
|
14
|
+
const walk = (at) => readdirSync(at, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walk(join(at, e.name)) : [join(at, e.name)]);
|
|
15
|
+
/** 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. */
|
|
16
|
+
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));
|
|
17
|
+
/** Held, starting and closing hosts, their owned scratch, and one instrument descriptor reused only after its complete current byte inventory matches. */
|
|
18
|
+
const [live, starting, closing, scratches, selected] = [new Map(), new Map(), new Map(), new Set(), { root: '', members: '', instrument: undefined }];
|
|
19
|
+
const cleared = (dir) => { rmSync(dir, { recursive: true, force: true }); scratches.delete(dir); };
|
|
20
|
+
const shut = (at) => { const one = live.get(at); live.delete(at); if (one) {
|
|
21
|
+
clearTimeout(one.idle);
|
|
22
|
+
closing.set(at, one.host.close().catch(() => undefined).finally(() => { cleared(one.scratch); closing.delete(at); }));
|
|
23
|
+
} };
|
|
24
|
+
/** 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. */
|
|
25
|
+
export const cooled = () => { for (const at of [...live.keys()])
|
|
26
|
+
shut(at); };
|
|
27
|
+
/** The process a place's fold host runs as, while one is held for it. */
|
|
28
|
+
export const hostOf = (place) => live.get(realpathSync(place))?.host.pid;
|
|
29
|
+
process.once('exit', () => { cooled(); for (const dir of [...scratches])
|
|
30
|
+
cleared(dir); });
|
|
31
|
+
/** One verified installed descriptor, reused only after hashing every current installed member. */
|
|
32
|
+
const instrumentAt = (from) => {
|
|
33
|
+
const members = inventory(from), current = JSON.stringify(members);
|
|
34
|
+
const instrument = selected.root === from && selected.members === current ? selected.instrument : Object.assign(selected, { root: from, members: current, 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')) }).instrument;
|
|
35
|
+
return { members: current, instrument };
|
|
36
|
+
};
|
|
37
|
+
const pinnedInstrument = (from) => { const { instrument } = instrumentAt(from); if (instrument.installed?.digest !== hosting.installed)
|
|
38
|
+
throw Error('REFUSE·host/instrument-pin'); return instrument; };
|
|
39
|
+
/** Native read-only snapshots use Bound's actual caller environment; their identities are not borrowed from a different CLI context. */
|
|
40
|
+
export const capturedViews = (place, views, signal, from = root) => {
|
|
41
|
+
try {
|
|
42
|
+
if (signal?.aborted)
|
|
43
|
+
return undefined;
|
|
44
|
+
const instrument = pinnedInstrument(from);
|
|
45
|
+
return prepareNativeViewIdentities({ place, views, instrument });
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
cooled();
|
|
49
|
+
return { kind: 'refuse', code: 'host/instrument-pin', diagnostic: error.message };
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/** 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. */
|
|
53
|
+
const opened = async (at, from, signers) => {
|
|
54
|
+
const { members, instrument } = instrumentAt(from);
|
|
55
|
+
const scratch = mkdtempSync(join(tmpdir(), 'head-host-'));
|
|
56
|
+
scratches.add(scratch);
|
|
57
|
+
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; });
|
|
58
|
+
return { host, members, born: Date.now(), scratch, busy: 0 };
|
|
59
|
+
};
|
|
60
|
+
const started = async (at, from) => ((one) => (live.set(at, one), one))(await opened(at, from));
|
|
61
|
+
const lines = (text) => text.split('\n').filter(Boolean), refused = (why) => ({ code: -1, out: [], err: [why] });
|
|
62
|
+
/**
|
|
63
|
+
* 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
|
|
64
|
+
* read, kept while it is asked within its idle and its life, the installed bound checked file by file before every call.
|
|
65
|
+
* A refusal, an error or a changed instrument closes it, and the next read starts another.
|
|
66
|
+
*/
|
|
67
|
+
export const hosted = async (place, args, signal, from = root) => {
|
|
68
|
+
if (args[0] !== 'fold')
|
|
69
|
+
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`);
|
|
70
|
+
const at = realpathSync(place), was = live.get(at);
|
|
71
|
+
if (was !== undefined && Date.now() - was.born > hosting.life * 1000)
|
|
72
|
+
shut(at);
|
|
73
|
+
let one;
|
|
74
|
+
try {
|
|
75
|
+
await closing.get(at);
|
|
76
|
+
one = live.get(at) ?? await (starting.get(at) ?? ((p) => (starting.set(at, p), p))(started(at, from).finally(() => starting.delete(at))));
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
return refused(e.message);
|
|
80
|
+
}
|
|
81
|
+
if (JSON.stringify(inventory(from)) !== one.members) {
|
|
82
|
+
shut(at);
|
|
83
|
+
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`);
|
|
84
|
+
}
|
|
85
|
+
clearTimeout(one.idle);
|
|
86
|
+
one.busy += 1;
|
|
87
|
+
try {
|
|
88
|
+
const got = await one.host.request({ verb: 'fold', args: args.slice(1) }, signal);
|
|
89
|
+
if (got.status !== 0)
|
|
90
|
+
shut(at);
|
|
91
|
+
return { code: got.status, out: lines(got.stdout), err: lines(got.stderr) };
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
shut(at);
|
|
95
|
+
return refused(e.message);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
one.busy -= 1;
|
|
99
|
+
if (one.busy === 0 && live.get(at) === one)
|
|
100
|
+
one.idle = setTimeout(() => shut(at), hosting.idle * 1000).unref();
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* 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
|
|
105
|
+
* answers: the signer never outlives the call it signed for. The place's fold host is closed first, a place having one
|
|
106
|
+
* host at a time; the lot goes in as the host's input, never a file of head's.
|
|
107
|
+
*/
|
|
108
|
+
export const signedIn = async (place, args, lot, signers, keyId, signal, from = root) => {
|
|
109
|
+
const at = realpathSync(place);
|
|
110
|
+
shut(at);
|
|
111
|
+
await closing.get(at);
|
|
112
|
+
let one;
|
|
113
|
+
try {
|
|
114
|
+
one = await opened(at, from, signers);
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
return refused(e.message);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
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);
|
|
121
|
+
return { code: got.status, out: lines(got.stdout), err: lines(got.stderr) };
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
return refused(e.message);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
await one.host.close().catch(() => undefined);
|
|
128
|
+
cleared(one.scratch);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
/** Configuration-only observations at an explicit receiver clock; no signing, admission or API execution occurs here. */
|
|
132
|
+
export const authorityAt = async (place, key, kind, records, now, signal, presented) => {
|
|
133
|
+
const current = () => { if (signal?.aborted)
|
|
134
|
+
throw Error('REFUSE·authority-preflight/cancelled'); };
|
|
135
|
+
try {
|
|
136
|
+
current();
|
|
137
|
+
const instrument = pinnedInstrument(root);
|
|
138
|
+
const action = { kind, records }, clock = { name: hosting.authority.clock, unit: hosting.authority.unit, now };
|
|
139
|
+
const context = { root: realpathSync(place), place: '.', key, instrument, clock, horizon: { clock: clock.name, unit: clock.unit, from: now, through: now }, limits: { bytes: hosting.bytes, items: hosting.authority.items }, actions: ['record-coverage', 'admission'].includes(kind) ? [action] : [] };
|
|
140
|
+
if (!clock.name || !clock.unit || !Number.isSafeInteger(now) || hosting.authority.items < 1)
|
|
141
|
+
throw Error('REFUSE·authority-preflight/context');
|
|
142
|
+
const prepared = await prepareNativeAuthorityCard(context);
|
|
143
|
+
if (prepared.kind !== 'fact')
|
|
144
|
+
return { refused: prepared.diagnostic, code: prepared.code };
|
|
145
|
+
const request = nativeAuthorityRequestIdentity(action), card = presented ? decodeAuthorityCard(presented, context.limits) : prepared.value.card;
|
|
146
|
+
const answer = await answerNativeAuthorityCard(card, request, context);
|
|
147
|
+
current();
|
|
148
|
+
if ('kind' in answer)
|
|
149
|
+
return { refused: answer.diagnostic, code: answer.code };
|
|
150
|
+
if (answer.status === 'refused')
|
|
151
|
+
return { refused: `REFUSE·${answer.code}`, code: answer.code };
|
|
152
|
+
return { authority: prepared.value.line, request, decision: answer.status === 'observed' ? answer.decision : 'open', needs: answer.status === 'open' ? answer.needs : prepared.value.needs };
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
return { refused: error instanceof Error ? error.message : String(error), code: 'authority-preflight/context' };
|
|
156
|
+
}
|
|
157
|
+
};
|
|
@@ -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
|
|
8
|
-
export declare const
|
|
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
|
|
8
|
-
export const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
}
|
|
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;
|