@lapxo/head 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/TARGET.bound +443 -0
- package/dist/src/access/contract.d.ts +69 -0
- package/dist/src/access/contract.js +6 -0
- package/dist/src/access/obtain.d.ts +19 -0
- package/dist/src/access/obtain.js +70 -0
- package/dist/src/adopt/adopt.d.ts +12 -0
- package/dist/src/adopt/adopt.js +37 -0
- package/dist/src/compile/pin.d.ts +26 -0
- package/dist/src/compile/pin.js +74 -0
- package/dist/src/core/act.d.ts +17 -0
- package/dist/src/core/act.js +17 -0
- package/dist/src/core/answer.d.ts +90 -0
- package/dist/src/core/answer.js +23 -0
- package/dist/src/core/device.d.ts +26 -0
- package/dist/src/core/device.js +27 -0
- package/dist/src/core/keys.d.ts +14 -0
- package/dist/src/core/keys.js +20 -0
- package/dist/src/core/lock.d.ts +9 -0
- package/dist/src/core/lock.js +11 -0
- package/dist/src/core/read.d.ts +51 -0
- package/dist/src/core/read.js +60 -0
- package/dist/src/core/receipt.d.ts +13 -0
- package/dist/src/core/receipt.js +12 -0
- package/dist/src/core/surface.d.ts +45 -0
- package/dist/src/core/surface.js +32 -0
- package/dist/src/core/worlds.d.ts +10 -0
- package/dist/src/core/worlds.js +9 -0
- package/dist/src/flows/act.d.ts +7 -0
- package/dist/src/flows/act.js +43 -0
- package/dist/src/flows/answer.d.ts +12 -0
- package/dist/src/flows/answer.js +25 -0
- package/dist/src/flows/call.d.ts +50 -0
- package/dist/src/flows/call.js +76 -0
- package/dist/src/flows/device.d.ts +26 -0
- package/dist/src/flows/device.js +37 -0
- package/dist/src/flows/plan.d.ts +8 -0
- package/dist/src/flows/plan.js +51 -0
- package/dist/src/flows/read.d.ts +9 -0
- package/dist/src/flows/read.js +85 -0
- package/dist/src/flows/world.d.ts +12 -0
- package/dist/src/flows/world.js +29 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +119 -0
- package/dist/src/places/admit.d.ts +7 -0
- package/dist/src/places/admit.js +32 -0
- package/dist/src/places/found.d.ts +6 -0
- package/dist/src/places/found.js +59 -0
- package/dist/src/ports/access/flow.d.ts +13 -0
- package/dist/src/ports/access/flow.js +120 -0
- package/dist/src/ports/access/page.d.ts +12 -0
- package/dist/src/ports/access/page.js +50 -0
- package/dist/src/ports/access/person.d.ts +25 -0
- package/dist/src/ports/access/person.js +34 -0
- package/dist/src/ports/adapters/request.d.ts +38 -0
- package/dist/src/ports/adapters/request.js +67 -0
- package/dist/src/ports/bound.d.ts +51 -0
- package/dist/src/ports/bound.js +68 -0
- package/dist/src/ports/cache.d.ts +4 -0
- package/dist/src/ports/cache.js +14 -0
- package/dist/src/ports/keychain.d.ts +7 -0
- package/dist/src/ports/keychain.js +11 -0
- package/dist/src/ports/place.d.ts +10 -0
- package/dist/src/ports/place.js +26 -0
- package/dist/src/ports/process.d.ts +20 -0
- package/dist/src/ports/process.js +49 -0
- package/dist/src/ports/signers.d.ts +6 -0
- package/dist/src/ports/signers.js +14 -0
- package/dist/src/ports/stores/credential-manager.d.ts +3 -0
- package/dist/src/ports/stores/credential-manager.js +22 -0
- package/dist/src/ports/stores/keychain.d.ts +3 -0
- package/dist/src/ports/stores/keychain.js +16 -0
- package/dist/src/ports/stores/secret-tool.d.ts +3 -0
- package/dist/src/ports/stores/secret-tool.js +16 -0
- package/dist/src/resources/view.d.ts +1 -0
- package/dist/src/resources/view.js +1 -0
- package/dist/src/signer.d.ts +2 -0
- package/dist/src/signer.js +14 -0
- package/dist/src/sources/access.d.ts +8 -0
- package/dist/src/sources/access.js +34 -0
- package/dist/src/sources/connect.d.ts +10 -0
- package/dist/src/sources/connect.js +68 -0
- package/dist/src/tools/access.d.ts +1 -0
- package/dist/src/tools/access.js +1 -0
- package/dist/src/tools/act.d.ts +1 -0
- package/dist/src/tools/act.js +1 -0
- package/dist/src/tools/admit.d.ts +1 -0
- package/dist/src/tools/admit.js +1 -0
- package/dist/src/tools/card.d.ts +3 -0
- package/dist/src/tools/card.js +13 -0
- package/dist/src/tools/connect.d.ts +1 -0
- package/dist/src/tools/connect.js +1 -0
- package/dist/src/tools/key.d.ts +3 -0
- package/dist/src/tools/key.js +11 -0
- package/dist/src/tools/land.d.ts +1 -0
- package/dist/src/tools/land.js +1 -0
- package/dist/src/tools/look.d.ts +8 -0
- package/dist/src/tools/look.js +40 -0
- package/dist/src/tools/place.d.ts +1 -0
- package/dist/src/tools/place.js +1 -0
- package/dist/src/tools/plan.d.ts +1 -0
- package/dist/src/tools/plan.js +1 -0
- package/dist/src/walk/context.d.ts +7 -0
- package/dist/src/walk/context.js +23 -0
- package/package.json +35 -4
- package/README.md +0 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { aboutOf, members, valueOf } from "../../core/lock.js";
|
|
4
|
+
import { fill } from "../../core/surface.js";
|
|
5
|
+
import { open } from "./page.js";
|
|
6
|
+
/**
|
|
7
|
+
* The person at this device, asked through the richest channel the client declares, in the order head's lock lines name:
|
|
8
|
+
* the client's own form; head's one-time page, sent by the client as an address; the same page opened by this device's
|
|
9
|
+
* browser. A secret goes only through the page — never a form the model could see — and a yes only through what the
|
|
10
|
+
* client can ask, else it is the person's to sign. A visit to a provider comes back to such a page.
|
|
11
|
+
*/
|
|
12
|
+
export const personOf = (lock, client) => {
|
|
13
|
+
const wait = Number(valueOf(lock, 'access/wait').split('..')[1]);
|
|
14
|
+
const channel = (rule) => ((can) => members(valueOf(lock, rule)).find((c) => (c === 'device' ? true : can[c])))(client.can());
|
|
15
|
+
const offer = async (message, url, how) => (how === 'url' ? (await client.elicit({ mode: 'url', message, url, elicitationId: randomUUID() })).action === 'accept'
|
|
16
|
+
: how === 'device' ? new Promise((resolve) => ((opener) => execFile(opener[0] ?? '', [...opener.slice(1), url], (error) => resolve(error === null)))(members(valueOf(lock, `platform/${process.platform}/browser`)))) : false);
|
|
17
|
+
const offered = async (message, page, how, url = page.url) => ((await offer(message, url, how)) ? page.held : (page.close(), undefined));
|
|
18
|
+
const paged = async (title, fields, how) => offered(fill(aboutOf(lock, fields.some((f) => f.secret) ? 'said/enter' : 'said/answer'), { title }), await open(title, fields, wait), how);
|
|
19
|
+
const form = async (message, properties) => ((got) => (got.action === 'accept' ? Object.fromEntries(Object.entries(got.content ?? {}).map(([k, v]) => [k, String(v)])) : undefined))(await client.elicit({ message, requestedSchema: { type: 'object', properties, required: Object.keys(properties) } }));
|
|
20
|
+
return {
|
|
21
|
+
consent: async (message, word) => ((how) => (how === undefined ? Promise.resolve('none')
|
|
22
|
+
: (how === 'form' ? form(message, { yes: { type: 'boolean', title: word } }) : paged(message, [{ name: 'yes', label: word, secret: false, options: [{ value: 'true', label: word }, { value: 'false', label: 'No' }] }], how))
|
|
23
|
+
.then((got) => (got?.['yes'] === 'true' ? 'yes' : 'no'))))(channel('ask/consent-channels')),
|
|
24
|
+
enter: async (title, fields) => paged(title, fields, channel('ask/secret-channels')),
|
|
25
|
+
choose: async (message, choices) => ((how) => (how === 'form'
|
|
26
|
+
? form(message, Object.fromEntries(choices.map((c) => [c.name, { type: 'string', title: c.label ?? c.name, ...(c.options ? { oneOf: c.options.map((o) => ({ const: o.value, title: o.label })) } : {}) }])))
|
|
27
|
+
: how === undefined ? Promise.resolve(undefined) : paged(message, choices.map((c) => ({ name: c.name, label: c.label ?? c.name, secret: false, ...(c.options ? { options: c.options } : {}) })), how)))(channel('ask/channels')),
|
|
28
|
+
visit: async (message, to) => {
|
|
29
|
+
const page = await open(message, 'return', wait);
|
|
30
|
+
const url = await to(page.url);
|
|
31
|
+
return url === undefined ? (page.close(), undefined) : offered(fill(aboutOf(lock, 'said/visit'), { message }), page, channel('ask/secret-channels'), url);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Said } from '../../core/lock.ts';
|
|
2
|
+
import type { Call } from '../../core/read.ts';
|
|
3
|
+
import type { Attachment } from '../../access/contract.ts';
|
|
4
|
+
export type Fetched = {
|
|
5
|
+
readonly kind: 'got';
|
|
6
|
+
readonly status: number;
|
|
7
|
+
readonly bytes: Uint8Array;
|
|
8
|
+
readonly type: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'over';
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: 'failed';
|
|
13
|
+
};
|
|
14
|
+
export type Prepared = {
|
|
15
|
+
readonly key: string;
|
|
16
|
+
readonly describe: string;
|
|
17
|
+
readonly send: (attachment: Attachment | undefined, most: number) => Promise<Fetched>;
|
|
18
|
+
} | {
|
|
19
|
+
readonly missing: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly refused: string;
|
|
22
|
+
};
|
|
23
|
+
export declare const published: (url: string, digest: string, most: number, wait: number) => Promise<Uint8Array | undefined>;
|
|
24
|
+
export declare const get: (url: string, wait: number) => Promise<{
|
|
25
|
+
readonly bytes: Uint8Array;
|
|
26
|
+
readonly type: string;
|
|
27
|
+
} | undefined>;
|
|
28
|
+
/** A body posted to an address a declaration names — as a form, or as JSON where the declaration's protocol asks it — as one request under the same rules: encrypted unless it is this device, capped, never redirected. */
|
|
29
|
+
export declare const post: (url: string, body: Readonly<Record<string, unknown>>, most: number, wait: number, as?: "form" | "json") => Promise<{
|
|
30
|
+
readonly status: number;
|
|
31
|
+
readonly json: unknown;
|
|
32
|
+
} | undefined>;
|
|
33
|
+
/**
|
|
34
|
+
* The request an offer's carry describes: its method and path, the path and query parameters its form names filled from
|
|
35
|
+
* what was asked, its body, and the headers and query members an access adapter attaches — only when it is sent, never
|
|
36
|
+
* in the request's key or its description. Plain transport reaches loopback only; anything else must be encrypted.
|
|
37
|
+
*/
|
|
38
|
+
export declare const prepare: (call: Call, args: Readonly<Record<string, unknown>>, lock: readonly Said[]) => Prepared;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { valueOf } from "../../core/lock.js";
|
|
3
|
+
import { jsonOf } from "../../access/contract.js";
|
|
4
|
+
/**
|
|
5
|
+
* One request, stopped as soon as the body runs past the bytes it may hold or the wait runs out, its stream released
|
|
6
|
+
* either way; nothing is retried. No redirect is followed, but to bytes a digest pins: asked with no header and no body,
|
|
7
|
+
* so a secret stays with its origin, and answered only when they hash to the pin, their integrity and never the address.
|
|
8
|
+
*/
|
|
9
|
+
const requestCapped = async (method, url, headers, body, most, wait, redirect = 'manual') => {
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch(url, { method, headers, body, redirect: redirect === 'follow' && !Object.keys(headers).length && body === undefined ? 'follow' : 'manual', signal: AbortSignal.timeout(wait) });
|
|
12
|
+
const chunks = [];
|
|
13
|
+
let size = 0;
|
|
14
|
+
for await (const chunk of response.body ?? []) {
|
|
15
|
+
size += chunk.byteLength;
|
|
16
|
+
if (size > most)
|
|
17
|
+
break;
|
|
18
|
+
chunks.push(chunk);
|
|
19
|
+
}
|
|
20
|
+
return size > most ? { kind: 'over' } : { kind: 'got', status: response.status, bytes: Buffer.concat(chunks), type: response.headers.get('content-type') ?? '' };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { kind: 'failed' };
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const loopback = new Set(['127.0.0.1', 'localhost', '[::1]']);
|
|
27
|
+
export const published = async (url, digest, most, wait) => (URL.canParse(url) && new URL(url).protocol === 'https:'
|
|
28
|
+
? ((got) => (got.kind === 'got' && got.status < 300 && `sha256:${createHash('sha256').update(got.bytes).digest('hex')}` === digest ? got.bytes : undefined))(await requestCapped('GET', url, {}, undefined, most, wait, 'follow')) : undefined);
|
|
29
|
+
export const get = async (url, wait) => (URL.canParse(url) && new URL(url).protocol === 'https:' ? ((got) => (got.kind === 'got' && got.status < 300 ? got : undefined))(await requestCapped('GET', url, {}, undefined, 16_777_216, wait)) : undefined);
|
|
30
|
+
/** A body posted to an address a declaration names — as a form, or as JSON where the declaration's protocol asks it — as one request under the same rules: encrypted unless it is this device, capped, never redirected. */
|
|
31
|
+
export const post = async (url, body, most, wait, as = 'form') => {
|
|
32
|
+
if (!URL.canParse(url) || !((at) => at.protocol === 'https:' || loopback.has(at.hostname))(new URL(url)))
|
|
33
|
+
return undefined;
|
|
34
|
+
const got = await requestCapped('POST', url, { 'content-type': as === 'json' ? 'application/json' : 'application/x-www-form-urlencoded', accept: 'application/json' }, as === 'json' ? JSON.stringify(body) : new URLSearchParams(Object.entries(body).map(([k, v]) => [k, String(v)])).toString(), most, wait);
|
|
35
|
+
return got.kind === 'got' ? { status: got.status, json: jsonOf(got.bytes) } : undefined;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The request an offer's carry describes: its method and path, the path and query parameters its form names filled from
|
|
39
|
+
* what was asked, its body, and the headers and query members an access adapter attaches — only when it is sent, never
|
|
40
|
+
* in the request's key or its description. Plain transport reaches loopback only; anything else must be encrypted.
|
|
41
|
+
*/
|
|
42
|
+
export const prepare = (call, args, lock) => {
|
|
43
|
+
const word = (key) => valueOf(lock, `adapter/request/${key}`);
|
|
44
|
+
const [method, ...more] = call.carry['method'] ?? [];
|
|
45
|
+
const [template, ...paths] = call.carry['path'] ?? [];
|
|
46
|
+
if (method === undefined || template === undefined || more.length || paths.length)
|
|
47
|
+
return { refused: 'carry-unsettled' };
|
|
48
|
+
const origin = ((at) => (at !== undefined && (at.protocol === 'https:' || loopback.has(at.hostname)) ? at : undefined))(URL.canParse(call.origin) ? new URL(call.origin) : undefined);
|
|
49
|
+
if (origin === undefined)
|
|
50
|
+
return { refused: 'insecure-origin' };
|
|
51
|
+
const named = (where) => (call.form['params'] ?? []).filter((p) => p.startsWith(`${where}:`)).map((p) => p.slice(where.length + 1));
|
|
52
|
+
const missing = named('path').find((p) => args[p] === undefined);
|
|
53
|
+
if (missing !== undefined)
|
|
54
|
+
return { missing };
|
|
55
|
+
const path = named('path').reduce((at, p) => at.split(`{${p}}`).join(encodeURIComponent(String(args[p]))), template);
|
|
56
|
+
const query = new URLSearchParams(named('query').flatMap((p) => (args[p] === undefined ? [] : [[p, String(args[p])]])));
|
|
57
|
+
const url = `${origin.href.replace(/\/$/, '')}${path}${query.size ? `?${query}` : ''}`;
|
|
58
|
+
const body = args['body'] === undefined ? undefined : JSON.stringify(args['body']);
|
|
59
|
+
const key = createHash('sha256').update(`${method.toUpperCase()} ${url} ${body ?? 'null'}`).digest('hex');
|
|
60
|
+
const idempotency = call.carry['idempotency']?.[0];
|
|
61
|
+
const send = (attachment, most) => {
|
|
62
|
+
const extra = new URLSearchParams(Object.entries(attachment?.query ?? {}));
|
|
63
|
+
const sent = extra.size ? `${url}${query.size ? '&' : '?'}${extra}` : url;
|
|
64
|
+
return requestCapped(method.toUpperCase(), sent, { ...(attachment?.headers ?? {}), ...(idempotency ? { [idempotency]: key } : {}), ...(body === undefined ? {} : { [word('content-type')]: word('json') }) }, body, most, Number(word('wait').split('..')[1]));
|
|
65
|
+
};
|
|
66
|
+
return { key, describe: `${method.toUpperCase()} ${url}`, send };
|
|
67
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Said } from '../core/lock.ts';
|
|
2
|
+
import type { Outcome } from '../core/answer.ts';
|
|
3
|
+
import type { Printed } from './process.ts';
|
|
4
|
+
export type { Printed };
|
|
5
|
+
export declare const limits: {
|
|
6
|
+
wait: number;
|
|
7
|
+
most: number;
|
|
8
|
+
};
|
|
9
|
+
/** bound run as a process in a folder, beside head and never blocking it, within the wait and the bytes head's lock gives it and stopped when the asker cancels. */
|
|
10
|
+
export declare const run: (name: string, cwd: string, args: readonly string[], env?: Readonly<Record<string, string>>, signal?: AbortSignal) => Promise<Printed | undefined>;
|
|
11
|
+
export type Folded = {
|
|
12
|
+
readonly lines: readonly Said[];
|
|
13
|
+
} | {
|
|
14
|
+
readonly refused: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The lines a place stands on, from bound's public views alone: its folded normative standing (the lines view), and from
|
|
18
|
+
* its authenticated evidence what this device's own keys signed. Receipts are history, each kept as it was made; any other
|
|
19
|
+
* line of a device key, the person's own attestation, stands only while it is the one line of its scope and measure,
|
|
20
|
+
* unwithdrawn and beside no line of the place's own: anything else needs a fold bound does not publish, and is refused by
|
|
21
|
+
* name. A place whose land prints no act result is refused before anything lands in it. Any refusal of bound is no standing.
|
|
22
|
+
*/
|
|
23
|
+
export declare const standingAt: (name: string, place: string, devicesOf: (lines: readonly Said[]) => readonly string[], isHistory: (line: Said) => boolean, signal?: AbortSignal) => Promise<Folded>;
|
|
24
|
+
export declare const sign: (name: string, place: string, lot: readonly string[], key: string, signer: string, env: Readonly<Record<string, string>>, signal?: AbortSignal) => Promise<Outcome>;
|
|
25
|
+
/** 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. */
|
|
26
|
+
export declare const landAct: (name: string, place: string, lot: readonly string[], key: string, signer: string, env: Readonly<Record<string, string>>, signal?: AbortSignal) => Promise<Outcome>;
|
|
27
|
+
export declare const land: (name: string, place: string, signed: readonly string[], signal?: AbortSignal) => Promise<Outcome>;
|
|
28
|
+
export declare const told: (name: string, place: string, signed: readonly string[], view: string, signal?: AbortSignal) => Promise<readonly string[]>;
|
|
29
|
+
export declare const pinnedAt: (name: string, place: string, file: string, signal?: AbortSignal) => Promise<{
|
|
30
|
+
readonly pin: string;
|
|
31
|
+
readonly bytes: string;
|
|
32
|
+
readonly lines: readonly Said[];
|
|
33
|
+
} | {
|
|
34
|
+
readonly refused: string;
|
|
35
|
+
} | undefined>;
|
|
36
|
+
export declare const founded: (name: string, place: string, key: string, signer: string, env: Readonly<Record<string, string>>, signal?: AbortSignal) => Promise<Outcome>;
|
|
37
|
+
export declare const words: {
|
|
38
|
+
read: string;
|
|
39
|
+
view: string;
|
|
40
|
+
sign: string;
|
|
41
|
+
land: string;
|
|
42
|
+
key: string;
|
|
43
|
+
signer: string;
|
|
44
|
+
pin: string;
|
|
45
|
+
topos: string;
|
|
46
|
+
standing: string;
|
|
47
|
+
evidence: string;
|
|
48
|
+
act: string;
|
|
49
|
+
profile: string;
|
|
50
|
+
};
|
|
51
|
+
export declare const settledView: (name: string, place: string, view: string, signal?: AbortSignal) => Promise<Folded>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { parse } from '@lapxo/topos/wire';
|
|
6
|
+
import { spawned } from "./process.js";
|
|
7
|
+
const binOf = (name) => {
|
|
8
|
+
try {
|
|
9
|
+
const manifest = createRequire(import.meta.url).resolve(join(name, 'package.json'));
|
|
10
|
+
const entry = Object.values(JSON.parse(readFileSync(manifest, 'utf8')).bin ?? {})[0];
|
|
11
|
+
return entry === undefined ? undefined : join(dirname(manifest), entry);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
export const limits = { wait: 0, most: 0 };
|
|
18
|
+
/** bound run as a process in a folder, beside head and never blocking it, within the wait and the bytes head's lock gives it and stopped when the asker cancels. */
|
|
19
|
+
export const run = async (name, cwd, args, env = {}, signal) => ((bin) => (bin === undefined ? undefined
|
|
20
|
+
: spawned(process.execPath, [bin, ...args], { cwd, env: { ...process.env, ...env } }, { wait: limits.wait, most: limits.most || Number.MAX_SAFE_INTEGER, ...(signal ? { signal } : {}) })))(binOf(name));
|
|
21
|
+
const facts = (out) => out.flatMap((line) => ((got) => (got.kind === 'fact' ? [got.value.fields] : []))(parse(line)));
|
|
22
|
+
const readOf = (printed) => (printed === undefined || printed.code !== 0 ? { refused: printed?.err.at(-1) ?? '' } : { lines: facts(printed.out) });
|
|
23
|
+
/**
|
|
24
|
+
* The lines a place stands on, from bound's public views alone: its folded normative standing (the lines view), and from
|
|
25
|
+
* its authenticated evidence what this device's own keys signed. Receipts are history, each kept as it was made; any other
|
|
26
|
+
* line of a device key, the person's own attestation, stands only while it is the one line of its scope and measure,
|
|
27
|
+
* unwithdrawn and beside no line of the place's own: anything else needs a fold bound does not publish, and is refused by
|
|
28
|
+
* name. A place whose land prints no act result is refused before anything lands in it. Any refusal of bound is no standing.
|
|
29
|
+
*/
|
|
30
|
+
export const standingAt = async (name, place, devicesOf, isHistory, signal) => {
|
|
31
|
+
const folded = readOf(await run(name, place, [words.read, words.view, words.standing], {}, signal));
|
|
32
|
+
if ('refused' in folded)
|
|
33
|
+
return folded;
|
|
34
|
+
if (!folded.lines.some((line) => line.scope === words.profile))
|
|
35
|
+
return { refused: `REFUSE·act the place declares no ${words.profile}: land would print nothing of what it admits, so head lands nothing there; its root can land the local-act@1 profile and view/act` };
|
|
36
|
+
const evidence = readOf(await run(name, place, [words.read, words.view, words.evidence], {}, signal));
|
|
37
|
+
if ('refused' in evidence)
|
|
38
|
+
return evidence;
|
|
39
|
+
const ids = new Set(devicesOf(folded.lines)), own = evidence.lines.filter((line) => ids.has(line.by ?? ''));
|
|
40
|
+
const key = (line) => `${line.scope} ${line.measure}`, attested = own.filter((line) => !isHistory(line));
|
|
41
|
+
const unfolded = attested.find((line) => line.value === 'withdraw' || attested.filter((other) => key(other) === key(line)).length > 1 || folded.lines.some((other) => key(other) === key(line)));
|
|
42
|
+
return unfolded === undefined ? { lines: [...folded.lines, ...own] }
|
|
43
|
+
: { refused: `REFUSE·standing ${unfolded.by} attests ${key(unfolded)} more than once, withdrawn, or beside the place's own line: folding a device key's lines is bound's, and bound publishes no view of it` };
|
|
44
|
+
};
|
|
45
|
+
const lotFile = (lines) => ((at) => { writeFileSync(at, `${lines.join('\n')}\n`); return at; })(join(mkdtempSync(join(tmpdir(), 'head-lot-')), 'lot.bound'));
|
|
46
|
+
export const sign = async (name, place, lot, key, signer, env, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: got.out.filter((line) => parse(line).kind === 'fact') } : { refused: got?.err.at(-1) ?? '' }))(await run(name, place, [words.sign, lotFile(lot), words.key, key, words.signer, signer], env, signal));
|
|
47
|
+
/** 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. */
|
|
48
|
+
export const landAct = async (name, place, lot, key, signer, env, signal) => ((got) => {
|
|
49
|
+
if (got === undefined || got.code !== 0)
|
|
50
|
+
return { refused: got?.err.at(-1) ?? '' };
|
|
51
|
+
const at = got.out.findIndex((line) => line.startsWith(words.act));
|
|
52
|
+
return at < 0 ? { refused: `REFUSE·act bound printed no act result, and the place declares ${words.profile}: what was admitted is bound's to say` }
|
|
53
|
+
: { lines: got.out.slice(at + 1).filter((line) => parse(line).kind === 'fact').slice(0, -1) };
|
|
54
|
+
})(await run(name, place, [words.land, lotFile(lot), words.key, key, words.signer, signer], env, signal));
|
|
55
|
+
export const land = async (name, place, signed, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: signed } : { refused: got?.err.at(-1) ?? '' }))(await run(name, place, [words.land, lotFile(signed)], {}, signal));
|
|
56
|
+
export const told = async (name, place, signed, view, signal) => ((got) => [...(got?.out ?? []), ...(got?.err ?? [])])(await run(name, place, [words.read, words.view, view, lotFile(signed)], {}, signal));
|
|
57
|
+
export const pinnedAt = async (name, place, file, signal) => {
|
|
58
|
+
const printed = await run(name, place, [words.read, file], {}, signal);
|
|
59
|
+
const read = readOf(printed);
|
|
60
|
+
if ('refused' in read)
|
|
61
|
+
return read;
|
|
62
|
+
const pin = printed.out.find((line) => line.startsWith(words.pin))?.slice(words.pin.length);
|
|
63
|
+
const bytes = printed.out.filter((line) => line.startsWith(words.topos)).map((line) => `${line.slice(words.topos.length)}\n`).join('');
|
|
64
|
+
return pin === undefined || !bytes ? undefined : { pin, bytes, lines: read.lines };
|
|
65
|
+
};
|
|
66
|
+
export const founded = async (name, place, key, signer, env, signal) => ((got) => (got !== undefined && got.code === 0 ? { lines: got.out } : { refused: got?.err.at(-1) ?? '' }))(await run(name, place, [words.land, words.key, key, words.signer, signer], env, signal));
|
|
67
|
+
export const words = { read: '', view: '', sign: '', land: '', key: '', signer: '', pin: '', topos: '', standing: '', evidence: '', act: '', profile: '' };
|
|
68
|
+
export const settledView = async (name, place, view, signal) => readOf(await run(name, place, [words.read, words.view, view], {}, signal));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const digestOf: (bytes: Uint8Array) => string;
|
|
2
|
+
/** A read-s body kept by its digest, readable only by its owner; what is read back must hash to the name it is kept under. */
|
|
3
|
+
export declare const keep: (bytes: Uint8Array) => string;
|
|
4
|
+
export declare const held: (digest: string) => Uint8Array | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
const folder = () => { const at = join(tmpdir(), 'head-reads'); mkdirSync(at, { recursive: true, mode: 0o700 }); return at; };
|
|
6
|
+
export const digestOf = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
7
|
+
/** A read-s body kept by its digest, readable only by its owner; what is read back must hash to the name it is kept under. */
|
|
8
|
+
export const keep = (bytes) => ((digest) => { writeFileSync(join(folder(), digest.slice(7)), bytes, { mode: 0o600 }); return digest; })(digestOf(bytes));
|
|
9
|
+
export const held = (digest) => { try {
|
|
10
|
+
return ((bytes) => (digestOf(bytes) === digest ? bytes : undefined))(readFileSync(join(folder(), digest.slice(7))));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
} };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface KeyStore {
|
|
2
|
+
readonly read: (account: string) => string | undefined;
|
|
3
|
+
readonly write: (account: string, secret: string) => void;
|
|
4
|
+
}
|
|
5
|
+
export declare const plain: (word: string) => string;
|
|
6
|
+
/** The secret store this host keeps head's keys in, by the name its lock gives the platform's: one adapter each, every secret crossing a command's stdin and stdout only. */
|
|
7
|
+
export declare const keychain: (service: string, kind: string) => KeyStore;
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
export const plain = (word) => { if (!/^[\w.@/+=-]+$/.test(word))
|
|
5
|
+
throw Error('REFUSE·keychain a name holds more than word characters'); return word; };
|
|
6
|
+
/** The secret store this host keeps head's keys in, by the name its lock gives the platform's: one adapter each, every secret crossing a command's stdin and stdout only. */
|
|
7
|
+
export const keychain = (service, kind) => ((made) => {
|
|
8
|
+
if (made === undefined)
|
|
9
|
+
throw Error(`REFUSE·keychain ${kind} is no store head carries`);
|
|
10
|
+
return made(service);
|
|
11
|
+
})({ keychain: keychainOfMac, 'secret-tool': secretTool, 'credential-manager': credentialManager }[kind]);
|
|
@@ -0,0 +1,10 @@
|
|
|
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;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, 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 });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface Printed {
|
|
2
|
+
readonly code: number;
|
|
3
|
+
readonly out: readonly string[];
|
|
4
|
+
readonly err: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export interface Bounds {
|
|
7
|
+
readonly wait: number;
|
|
8
|
+
readonly most: number;
|
|
9
|
+
readonly signal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A process run beside head, never blocking it: what it printed, out and err, line by line, and its exit, which is
|
|
13
|
+
* transport and never the verdict. Its whole group is stopped when the wait runs out, when it prints more bytes than it
|
|
14
|
+
* may, when the asker cancels or when head exits; the exit is then -1 and the cause is err's last line.
|
|
15
|
+
*/
|
|
16
|
+
export declare const spawned: (command: string, args: readonly string[], at: {
|
|
17
|
+
readonly cwd?: string;
|
|
18
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
19
|
+
readonly input?: string;
|
|
20
|
+
}, bounds: Bounds) => Promise<Printed>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
const running = new Set();
|
|
3
|
+
process.once('exit', () => { for (const stop of running)
|
|
4
|
+
stop(); });
|
|
5
|
+
/**
|
|
6
|
+
* A process run beside head, never blocking it: what it printed, out and err, line by line, and its exit, which is
|
|
7
|
+
* transport and never the verdict. Its whole group is stopped when the wait runs out, when it prints more bytes than it
|
|
8
|
+
* may, when the asker cancels or when head exits; the exit is then -1 and the cause is err's last line.
|
|
9
|
+
*/
|
|
10
|
+
export const spawned = (command, args, at, bounds) => new Promise((resolve) => {
|
|
11
|
+
const child = spawn(command, [...args], { cwd: at.cwd, env: at.env, stdio: ['pipe', 'pipe', 'pipe'], detached: process.platform !== 'win32', windowsHide: true });
|
|
12
|
+
const [out, err] = [[], []];
|
|
13
|
+
let [printed, cause, done] = [0, undefined, false];
|
|
14
|
+
const kill = () => { try {
|
|
15
|
+
if (child.pid !== undefined && process.platform !== 'win32')
|
|
16
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
17
|
+
else
|
|
18
|
+
child.kill('SIGKILL');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
child.kill('SIGKILL');
|
|
22
|
+
} };
|
|
23
|
+
const stop = (why) => { cause ??= why; kill(); };
|
|
24
|
+
const signal = AbortSignal.any([...(bounds.wait > 0 ? [AbortSignal.timeout(bounds.wait)] : []), ...(bounds.signal ? [bounds.signal] : [])]);
|
|
25
|
+
const cancel = () => stop(bounds.signal?.aborted ? 'cancelled' : `waited ${bounds.wait} ms`);
|
|
26
|
+
const finish = (code) => {
|
|
27
|
+
if (done)
|
|
28
|
+
return;
|
|
29
|
+
done = true;
|
|
30
|
+
signal.removeEventListener('abort', cancel);
|
|
31
|
+
running.delete(kill);
|
|
32
|
+
const lines = (chunks) => Buffer.concat(chunks).toString('utf8').split('\n').filter(Boolean);
|
|
33
|
+
resolve({ code: cause === undefined ? code : -1, out: lines(out), err: [...lines(err), ...(cause === undefined ? [] : [cause])] });
|
|
34
|
+
};
|
|
35
|
+
const take = (into) => (chunk) => { printed += chunk.length; if (printed > bounds.most)
|
|
36
|
+
stop(`printed more than ${bounds.most} bytes`);
|
|
37
|
+
else
|
|
38
|
+
into.push(chunk); };
|
|
39
|
+
running.add(kill);
|
|
40
|
+
child.stdout.on('data', take(out));
|
|
41
|
+
child.stderr.on('data', take(err));
|
|
42
|
+
child.on('error', (error) => { cause ??= error.message; finish(-1); });
|
|
43
|
+
child.on('close', (code) => finish(code ?? -1));
|
|
44
|
+
child.stdin.on('error', () => undefined).end(at.input ?? '');
|
|
45
|
+
if (signal.aborted)
|
|
46
|
+
cancel();
|
|
47
|
+
else
|
|
48
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
49
|
+
});
|
|
@@ -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,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,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 @@
|
|
|
1
|
+
export { answer } from '../tools/look.ts';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { answer } from "../tools/look.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { signLot } from "./core/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);
|
|
9
|
+
try {
|
|
10
|
+
process.stdout.write(signLot(readFileSync(0, 'utf8'), secret, key, algorithm));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Asked, Context } from '../core/answer.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Access to a source obtained ahead of any call, at the origin the person picked once where several are declared: the
|
|
4
|
+
* way its document says access is proven, run by the adapter of
|
|
5
|
+
* that kind — the person's own page, or the provider they are sent to with a client registered for the visit — its
|
|
6
|
+
* secret kept in this keychain and its obtaining landed as a receipt the device key signs. Nothing is called.
|
|
7
|
+
*/
|
|
8
|
+
export declare const accessed: (asked: Asked, context: Context) => Promise<readonly string[]>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { inPlace, placeNameOf } from "../core/answer.js";
|
|
2
|
+
import { uncoveredIn } from "../core/device.js";
|
|
3
|
+
import { valueOf } from "../core/lock.js";
|
|
4
|
+
import { planRead, vocabOf } from "../core/read.js";
|
|
5
|
+
import { sayIn } from "../core/surface.js";
|
|
6
|
+
import { attachmentFor } from "../access/obtain.js";
|
|
7
|
+
import { pickedOrigin } from "../flows/call.js";
|
|
8
|
+
import { standingFor } from "../flows/world.js";
|
|
9
|
+
/**
|
|
10
|
+
* Access to a source obtained ahead of any call, at the origin the person picked once where several are declared: the
|
|
11
|
+
* way its document says access is proven, run by the adapter of
|
|
12
|
+
* that kind — the person's own page, or the provider they are sent to with a client registered for the visit — its
|
|
13
|
+
* secret kept in this keychain and its obtaining landed as a receipt the device key signs. Nothing is called.
|
|
14
|
+
*/
|
|
15
|
+
export const accessed = (asked, context) => inPlace(asked, context, async (place) => {
|
|
16
|
+
const [say, v, source] = [sayIn(context.lock), vocabOf(context.lock), asked['source'] ?? ''];
|
|
17
|
+
const held = await standingFor(context, place);
|
|
18
|
+
if ('unsettled' in held)
|
|
19
|
+
return [say('unsettled', { source, why: held.unsettled })];
|
|
20
|
+
const operation = asked['operation'] || held.lines.filter((l) => l.measure === v.class && l.value === v.callable && l.scope?.startsWith(`${v.offers}/${source}/`)).map((l) => l.scope.split('/').slice(2).join('/')).sort()[0];
|
|
21
|
+
if (operation === undefined)
|
|
22
|
+
return [say('unread', { source })];
|
|
23
|
+
const picked = await pickedOrigin(context, place, held.lines, source, planRead(held.lines, context.lock, source, operation, placeNameOf(place)));
|
|
24
|
+
if ('said' in picked)
|
|
25
|
+
return picked.said;
|
|
26
|
+
const plan = planRead(picked.lines, context.lock, source, operation, placeNameOf(place));
|
|
27
|
+
if (plan.kind === 'said')
|
|
28
|
+
return [say(plan.key, plan.fields)];
|
|
29
|
+
const stopped = uncoveredIn(context.lock, held.lines, context.device('head'), place, [], [`${valueOf(context.lock, 'access/family')}/${source}/${plan.access.scheme || v.anonymous}/obtained`]);
|
|
30
|
+
if (stopped)
|
|
31
|
+
return stopped;
|
|
32
|
+
const got = await attachmentFor(context, place, picked.lines, plan);
|
|
33
|
+
return 'said' in got ? [say(got.said, { source, operation, kinds: plan.access.kind, input: got.input })] : [say(plan.access.account === undefined ? 'access-anonymous' : 'access-kept', { source })];
|
|
34
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Asked, Context } from '../core/answer.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A source connected to a place, once the person said yes; a lock is no source, and adopting one as a parent is refused
|
|
4
|
+
* by name, since composing two places' policies is bound's. A document — and an overlay beside it — is laid whatever its
|
|
5
|
+
* kind, with every world head carries adopted, and the fold decides which reads it: what no world compiles is open, the
|
|
6
|
+
* bytes taken back and the line that pays it handed on. What one does is compiled once per digest of what it read — the
|
|
7
|
+
* same bytes again find their compiled standing by name — and pinned from the place's store: a new pin lands with the
|
|
8
|
+
* old one's withdraw, said at its own time, since a withdrawn saying stays withdrawn.
|
|
9
|
+
*/
|
|
10
|
+
export declare const connected: (asked: Asked, context: Context) => Promise<readonly string[]>;
|