@lapxo/head 0.0.0-stage → 0.1.3

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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/TARGET.bound +786 -0
  3. package/dist/src/access/contract.d.ts +71 -0
  4. package/dist/src/access/contract.js +6 -0
  5. package/dist/src/access/obtain.d.ts +19 -0
  6. package/dist/src/access/obtain.js +70 -0
  7. package/dist/src/adopt/adopt.d.ts +12 -0
  8. package/dist/src/adopt/adopt.js +40 -0
  9. package/dist/src/compile/pin.d.ts +26 -0
  10. package/dist/src/compile/pin.js +74 -0
  11. package/dist/src/core/act.d.ts +18 -0
  12. package/dist/src/core/act.js +18 -0
  13. package/dist/src/core/answer.d.ts +102 -0
  14. package/dist/src/core/answer.js +26 -0
  15. package/dist/src/core/device.d.ts +26 -0
  16. package/dist/src/core/device.js +27 -0
  17. package/dist/src/core/lock.d.ts +11 -0
  18. package/dist/src/core/lock.js +13 -0
  19. package/dist/src/core/package.d.ts +54 -0
  20. package/dist/src/core/package.js +65 -0
  21. package/dist/src/core/read.d.ts +54 -0
  22. package/dist/src/core/read.js +64 -0
  23. package/dist/src/core/receipt.d.ts +13 -0
  24. package/dist/src/core/receipt.js +12 -0
  25. package/dist/src/core/surface.d.ts +6 -0
  26. package/dist/src/core/surface.js +7 -0
  27. package/dist/src/core/trust.d.ts +7 -0
  28. package/dist/src/core/trust.js +19 -0
  29. package/dist/src/core/worlds.d.ts +10 -0
  30. package/dist/src/core/worlds.js +9 -0
  31. package/dist/src/flows/act.d.ts +7 -0
  32. package/dist/src/flows/act.js +43 -0
  33. package/dist/src/flows/answer.d.ts +12 -0
  34. package/dist/src/flows/answer.js +25 -0
  35. package/dist/src/flows/call.d.ts +50 -0
  36. package/dist/src/flows/call.js +76 -0
  37. package/dist/src/flows/device.d.ts +26 -0
  38. package/dist/src/flows/device.js +32 -0
  39. package/dist/src/flows/plan.d.ts +8 -0
  40. package/dist/src/flows/plan.js +51 -0
  41. package/dist/src/flows/read.d.ts +9 -0
  42. package/dist/src/flows/read.js +95 -0
  43. package/dist/src/flows/world.d.ts +12 -0
  44. package/dist/src/flows/world.js +29 -0
  45. package/dist/src/index.d.ts +2 -0
  46. package/dist/src/index.js +138 -0
  47. package/dist/src/onboard/first.d.ts +7 -0
  48. package/dist/src/onboard/first.js +21 -0
  49. package/dist/src/places/admit.d.ts +7 -0
  50. package/dist/src/places/admit.js +32 -0
  51. package/dist/src/places/found.d.ts +7 -0
  52. package/dist/src/places/found.js +60 -0
  53. package/dist/src/ports/access/authorizer.d.ts +10 -0
  54. package/dist/src/ports/access/authorizer.js +34 -0
  55. package/dist/src/ports/access/flow.d.ts +13 -0
  56. package/dist/src/ports/access/flow.js +130 -0
  57. package/dist/src/ports/access/page.d.ts +12 -0
  58. package/dist/src/ports/access/page.js +50 -0
  59. package/dist/src/ports/access/person.d.ts +26 -0
  60. package/dist/src/ports/access/person.js +41 -0
  61. package/dist/src/ports/adapters/request.d.ts +41 -0
  62. package/dist/src/ports/adapters/request.js +69 -0
  63. package/dist/src/ports/bound.d.ts +61 -0
  64. package/dist/src/ports/bound.js +85 -0
  65. package/dist/src/ports/cache.d.ts +4 -0
  66. package/dist/src/ports/cache.js +14 -0
  67. package/dist/src/ports/documents.d.ts +34 -0
  68. package/dist/src/ports/documents.js +64 -0
  69. package/dist/src/ports/keychain.d.ts +8 -0
  70. package/dist/src/ports/keychain.js +12 -0
  71. package/dist/src/ports/keys.d.ts +14 -0
  72. package/dist/src/ports/keys.js +20 -0
  73. package/dist/src/ports/place.d.ts +11 -0
  74. package/dist/src/ports/place.js +32 -0
  75. package/dist/src/ports/process.d.ts +29 -0
  76. package/dist/src/ports/process.js +51 -0
  77. package/dist/src/ports/signers.d.ts +6 -0
  78. package/dist/src/ports/signers.js +14 -0
  79. package/dist/src/ports/spare.d.ts +1 -0
  80. package/dist/src/ports/spare.js +33 -0
  81. package/dist/src/ports/stores/credential-manager.d.ts +3 -0
  82. package/dist/src/ports/stores/credential-manager.js +22 -0
  83. package/dist/src/ports/stores/keychain.d.ts +3 -0
  84. package/dist/src/ports/stores/keychain.js +16 -0
  85. package/dist/src/ports/stores/secret-tool.d.ts +3 -0
  86. package/dist/src/ports/stores/secret-tool.js +16 -0
  87. package/dist/src/ports/surface.d.ts +126 -0
  88. package/dist/src/ports/surface.js +48 -0
  89. package/dist/src/ports/warm.d.ts +19 -0
  90. package/dist/src/ports/warm.js +46 -0
  91. package/dist/src/resources/view.d.ts +1 -0
  92. package/dist/src/resources/view.js +1 -0
  93. package/dist/src/signer.d.ts +2 -0
  94. package/dist/src/signer.js +14 -0
  95. package/dist/src/sources/access.d.ts +17 -0
  96. package/dist/src/sources/access.js +39 -0
  97. package/dist/src/sources/connect.d.ts +13 -0
  98. package/dist/src/sources/connect.js +130 -0
  99. package/dist/src/tools/access.d.ts +1 -0
  100. package/dist/src/tools/access.js +1 -0
  101. package/dist/src/tools/act.d.ts +1 -0
  102. package/dist/src/tools/act.js +1 -0
  103. package/dist/src/tools/admit.d.ts +1 -0
  104. package/dist/src/tools/admit.js +1 -0
  105. package/dist/src/tools/card.d.ts +3 -0
  106. package/dist/src/tools/card.js +13 -0
  107. package/dist/src/tools/connect.d.ts +1 -0
  108. package/dist/src/tools/connect.js +1 -0
  109. package/dist/src/tools/key.d.ts +3 -0
  110. package/dist/src/tools/key.js +11 -0
  111. package/dist/src/tools/land.d.ts +1 -0
  112. package/dist/src/tools/land.js +1 -0
  113. package/dist/src/tools/look.d.ts +8 -0
  114. package/dist/src/tools/look.js +41 -0
  115. package/dist/src/tools/place.d.ts +1 -0
  116. package/dist/src/tools/place.js +1 -0
  117. package/dist/src/tools/places.d.ts +7 -0
  118. package/dist/src/tools/places.js +32 -0
  119. package/dist/src/tools/plan.d.ts +1 -0
  120. package/dist/src/tools/plan.js +1 -0
  121. package/dist/src/tools/share.d.ts +1 -0
  122. package/dist/src/tools/share.js +1 -0
  123. package/dist/src/walk/context.d.ts +7 -0
  124. package/dist/src/walk/context.js +23 -0
  125. package/package.json +36 -4
  126. package/README.md +0 -3
@@ -0,0 +1,64 @@
1
+ import { dirname, extname, join } from 'node:path';
2
+ import { within } from "../core/answer.js";
3
+ import { members, valueOf } from "../core/lock.js";
4
+ import { sayIn } from "../core/surface.js";
5
+ const limits = (context) => ['adapter/request/wait', 'adapter/request/document-bytes'].map((k) => Number(valueOf(context.lock, k).split('..')[1]));
6
+ export const kindOf = (at, type = '') => (extname(URL.canParse(at) ? new URL(at).pathname : at).slice(1)
7
+ || (type.split(';')[0].split('/')[1] ?? '').split('+').at(-1).replace(/^x-/, '') || 'octet-stream').toLowerCase();
8
+ /**
9
+ * A document the person names: a file inside the place or a folder the client shares, or an encrypted address read with
10
+ * nothing attached — signed or expiring as it may be — and, when its digest is named, followed wherever it moves to the
11
+ * bytes that digest names, and only to them. Anything else is not read. Its kind is named, never read for: its file's
12
+ * extension, its address's, or the media type it was served as; bytes that say none are octet-stream.
13
+ */
14
+ export const documentOf = async (context, place, at, named = '') => {
15
+ const [wait, most] = limits(context), transport = URL.canParse(at) ? await context.adapter(at) : undefined;
16
+ if (URL.canParse(at))
17
+ return named ? ((bytes) => bytes && { bytes, kind: kindOf(at) })(await transport?.published(at, named, most, wait)) : ((got) => got && { bytes: got.bytes, kind: kindOf(at, got.type) })(await transport?.get(at, wait));
18
+ return ((bytes) => bytes && { bytes, kind: kindOf(at) })((await within(context, place, at)) ? context.files.take(at) : undefined);
19
+ };
20
+ /** A document read with the access a source the place connected holds: sent to that source's own origin alone, never redirected with it; a move is followed with nothing attached, only to the bytes its named digest names. */
21
+ export const fetchedAs = async (context, at, via, got, named, accept) => {
22
+ const [say, [wait, most]] = [sayIn(context.lock), limits(context)];
23
+ if (!URL.canParse(at))
24
+ return { said: [say('document-unread', { document: at })] };
25
+ if (new URL(at).origin !== new URL(got.plan.origin).origin)
26
+ return { said: [say('fetch-cross-origin', { document: at, via })] };
27
+ const transport = await context.adapter(at), answer = transport ? await transport.fetched(at, got.attachment, accept, most, wait) : undefined;
28
+ if (answer?.kind !== 'got')
29
+ return { said: [say('document-unread', { document: at })] };
30
+ if (answer.status >= 300 && answer.status < 400 && answer.location !== undefined) {
31
+ if (!named)
32
+ return { said: [say('fetch-redirect', { document: at })] };
33
+ return ((bytes) => (bytes ? { bytes, kind: kindOf(at) } : { said: [say('document-unread', { document: at })] }))(await transport.published(new URL(answer.location, at).href, named, most, wait));
34
+ }
35
+ return answer.status < 300 ? { bytes: answer.bytes, kind: kindOf(at, answer.type) } : { said: [say('fetch-status', { document: at, via, status: String(answer.status) })] };
36
+ };
37
+ /**
38
+ * Every byte a package's lines name, by the path each names: read where a line of the same scope says — an encrypted
39
+ * address, signed or expiring, read with nothing attached and only to the bytes its digest names — or else beside the
40
+ * package's lock, in its folder or at its address. A path outside the package is never read. And what a place laid
41
+ * for a source it connected — its document and the overlay beside it — by the path the place keeps them at. And where
42
+ * an origin says its documents are: the catalog it publishes at its well-known address (RFC 9727), then the paths head's
43
+ * lock names, each once.
44
+ */
45
+ export const besideOf = async (context, document, lines) => {
46
+ const [wait, most] = limits(context), read = new Map();
47
+ for (const l of lines.filter((one) => one.needs && one.measure === 'digest' && !one.needs.split('/').includes('..') && !one.needs.startsWith('/'))) {
48
+ const at = lines.find((o) => o.scope === l.scope && o.measure === 'id')?.value ?? (URL.canParse(document) ? new URL(l.needs, document).href : join(dirname(document), l.needs));
49
+ read.set(l.needs, URL.canParse(at) ? await (await context.adapter(at))?.published(at, l.value ?? '', most, wait) : context.files.take(at));
50
+ }
51
+ return read;
52
+ };
53
+ export const describedAt = async (context, origin) => {
54
+ const [wait] = limits(context), catalog = await (await context.adapter(origin))?.get(new URL(valueOf(context.lock, 'use/catalog'), origin).href, wait), rel = valueOf(context.lock, 'use/catalog-rel');
55
+ const listed = (() => { try {
56
+ return JSON.parse(new TextDecoder().decode(catalog?.bytes ?? new Uint8Array())).linkset?.flatMap((l) => (l[rel] ?? []).flatMap((d) => d.href ?? [])) ?? [];
57
+ }
58
+ catch {
59
+ return [];
60
+ } })();
61
+ return [...new Set([...listed, ...members(valueOf(context.lock, 'use/documents'))].flatMap((at) => (URL.canParse(at, origin) ? [new URL(at, origin).href] : [])))];
62
+ };
63
+ export const laidOf = (context, place, source) => ((at) => context.files.list(place, at).filter((f) => new RegExp(`^${source}\\.(overlay\\.)?[^.]+$`).test(f))
64
+ .map((f) => [`${at}/${f}`, context.files.take(join(place, at, f)) ?? new Uint8Array()]))(valueOf(context.lock, 'place/connected'));
@@ -0,0 +1,8 @@
1
+ export interface KeyStore {
2
+ readonly read: (account: string) => string | undefined;
3
+ readonly write: (account: string, secret: string) => void;
4
+ readonly made?: (account: string) => string | undefined;
5
+ }
6
+ 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;
@@ -0,0 +1,12 @@
1
+ import { store as credentialManager } from "./stores/credential-manager.js";
2
+ import { store as keychainOfMac } from "./stores/keychain.js";
3
+ import { store as secretTool } from "./stores/secret-tool.js";
4
+ import { newKey, publicOf } from "./keys.js";
5
+ export const plain = (word) => { if (!/^[\w.@/+=-]+$/.test(word))
6
+ 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]);
@@ -0,0 +1,14 @@
1
+ export interface Key {
2
+ readonly secret: string;
3
+ readonly publicKey: string;
4
+ }
5
+ /** A new device key: its secret as base64 PKCS8 for the keychain to keep, its public half as base64 SPKI for a lock to admit. */
6
+ export declare const newKey: () => Key;
7
+ /**
8
+ * One signing lot answered as the signing-lot/1 contract asks: every line the lot carries, already the bytes a signature
9
+ * covers and named by this key alone, signed in order under the algorithm the host was told. A lot naming any other key,
10
+ * or bytes not in their canonical form, is refused whole: topos's own request reader decides, never this file.
11
+ */
12
+ export declare const signLot: (input: string, secret: string, keyId: string, algorithm: string) => string;
13
+ /** The public half of a kept secret, as a lock admits it. */
14
+ export declare const publicOf: (secret: string) => string;
@@ -0,0 +1,20 @@
1
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from 'node:crypto';
2
+ import { formatSignature, signerRequestBytes, signerResponseBytes } from '@lapxo/topos/wire';
3
+ /** A new device key: its secret as base64 PKCS8 for the keychain to keep, its public half as base64 SPKI for a lock to admit. */
4
+ export const newKey = () => ((pair) => ({
5
+ secret: pair.privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64'),
6
+ publicKey: pair.publicKey.export({ type: 'spki', format: 'der' }).toString('base64'),
7
+ }))(generateKeyPairSync('ed25519'));
8
+ /**
9
+ * One signing lot answered as the signing-lot/1 contract asks: every line the lot carries, already the bytes a signature
10
+ * covers and named by this key alone, signed in order under the algorithm the host was told. A lot naming any other key,
11
+ * or bytes not in their canonical form, is refused whole: topos's own request reader decides, never this file.
12
+ */
13
+ export const signLot = (input, secret, keyId, algorithm) => {
14
+ const bytes = input.endsWith('\n') ? input.slice(0, -1).split('\n') : [];
15
+ signerRequestBytes({ bytes, keyId, algorithm });
16
+ const key = createPrivateKey({ key: Buffer.from(secret, 'base64'), format: 'der', type: 'pkcs8' });
17
+ return signerResponseBytes(bytes.map((one) => ({ keyId, signature: formatSignature(algorithm, sign(null, Buffer.from(one, 'utf8'), key).toString('base64')) })));
18
+ };
19
+ /** The public half of a kept secret, as a lock admits it. */
20
+ export const publicOf = (secret) => createPublicKey(createPrivateKey({ key: Buffer.from(secret, 'base64'), format: 'der', type: 'pkcs8' })).export({ type: 'spki', format: 'der' }).toString('base64');
@@ -0,0 +1,11 @@
1
+ import type { Said } from '../core/lock.ts';
2
+ /** The lines head's own lock declares, decoded; a missing lock is an error, never an empty lock. */
3
+ export declare const lockAt: (dir: string) => readonly Said[];
4
+ /** A root the client shares, as the folder it names; a root that names no file folder is no place. */
5
+ export declare const folderOf: (uri: string) => string | undefined;
6
+ /** Bytes laid at a path inside a place, its folders made; a path outside the place is refused. */
7
+ export declare const layAt: (place: string, rel: string, bytes: Uint8Array) => void;
8
+ export declare const takeAt: (path: string) => Uint8Array | undefined;
9
+ export declare const hasAt: (place: string, rel: string) => boolean;
10
+ export declare const dropAt: (place: string, rel: string) => void;
11
+ export declare const listAt: (place: string, rel: string) => readonly string[];
@@ -0,0 +1,32 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve, sep } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { LOCK } from '@lapxo/topos/wire';
5
+ import { declaredOf } from "../core/lock.js";
6
+ /** The lines head's own lock declares, decoded; a missing lock is an error, never an empty lock. */
7
+ export const lockAt = (dir) => declaredOf(readFileSync(join(dir, LOCK), 'utf8'));
8
+ /** A root the client shares, as the folder it names; a root that names no file folder is no place. */
9
+ export const folderOf = (uri) => { try {
10
+ return fileURLToPath(uri);
11
+ }
12
+ catch {
13
+ return undefined;
14
+ } };
15
+ const inside = (place, rel) => ((at) => { if (!at.startsWith(`${resolve(place)}${sep}`))
16
+ throw Error('REFUSE·place a path outside its place'); return at; })(resolve(place, rel));
17
+ /** Bytes laid at a path inside a place, its folders made; a path outside the place is refused. */
18
+ export const layAt = (place, rel, bytes) => { const at = inside(place, rel); mkdirSync(dirname(at), { recursive: true }); writeFileSync(at, bytes); };
19
+ export const takeAt = (path) => { try {
20
+ return readFileSync(path);
21
+ }
22
+ catch {
23
+ return undefined;
24
+ } };
25
+ export const hasAt = (place, rel) => existsSync(resolve(place, rel));
26
+ export const dropAt = (place, rel) => rmSync(inside(place, rel), { force: true });
27
+ export const listAt = (place, rel) => { try {
28
+ return readdirSync(inside(place, rel));
29
+ }
30
+ catch {
31
+ return [];
32
+ } };
@@ -0,0 +1,29 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ export interface Printed {
3
+ readonly code: number;
4
+ readonly out: readonly string[];
5
+ readonly err: readonly string[];
6
+ readonly refusal?: Readonly<Record<string, string>>;
7
+ }
8
+ export interface Bounds {
9
+ readonly wait: number;
10
+ readonly most: number;
11
+ readonly signal?: AbortSignal;
12
+ }
13
+ /** A process head starts in a group of its own, so stopping it stops everything it started. */
14
+ export declare const grouped: (command: string, args: readonly string[], at: {
15
+ readonly cwd?: string;
16
+ readonly env?: NodeJS.ProcessEnv;
17
+ }, pipes?: number) => ChildProcess;
18
+ /**
19
+ * A process run beside head, never blocking it: what it printed, out and err, line by line, and its exit, which is
20
+ * transport and never the verdict. Its whole group is stopped when the wait runs out, when it prints more bytes than it
21
+ * may, when the asker cancels or when head exits; the exit is then -1 and the cause is err's last line.
22
+ */
23
+ export declare const spawned: (command: string, args: readonly string[], at: {
24
+ readonly cwd?: string;
25
+ readonly env?: NodeJS.ProcessEnv;
26
+ readonly input?: string;
27
+ }, bounds: Bounds) => Promise<Printed>;
28
+ /** What a process head already started prints from now on, within the same bounds. */
29
+ export declare const watched: (child: ChildProcess, bounds: Bounds) => Promise<Printed>;
@@ -0,0 +1,51 @@
1
+ import { spawn } from 'node:child_process';
2
+ const running = new Set();
3
+ process.once('exit', () => { for (const stop of running)
4
+ stop(); });
5
+ /** A process head starts in a group of its own, so stopping it stops everything it started. */
6
+ export const grouped = (command, args, at, pipes = 3) => spawn(command, [...args], { cwd: at.cwd, env: at.env, stdio: Array(pipes).fill('pipe'), detached: process.platform !== 'win32', windowsHide: true });
7
+ /**
8
+ * A process run beside head, never blocking it: what it printed, out and err, line by line, and its exit, which is
9
+ * transport and never the verdict. Its whole group is stopped when the wait runs out, when it prints more bytes than it
10
+ * may, when the asker cancels or when head exits; the exit is then -1 and the cause is err's last line.
11
+ */
12
+ export const spawned = (command, args, at, bounds) => ((child) => { child.stdin.on('error', () => undefined).end(at.input ?? ''); return watched(child, bounds); })(grouped(command, args, at));
13
+ /** What a process head already started prints from now on, within the same bounds. */
14
+ export const watched = (child, bounds) => new Promise((resolve) => {
15
+ const [out, err] = [[], []];
16
+ let [printed, cause, done] = [0, undefined, false];
17
+ const kill = () => { try {
18
+ if (child.pid !== undefined && process.platform !== 'win32')
19
+ process.kill(-child.pid, 'SIGKILL');
20
+ else
21
+ child.kill('SIGKILL');
22
+ }
23
+ catch {
24
+ child.kill('SIGKILL');
25
+ } };
26
+ const stop = (why) => { cause ??= why; kill(); };
27
+ const signal = AbortSignal.any([...(bounds.wait > 0 ? [AbortSignal.timeout(bounds.wait)] : []), ...(bounds.signal ? [bounds.signal] : [])]);
28
+ const cancel = () => stop(bounds.signal?.aborted ? 'cancelled' : `waited ${bounds.wait} ms`);
29
+ const finish = (code) => {
30
+ if (done)
31
+ return;
32
+ done = true;
33
+ signal.removeEventListener('abort', cancel);
34
+ running.delete(kill);
35
+ const lines = (chunks) => Buffer.concat(chunks).toString('utf8').split('\n').filter(Boolean);
36
+ resolve({ code: cause === undefined ? code : -1, out: lines(out), err: [...lines(err), ...(cause === undefined ? [] : [cause])] });
37
+ };
38
+ const take = (into) => (chunk) => { printed += chunk.length; if (printed > bounds.most)
39
+ stop(`printed more than ${bounds.most} bytes`);
40
+ else
41
+ into.push(chunk); };
42
+ running.add(kill);
43
+ child.stdout.on('data', take(out));
44
+ child.stderr.on('data', take(err));
45
+ child.on('error', (error) => { cause ??= error.message; finish(-1); });
46
+ child.on('close', (code) => finish(code ?? -1));
47
+ if (signal.aborted)
48
+ cancel();
49
+ else
50
+ signal.addEventListener('abort', cancel, { once: true });
51
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The host's registry bound reads to bind a declared signer name to a program: head's own signer, started by this node,
3
+ * told the keychain service, the account its secret is kept under, the key it signs as, the algorithm and the store this
4
+ * host keeps secrets in. It holds no secret; it lives beside nothing a repository ships.
5
+ */
6
+ export declare const registryFor: (name: string, service: string, account: string, key: string, algorithm: string, store?: string) => string;
@@ -0,0 +1,14 @@
1
+ import { writeFileSync, mkdtempSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ /**
6
+ * The host's registry bound reads to bind a declared signer name to a program: head's own signer, started by this node,
7
+ * told the keychain service, the account its secret is kept under, the key it signs as, the algorithm and the store this
8
+ * host keeps secrets in. It holds no secret; it lives beside nothing a repository ships.
9
+ */
10
+ export const registryFor = (name, service, account, key, algorithm, store = 'keychain') => {
11
+ const at = join(mkdtempSync(join(tmpdir(), 'head-signers-')), 'signers.json');
12
+ writeFileSync(at, JSON.stringify({ [name]: { kind: 'command', executable: process.execPath, args: [fileURLToPath(new URL(`../signer${import.meta.url.slice(import.meta.url.lastIndexOf('.'))}`, import.meta.url)), service, account, key, algorithm, store] } }));
13
+ return at;
14
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { readFileSync } from 'node:fs';
10
+ import { Socket } from 'node:net';
11
+ import { pathToFileURL } from 'node:url';
12
+ /**
13
+ * A process started before it is asked: it loads what the program it will run imports, says it is ready on its fourth
14
+ * pipe, and waits there for one ask, the folder, the arguments and the variables a process started then would have been
15
+ * given. It takes them as its own, closes that pipe and runs the program once, as that process would have. An ask that
16
+ * never comes ends it.
17
+ */
18
+ const [program] = process.argv.slice(2);
19
+ const url = pathToFileURL(program), pipe = new Socket({ fd: 3, readable: true, writable: true });
20
+ const asked = new Promise((resolve) => { let text = ''; pipe.setEncoding('utf8').on('data', (chunk) => { text += chunk; }).on('error', () => undefined).on('close', () => resolve(text)); });
21
+ await Promise.all([...readFileSync(program, 'utf8').matchAll(/^import [^'"]*['"](\.{1,2}\/[^'"]+)['"];?$/gm)].map(([, path]) => import(__rewriteRelativeImportExtension(new URL(path, url).href)).catch(() => undefined)));
22
+ pipe.write('ready\n');
23
+ const text = await asked;
24
+ if (!text)
25
+ process.exit(0);
26
+ const { cwd, args, env } = JSON.parse(text);
27
+ for (const name of Object.keys(process.env))
28
+ if (!(name in env))
29
+ delete process.env[name];
30
+ Object.assign(process.env, env);
31
+ process.chdir(cwd);
32
+ process.argv = [process.argv[0], program, ...args];
33
+ await import(__rewriteRelativeImportExtension(url.href));
@@ -0,0 +1,3 @@
1
+ import type { KeyStore } from '../keychain.ts';
2
+ /** The Windows Credential Manager: the secret travels inside the program head writes to PowerShell's stdin, base64, and comes back on its stdout; never an argument a process list shows. */
3
+ export declare const store: (service: string) => KeyStore;
@@ -0,0 +1,22 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { plain } from "../keychain.js";
3
+ /** Windows PowerShell, reading its whole program from stdin: the password vault of the Credential Manager, an entry by its resource and user. */
4
+ const vault = (program) => execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '-'], {
5
+ input: ['$ErrorActionPreference = \'Stop\'', '[void][Windows.Security.Credentials.PasswordVault, Windows.Security.Credentials, ContentType = WindowsRuntime]', '$v = New-Object Windows.Security.Credentials.PasswordVault', ...program, ''].join('\n'),
6
+ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'],
7
+ });
8
+ /** The Windows Credential Manager: the secret travels inside the program head writes to PowerShell's stdin, base64, and comes back on its stdout; never an argument a process list shows. */
9
+ export const store = (service) => ({
10
+ read: (account) => {
11
+ try {
12
+ return vault([`$r = '${plain(service)}'; $u = '${plain(account)}'`, 'try { $c = $v.Retrieve($r, $u); $c.RetrievePassword(); [Console]::Out.Write($c.Password) } catch { exit 1 }']).trim() || undefined;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ },
18
+ write: (account, secret) => {
19
+ vault([`$r = '${plain(service)}'; $u = '${plain(account)}'; $b = '${Buffer.from(plain(secret)).toString('base64')}'`, 'try { $v.Remove($v.Retrieve($r, $u)) } catch { }',
20
+ '$v.Add((New-Object Windows.Security.Credentials.PasswordCredential($r, $u, [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($b)))))']);
21
+ },
22
+ });
@@ -0,0 +1,3 @@
1
+ import type { KeyStore } from '../keychain.ts';
2
+ /** The macOS keychain through the system's own command: a secret crosses its stdin and stdout only, never an argument a process list shows. */
3
+ export declare const store: (service: string) => KeyStore;
@@ -0,0 +1,16 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { plain } from "../keychain.js";
3
+ /** The macOS keychain through the system's own command: a secret crosses its stdin and stdout only, never an argument a process list shows. */
4
+ export const store = (service) => ({
5
+ read: (account) => {
6
+ try {
7
+ return execFileSync('security', ['find-generic-password', '-a', plain(account), '-s', plain(service), '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || undefined;
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ },
13
+ write: (account, secret) => {
14
+ execFileSync('security', ['-i'], { input: `add-generic-password -U -a ${plain(account)} -s ${plain(service)} -w ${plain(secret)}\n`, stdio: ['pipe', 'ignore', 'ignore'] });
15
+ },
16
+ });
@@ -0,0 +1,3 @@
1
+ import type { KeyStore } from '../keychain.ts';
2
+ /** The Secret Service of a Linux session through libsecret's own command: an entry found by its service and account attributes, the secret crossing stdin and stdout only. */
3
+ export declare const store: (service: string) => KeyStore;
@@ -0,0 +1,16 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { plain } from "../keychain.js";
3
+ /** The Secret Service of a Linux session through libsecret's own command: an entry found by its service and account attributes, the secret crossing stdin and stdout only. */
4
+ export const store = (service) => ({
5
+ read: (account) => {
6
+ try {
7
+ return execFileSync('secret-tool', ['lookup', 'service', plain(service), 'account', plain(account)], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || undefined;
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ },
13
+ write: (account, secret) => {
14
+ execFileSync('secret-tool', ['store', `--label=${plain(service)} ${plain(account)}`, 'service', plain(service), 'account', plain(account)], { input: plain(secret), stdio: ['pipe', 'ignore', 'ignore'] });
15
+ },
16
+ });
@@ -0,0 +1,126 @@
1
+ import type { Said } from '../core/lock.ts';
2
+ export interface Field {
3
+ readonly name: string;
4
+ readonly about: string;
5
+ readonly type: string;
6
+ readonly required: boolean;
7
+ }
8
+ export interface Tool {
9
+ readonly name: string;
10
+ readonly about: string;
11
+ readonly fields: readonly Field[];
12
+ readonly hints: readonly string[];
13
+ }
14
+ export interface Resource {
15
+ readonly name: string;
16
+ readonly about: string;
17
+ readonly template: string;
18
+ readonly mime: string;
19
+ }
20
+ export interface Prompt {
21
+ readonly name: string;
22
+ readonly about: string;
23
+ readonly text: string;
24
+ readonly line: string;
25
+ readonly fields: readonly Field[];
26
+ }
27
+ export interface Surface {
28
+ readonly name: string;
29
+ readonly version: string;
30
+ readonly tools: readonly Tool[];
31
+ readonly resources: readonly Resource[];
32
+ readonly prompts: readonly Prompt[];
33
+ }
34
+ /** Everything a client is offered, read off the lock alone: its tools, resources and prompts, each with the words a model sees. */
35
+ export declare const surfaceOf: (lock: readonly Said[]) => Surface;
36
+ export declare const refusedOn: (lock: readonly Said[], platform: string) => string | undefined;
37
+ /**
38
+ * The fields a URI gives a resource template, or none when it is not one of its URIs: {name} matches one path step,
39
+ * {?a,b} names query fields any of which may be absent, in any order, each decoded.
40
+ */
41
+ export declare const matchTemplate: (template: string, uri: string) => Readonly<Record<string, string>> | undefined;
42
+ export interface Cell {
43
+ readonly code: string;
44
+ readonly state: string;
45
+ readonly next: {
46
+ readonly tool: string;
47
+ readonly args: Readonly<Record<string, string>>;
48
+ } | {
49
+ readonly none: string;
50
+ };
51
+ readonly fields: Readonly<Record<string, string>>;
52
+ readonly head?: string;
53
+ }
54
+ /** A said code as the cell head answers with: its state and next from the lock, the next's arguments read from the cell's own fields; a bound refusal a field carries keeps bound's code, head's own beside it. */
55
+ export declare const cellOf: (lock: readonly Said[], key: string, given: Readonly<Record<string, string | undefined>>) => Cell;
56
+ export declare const cellLine: (cell: Cell) => string;
57
+ /** An answer as cells and the lines bound printed beside them, read back from what head said: the shape every tool result has. */
58
+ export declare const shapeOf: (answer: readonly string[]) => {
59
+ readonly cells: readonly Cell[];
60
+ readonly lines: readonly string[];
61
+ };
62
+ /** The one output schema every tool declares: cells in the states the lock names, and the lines bound printed. */
63
+ export declare const shapeSchema: (lock: readonly Said[]) => {
64
+ type: "object";
65
+ required: string[];
66
+ properties: {
67
+ cells: {
68
+ type: string;
69
+ items: {
70
+ type: string;
71
+ required: string[];
72
+ properties: {
73
+ code: {
74
+ type: string;
75
+ };
76
+ state: {
77
+ enum: string[];
78
+ };
79
+ head: {
80
+ type: string;
81
+ };
82
+ next: {
83
+ oneOf: ({
84
+ type: string;
85
+ required: string[];
86
+ properties: {
87
+ tool: {
88
+ type: string;
89
+ };
90
+ args: {
91
+ type: string;
92
+ additionalProperties: {
93
+ type: string;
94
+ };
95
+ };
96
+ none?: undefined;
97
+ };
98
+ } | {
99
+ type: string;
100
+ required: string[];
101
+ properties: {
102
+ none: {
103
+ type: string;
104
+ };
105
+ tool?: undefined;
106
+ args?: undefined;
107
+ };
108
+ })[];
109
+ };
110
+ fields: {
111
+ type: string;
112
+ additionalProperties: {
113
+ type: string;
114
+ };
115
+ };
116
+ };
117
+ };
118
+ };
119
+ lines: {
120
+ type: string;
121
+ items: {
122
+ type: string;
123
+ };
124
+ };
125
+ };
126
+ };
@@ -0,0 +1,48 @@
1
+ import { aboutOf, below, members, valueOf } from "../core/lock.js";
2
+ import { sayIn } from "../core/surface.js";
3
+ const nameOf = (line) => line.scope.split('/')[1];
4
+ const fieldsOf = (lock, scope) => ((required) => members(valueOf(lock, scope)).map((name) => ({
5
+ name, about: aboutOf(lock, `${scope}/${name}`), type: valueOf(lock, `${scope}/${name}`), required: required.includes(name)
6
+ })))(members(valueOf(lock, `${scope}/required`)));
7
+ /** Everything a client is offered, read off the lock alone: its tools, resources and prompts, each with the words a model sees. */
8
+ export const surfaceOf = (lock) => ({
9
+ name: valueOf(lock, 'name'), version: valueOf(lock, 'version'),
10
+ tools: below(lock, 'tool').map((line) => ({ name: nameOf(line), about: line.about ?? '', fields: fieldsOf(lock, line.scope), hints: members(valueOf(lock, `${line.scope}/hints`)) })),
11
+ resources: below(lock, 'resource').map((line) => ({ name: nameOf(line), about: line.about ?? '', template: line.value ?? '', mime: valueOf(lock, `${line.scope}/mime`) })),
12
+ prompts: below(lock, 'prompt').map((line) => ({ name: nameOf(line), about: line.about ?? '', text: aboutOf(lock, `${line.scope}/text`), line: aboutOf(lock, `${line.scope}/line`), fields: fieldsOf(lock, line.scope) })),
13
+ });
14
+ export const refusedOn = (lock, platform) => ((supported) => (supported.includes(platform) ? undefined : sayIn(lock)('platform', { platform, supported: supported.join('|') })))(members(valueOf(lock, 'platform/supported')));
15
+ /**
16
+ * The fields a URI gives a resource template, or none when it is not one of its URIs: {name} matches one path step,
17
+ * {?a,b} names query fields any of which may be absent, in any order, each decoded.
18
+ */
19
+ export const matchTemplate = (template, uri) => {
20
+ const [path = '', query = ''] = template.split('{?');
21
+ const names = [...path.matchAll(/\{(\w+)\}/g)].map((m) => m[1]);
22
+ const pattern = new RegExp(`^${path.split(/\{\w+\}/).map((part) => part.replace(/[.*+?^$()|[\]\\]/g, '\\$&')).join('([^/?#]+)')}(?:\\?(.*))?$`);
23
+ const got = pattern.exec(uri);
24
+ if (got === null)
25
+ return undefined;
26
+ const params = new URLSearchParams(got[names.length + 1] ?? '');
27
+ return { ...Object.fromEntries(names.map((name, i) => [name, decodeURIComponent(got[i + 1])])),
28
+ ...Object.fromEntries(query.replace('}', '').split(',').filter(Boolean).flatMap((name) => ((v) => (v === null ? [] : [[name, v]]))(params.get(name)))) };
29
+ };
30
+ /** A said code as the cell head answers with: its state and next from the lock, the next's arguments read from the cell's own fields; a bound refusal a field carries keeps bound's code, head's own beside it. */
31
+ export const cellOf = (lock, key, given) => {
32
+ const fields = Object.fromEntries(Object.entries(given).filter((e) => e[1] !== undefined)), bound = Object.values(fields).map((v) => /^REFUSE·[\w-]+/.exec(v)?.[0]).find(Boolean);
33
+ const [tool, args] = [valueOf(lock, `said/${key}/next`), members(valueOf(lock, `said/${key}/next/args`)).map((a) => ((k, v) => [k, v.startsWith('$') ? fields[v.slice(1)] ?? '' : v])(a.split(':')[0], a.slice(a.indexOf(':') + 1)))];
34
+ return { code: bound ?? `head/${key}`, state: valueOf(lock, `said/${key}/state`), next: tool === 'none' ? { none: aboutOf(lock, `said/${key}/next`) } : { tool, args: Object.fromEntries(args) }, fields, ...(bound ? { head: `head/${key}` } : {}) };
35
+ };
36
+ export const cellLine = (cell) => [`said/${cell.code} state=${cell.state}`, ...('tool' in cell.next ? [`next=${cell.next.tool}`, ...Object.entries(cell.next.args).map(([k, v]) => `next.${k}=${JSON.stringify(v)}`)] : ['next=none', `next.why=${JSON.stringify(cell.next.none)}`]),
37
+ ...(cell.head ? [`head=${JSON.stringify(cell.head)}`] : []), ...Object.entries(cell.fields).map(([k, v]) => `${k}=${JSON.stringify(v)}`)].join(' ');
38
+ /** An answer as cells and the lines bound printed beside them, read back from what head said: the shape every tool result has. */
39
+ export const shapeOf = (answer) => ({ lines: answer.filter((l) => !l.startsWith('said/')),
40
+ cells: answer.filter((l) => l.startsWith('said/')).map((l) => ((code, rest) => ((t) => ({ code, state: t['state'] ?? '', next: t['next'] === 'none' || t['next'] === undefined ? { none: t['next.why'] ?? '' } : { tool: t['next'], args: Object.fromEntries(Object.entries(t).filter(([k]) => k.startsWith('next.')).map(([k, v]) => [k.slice(5), v])) },
41
+ fields: Object.fromEntries(Object.entries(t).filter(([k]) => !['state', 'next', 'head'].includes(k) && !k.startsWith('next.'))), ...(t['head'] ? { head: t['head'] } : {}) }))(Object.fromEntries([...rest.matchAll(/(\S+?)=("(?:[^"\\]|\\.)*"|\S+)/g)].map((m) => [m[1], m[2].startsWith('"') ? JSON.parse(m[2]) : m[2]]))))(l.slice('said/'.length, l.indexOf(' ')), l.slice(l.indexOf(' ') + 1))) });
42
+ /** The one output schema every tool declares: cells in the states the lock names, and the lines bound printed. */
43
+ export const shapeSchema = (lock) => ({ type: 'object', required: ['cells', 'lines'], properties: {
44
+ cells: { type: 'array', items: { type: 'object', required: ['code', 'state', 'next', 'fields'], properties: { code: { type: 'string' }, state: { enum: [...members(valueOf(lock, 'shape/states'))] }, head: { type: 'string' },
45
+ next: { oneOf: [{ type: 'object', required: ['tool', 'args'], properties: { tool: { type: 'string' }, args: { type: 'object', additionalProperties: { type: 'string' } } } }, { type: 'object', required: ['none'], properties: { none: { type: 'string' } } }] },
46
+ fields: { type: 'object', additionalProperties: { type: 'string' } } } } },
47
+ lines: { type: 'array', items: { type: 'string' } }
48
+ } });
@@ -0,0 +1,19 @@
1
+ import type { Bounds, Printed } from './process.ts';
2
+ /** How many processes head keeps started ahead of each program while it answers a call, and the variables node reads as it starts: a process started under other values of them is never asked. */
3
+ export declare const warmth: {
4
+ spares: number;
5
+ boot: readonly string[];
6
+ };
7
+ /** Keeps as many processes started ahead of a program as head's lock says; resolved when each has loaded what it runs. */
8
+ export declare const kept: (program: string) => Promise<unknown>;
9
+ /** Every process still waiting to be asked is stopped: head keeps none past the call it started them for. */
10
+ export declare const cooled: () => void;
11
+ /**
12
+ * A program run in a process started ahead under the same starting variables, handed the folder, arguments and
13
+ * variables a process started now would get, and watched within the same bounds; undefined when none is held ready,
14
+ * so the caller starts one as before and never waits on one still loading. Each process answers one ask and ends.
15
+ */
16
+ export declare const warmed: (program: string, args: readonly string[], at: {
17
+ readonly cwd: string;
18
+ readonly env: NodeJS.ProcessEnv;
19
+ }, bounds: Bounds) => Promise<Printed> | undefined;