@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,71 @@
1
+ import type { Fields } from '../core/read.ts';
2
+ export type { Fields };
3
+ export interface Field {
4
+ readonly name: string;
5
+ readonly label: string;
6
+ readonly secret: boolean;
7
+ readonly options?: readonly {
8
+ readonly value: string;
9
+ readonly label: string;
10
+ }[];
11
+ }
12
+ export type Held = Readonly<Record<string, string>>;
13
+ export interface Choice {
14
+ readonly name: string;
15
+ readonly label?: string;
16
+ readonly options?: readonly {
17
+ readonly value: string;
18
+ readonly label: string;
19
+ }[];
20
+ }
21
+ export interface Attachment {
22
+ readonly headers: Held;
23
+ readonly query: Held;
24
+ }
25
+ /** A secret obtained by a declared flow: the secret, when it stops holding and what renews it, when the flow says; manual when a person entered it with no flow declared. */
26
+ export interface Obtained {
27
+ readonly secret: string;
28
+ readonly expires?: number;
29
+ readonly renew?: string;
30
+ readonly manual?: true;
31
+ }
32
+ export type Answered = {
33
+ readonly status: number;
34
+ readonly json: unknown;
35
+ } | undefined;
36
+ /**
37
+ * What the host hands an access adapter to run its flow with: the declared lines of its scheme — by the step below it they
38
+ * are said under — the origin the source is reached at, how many secrets the place's root lets it ask, the values a flow
39
+ * may name, and the person; never a shell, never a file.
40
+ */
41
+ export interface Want {
42
+ readonly place: string;
43
+ readonly source: string;
44
+ readonly scheme: string;
45
+ readonly params: Fields;
46
+ readonly scopes: readonly string[];
47
+ readonly now: number;
48
+ readonly renew?: string;
49
+ readonly declared: Readonly<Record<string, Fields>>;
50
+ readonly origin: string;
51
+ readonly secrets: number;
52
+ readonly values: readonly string[];
53
+ readonly host: Readonly<Record<string, string>>;
54
+ readonly enter: (title: string, fields: readonly Field[]) => Promise<Held | undefined>;
55
+ readonly consent: (message: string) => Promise<boolean>;
56
+ readonly choose: (message: string, choices: readonly Choice[]) => Promise<Held | undefined>;
57
+ readonly visit: (message: string, to: (back: string) => string | undefined | Promise<string | undefined>) => Promise<Held | undefined>;
58
+ readonly form: (source: string, operation: string) => Fields;
59
+ readonly call: (source: string, operation: string, body: unknown, attachment?: Attachment) => Promise<Answered>;
60
+ readonly post: (url: string, body: Readonly<Record<string, unknown>>, as?: 'form' | 'json') => Promise<Answered>;
61
+ }
62
+ /** A kind of access, executed by the host: how its secret rides a request, and the flow that obtains it. */
63
+ export interface AccessAdapter {
64
+ readonly attach: (secret: string, params: Fields) => Attachment;
65
+ readonly obtain: (want: Want) => Promise<Obtained | {
66
+ readonly refused: string;
67
+ readonly input?: string;
68
+ }>;
69
+ readonly discover?: (resource: string, challenge: string, get: (url: string) => Promise<unknown>) => Promise<Readonly<Record<string, Fields>> | undefined>;
70
+ }
71
+ export declare const jsonOf: (body: Uint8Array | string) => unknown;
@@ -0,0 +1,6 @@
1
+ export const jsonOf = (body) => { try {
2
+ return JSON.parse(typeof body === 'string' ? body : Buffer.from(body).toString('utf8'));
3
+ }
4
+ catch {
5
+ return undefined;
6
+ } };
@@ -0,0 +1,19 @@
1
+ import type { Attachment } from './contract.ts';
2
+ import type { Context } from '../core/answer.ts';
3
+ import type { Said } from '../core/lock.ts';
4
+ import type { Call } from '../core/read.ts';
5
+ /** Whom a call is made as, never its secret: anonymous, or the access account and when the secret it holds now was obtained — still holding, unless what was made as it is only being looked back on; none while no secret is kept, or one is kept without saying when. */
6
+ export declare const principalOf: (context: Context, plan: Call, current?: boolean) => string | undefined;
7
+ /**
8
+ * What a call's access attaches: nothing when it is anonymous; the kept secret while it holds; else the flow its kind's
9
+ * adapter runs — found by the line, never by a name head knows — its secret kept in the place's keychain entry and its
10
+ * obtaining landed as a receipt without the secret. What a person enters goes only to the origin of the source in use:
11
+ * a step of another source is sent there too, where that source declares it among its own, and refused by name where not. A kind no adapter carries, or a flow that does not finish, is no call.
12
+ */
13
+ export declare const attachmentFor: (context: Context, place: string, lines: readonly Said[], plan: Call) => Promise<{
14
+ readonly attachment: Attachment | undefined;
15
+ readonly principal: string | undefined;
16
+ } | {
17
+ readonly said: string;
18
+ readonly input?: string;
19
+ }>;
@@ -0,0 +1,70 @@
1
+ import { canonical } from '@lapxo/topos/wire';
2
+ import { jsonOf } from "./contract.js";
3
+ import { placeNameOf } from "../core/answer.js";
4
+ import { members, valueOf } from "../core/lock.js";
5
+ import { fieldsOf, stepOf, vocabOf } from "../core/read.js";
6
+ const sealed = (got) => Buffer.from(JSON.stringify(got)).toString('base64url');
7
+ const opened = (kept) => { try {
8
+ return kept === undefined ? undefined : JSON.parse(Buffer.from(kept, 'base64url').toString('utf8'));
9
+ }
10
+ catch {
11
+ return undefined;
12
+ } };
13
+ const holds = (held, now) => held !== undefined && (held.expires === undefined || held.expires > now);
14
+ const asOf = (account, held) => (held.at === undefined ? undefined : `${account}@${held.at}`);
15
+ /** Whom a call is made as, never its secret: anonymous, or the access account and when the secret it holds now was obtained — still holding, unless what was made as it is only being looked back on; none while no secret is kept, or one is kept without saying when. */
16
+ export const principalOf = (context, plan, current = true) => ((account) => (account === undefined ? 'anonymous'
17
+ : ((held) => (held !== undefined && (!current || holds(held, context.now())) ? asOf(account, held) : undefined))(opened(context.keys.read(account)))))(plan.access.account);
18
+ /**
19
+ * What a call's access attaches: nothing when it is anonymous; the kept secret while it holds; else the flow its kind's
20
+ * adapter runs — found by the line, never by a name head knows — its secret kept in the place's keychain entry and its
21
+ * obtaining landed as a receipt without the secret. What a person enters goes only to the origin of the source in use:
22
+ * a step of another source is sent there too, where that source declares it among its own, and refused by name where not. A kind no adapter carries, or a flow that does not finish, is no call.
23
+ */
24
+ export const attachmentFor = async (context, place, lines, plan) => {
25
+ const access = plan.access;
26
+ if (access.account === undefined)
27
+ return { attachment: undefined, principal: 'anonymous' };
28
+ const adapter = await context.access(access.kind);
29
+ if (adapter === undefined)
30
+ return { said: 'no-adapter' };
31
+ const held = opened(context.keys.read(access.account));
32
+ if (holds(held, context.now()))
33
+ return { attachment: adapter.attach(held.secret, access.params), principal: asOf(access.account, held) };
34
+ const [v, most] = [vocabOf(context.lock), Number((lines.find((l) => l.scope === 'read/response-bytes')?.value ?? '0..0').split('..')[1])];
35
+ const origin = (url) => (URL.canParse(url) ? new URL(url).origin : '');
36
+ let crossed = false;
37
+ const step = async (source, operation, body, attachment) => {
38
+ const declares = (fieldsOf(lines, `${v.region}/${source}`)[v.origins] ?? []).includes(plan.origin);
39
+ const call = stepOf(declares ? [...lines.filter((l) => l.scope !== `${v.pick}/${source}`), { scope: `${v.pick}/${source}`, value: plan.origin }] : lines, context.lock, source, operation);
40
+ if (call.kind !== 'call' || origin(call.origin) !== origin(plan.origin)) {
41
+ crossed = call.kind === 'call' || !declares;
42
+ return undefined;
43
+ }
44
+ const transport = call.kind === 'call' ? await context.adapter(call.origin) : undefined;
45
+ const prepared = call.kind === 'call' && transport ? transport.prepare(call, { body }, context.lock) : undefined;
46
+ const got = prepared && 'send' in prepared ? await prepared.send(attachment, most) : undefined;
47
+ return got?.kind === 'got' ? { status: got.status, json: jsonOf(got.bytes) } : undefined;
48
+ };
49
+ const reads = new Set(lines.filter((l) => l.scope?.startsWith(`${v.offers}/${plan.source}/`) && l.measure === v.class && l.value === v.callable).map((l) => l.scope));
50
+ const [scheme, root] = [access.from ?? `${v.keys}/${access.flow}`, valueOf(context.lock, 'founding/root').split('/')[1]];
51
+ const want = {
52
+ place: placeNameOf(place), source: plan.source, scheme: access.scheme, params: access.params, now: context.now(), origin: plan.origin, renew: held?.renew,
53
+ declared: Object.fromEntries([...new Set(lines.filter((l) => l.scope === scheme || l.scope?.startsWith(`${scheme}/`)).map((l) => l.scope))].map((at) => [at.slice(scheme.length + 1), fieldsOf(lines, at)])),
54
+ secrets: Number((lines.find((l) => l.scope === valueOf(context.lock, 'access/secrets') && l.by === root)?.value ?? '0..0').split('..')[1]),
55
+ values: members(valueOf(lines, 'wire/access-values') || valueOf(context.lock, 'wire/access-values')),
56
+ host: Object.fromEntries(context.lock.filter((l) => l.scope?.startsWith('host/')).map((l) => [l.scope.slice('host/'.length), l.value ?? ''])),
57
+ scopes: [...new Set(lines.filter((l) => reads.has(l.scope) && l.measure === 'scopes').flatMap((l) => (l.value ?? '').split('|')))].sort(),
58
+ enter: (title, fields) => context.person.enter(title, fields), consent: async (message) => (await context.person.consent(message, valueOf(context.lock, 'said/elicit/yes'))) === 'yes', choose: (message, choices) => context.person.choose(message, choices), visit: (message, to) => context.person.visit(message, to),
59
+ form: (source, operation) => fieldsOf(lines, `${v.offers}/${source}/${operation}/${v.form}`), call: step,
60
+ post: async (url, body, as) => ((transport) => (transport === undefined ? undefined : transport.post(url, body, most, Number(valueOf(context.lock, 'adapter/request/wait').split('..')[1]), as)))(await context.adapter(url)),
61
+ };
62
+ const got = await adapter.obtain(want);
63
+ if ('refused' in got)
64
+ return { said: crossed ? 'cross-origin' : got.refused, ...(got.input === undefined ? {} : { input: got.input }) };
65
+ const when = new Date(context.now()).toISOString();
66
+ context.keys.write(access.account, sealed({ ...got, at: when }));
67
+ const receipt = canonical({ at: `${valueOf(context.lock, 'receipt/at')}:${when}`, by: 'target', form: 'alphabet', measure: 'status', role: 'writes', scope: `${valueOf(context.lock, 'access/family')}/${access.flow}/${got.manual ? 'entered' : held === undefined ? 'obtained' : 'renewed'}`, value: 'present' });
68
+ await context.landAs(place, [receipt], 'head', lines);
69
+ return { attachment: adapter.attach(got.secret, access.params), principal: `${access.account}@${when}` };
70
+ };
@@ -0,0 +1,12 @@
1
+ import type { Context, Outcome } from '../core/answer.ts';
2
+ import type { Said } from '../core/lock.ts';
3
+ import type { Line } from '../compile/pin.ts';
4
+ /**
5
+ * Every world head adopts, once per place: its standing and blob pinned where they are published, for bound to
6
+ * fetch and verify, and each of its readers told where head lays what it reads — read off the standing head reads from
7
+ * the same address once per device, kept by its digest and answered only when it hashes to the pin. A world whose
8
+ * standing cannot be read so is no adoption.
9
+ */
10
+ export declare const adopted: (context: Context, place: string, lands: (lot: readonly string[]) => Promise<Outcome>, line: Line) => Promise<Outcome>;
11
+ /** The name of a lock the place pins as its parent, when it pins one: composing a parent's policy with a child's is bound's, so head reads nothing of such a place and signs nothing for it. */
12
+ export declare const adoptsIn: (lock: readonly Said[], lines: readonly Said[]) => string | undefined;
@@ -0,0 +1,40 @@
1
+ import { EXTENSION } from '@lapxo/topos/wire';
2
+ import { members, valueOf } from "../core/lock.js";
3
+ import { linesOf } from "../core/worlds.js";
4
+ /**
5
+ * What a place declares of a world it adopts, read off the world's own standing: each region that writes readings is a
6
+ * reader, given the coordinates of what is connected when the place's view reads it — else of what was answered, when
7
+ * only the view of answers does — and what it writes as its receipts. head names none of a world's regions.
8
+ */
9
+ const readersOf = (lock, standing) => {
10
+ const region = valueOf(lock, 'vocab/region');
11
+ const reads = (view) => members(standing.find((l) => l.scope === `${region}/${valueOf(lock, view)}` && l.measure === 'reads')?.value ?? '');
12
+ const [connected, answered] = [reads('place/view'), reads('place/answer')];
13
+ return standing.filter((l) => l.scope?.startsWith(`${region}/`) && l.measure === 'writes' && l.role === 'writes').flatMap((l) => ((coordinates) => (coordinates
14
+ ? [{ scope: l.scope, measure: 'coordinates', value: coordinates }, { scope: l.scope, measure: 'receipts', value: l.value }] : []))(connected.includes(l.scope) ? valueOf(lock, 'place/connected/coordinates') : answered.includes(l.scope) ? valueOf(lock, 'place/answers/coordinates') : ''));
15
+ };
16
+ /**
17
+ * Every world head adopts, once per place: its standing and blob pinned where they are published, for bound to
18
+ * fetch and verify, and each of its readers told where head lays what it reads — read off the standing head reads from
19
+ * the same address once per device, kept by its digest and answered only when it hashes to the pin. A world whose
20
+ * standing cannot be read so is no adoption.
21
+ */
22
+ export const adopted = async (context, place, lands, line) => {
23
+ const [word, held] = [(k) => valueOf(context.lock, k), await context.policy(place)];
24
+ if ('refused' in held)
25
+ return held;
26
+ const own = held.lines, [wait, most] = ['adapter/request/wait', 'world/standing-bytes'].map((k) => Number(word(k).split('..')[1]));
27
+ const lot = [];
28
+ for (const w of members(word('world/adopts')).filter((w) => !own.some((l) => l.scope === `uses/${w}`))) {
29
+ const [standing, blob, at, blobAt] = [word(`world/${w}/standing`), word(`world/${w}/blob`), word(`sources/${w}`), word(`sources/${w}-blob`)];
30
+ const bytes = context.cache.held(standing) ?? ((got) => (got === undefined ? undefined : (context.cache.keep(got), got)))(await (await context.adapter(at))?.published(at, standing, most, wait));
31
+ if (bytes === undefined)
32
+ return { refused: `REFUSE·world ${w}: ${at} answered no bytes that hash to its pin ${standing}` };
33
+ const laid = `${word('place/worlds')}/${w}${EXTENSION}`;
34
+ context.files.lay(place, laid, bytes);
35
+ lot.push(line(`uses/${w}`, 'digest', standing, { needs: laid }), line(`sources/${w}`, 'id', at), line(`dep/${w}-blob`, 'digest', blob, { shape: word('world/shape') }), line(`sources/${w}-blob`, 'id', blobAt), ...readersOf(context.lock, linesOf(bytes)).map((r) => line(r.scope, r.measure, r.value, { role: 'reads' })));
36
+ }
37
+ return lot.length ? lands(lot) : { lines: [] };
38
+ };
39
+ /** The name of a lock the place pins as its parent, when it pins one: composing a parent's policy with a child's is bound's, so head reads nothing of such a place and signs nothing for it. */
40
+ export const adoptsIn = (lock, lines) => lines.find((l) => l.scope?.startsWith('uses/') && l.needs?.startsWith(`${valueOf(lock, 'place/adopted')}/`))?.scope?.slice('uses/'.length);
@@ -0,0 +1,26 @@
1
+ import type { Context, Outcome } from '../core/answer.ts';
2
+ import type { Said } from '../core/lock.ts';
3
+ export type Line = (scope: string, measure: string, value: string, more?: Readonly<Record<string, string>>) => string;
4
+ export type Pinned = {
5
+ readonly pin: string;
6
+ readonly lines: readonly Said[];
7
+ } | {
8
+ readonly said: readonly string[];
9
+ readonly open?: true;
10
+ readonly dropped?: true;
11
+ };
12
+ /**
13
+ * A source pinned in a place: compiled once per digest of what it read — the place's other pins and what was laid for it
14
+ * — so the same input finds its compiled standing by name, and pinned from the place's store: a new pin lands with the old
15
+ * one's withdraw, said at its own time, since a withdrawn saying stays withdrawn.
16
+ */
17
+ export declare const pinIn: (context: Context, place: string, lands: (lot: readonly string[]) => Promise<Outcome>, line: Line) => (source: string, read: readonly string[]) => Promise<Pinned>;
18
+ /**
19
+ * The demands a document laid in a place pays: every other pin whose compile named a reference to it unresolved, as it
20
+ * was not there yet, recompiled now that it is — the input its old pin read and the place's pins as they now stand — and
21
+ * never reconnected. What each answers is said by the pin; one still unresolved stays a demand.
22
+ */
23
+ export declare const paidBy: (context: Context, place: string, pin: ReturnType<typeof pinIn>, laid: string) => Promise<readonly {
24
+ readonly source: string;
25
+ readonly pinned: Pinned;
26
+ }[]>;
@@ -0,0 +1,74 @@
1
+ import { basename, join } from 'node:path';
2
+ import { canonical, EXTENSION } from '@lapxo/topos/wire';
3
+ import { members, under, valueOf } from "../core/lock.js";
4
+ import { fieldsOf, vocabOf } from "../core/read.js";
5
+ import { sayIn } from "../core/surface.js";
6
+ import { digestIn, linesOf, pinnedOf } from "../core/worlds.js";
7
+ /**
8
+ * One compile, by the fold: the place view asked once, this source's lines kept with a region of its own — and the flow
9
+ * of each issuer the source declares, so a flow is pinned once, by whoever declares it — and the standing bound selects
10
+ * from them pinned and laid where its input names it.
11
+ */
12
+ const compiledOf = async (context, place, source, file) => {
13
+ const say = sayIn(context.lock), v = vocabOf(context.lock);
14
+ const compiled = await context.world(place, valueOf(context.lock, 'place/view'));
15
+ if ('refused' in compiled)
16
+ return { said: [say('unsettled', { source, why: compiled.refused })] };
17
+ const issued = (fieldsOf(compiled.lines, `${v.region}/${source}`)[v.issuers] ?? []).map((issuer) => `${v.keys}/${issuer}`);
18
+ const mine = compiled.lines.filter((l) => l.scope === `${v.region}/${source}` || l.scope?.startsWith(`${v.keys}/${source}/`) || l.scope?.startsWith(`${v.offers}/${source}/`) || /^(read|write)\//.test(l.scope ?? '')
19
+ || issued.some((at) => l.scope === at || l.scope?.startsWith(`${at}/`)));
20
+ if (!mine.some((l) => !/^(read|write)\//.test(l.scope ?? '')))
21
+ return { said: [], open: true };
22
+ const refused = ['unparsed', 'unsupported', 'collision'].find((word) => fieldsOf(compiled.lines, `${v.region}/${source}`)[v[word]] !== undefined);
23
+ if (refused)
24
+ return { said: [say(`document-${refused}`, { source, why: fieldsOf(compiled.lines, `${v.region}/${source}`)[v[refused]].join('; ') })], dropped: true };
25
+ if (!mine.some((l) => l.measure === v.class && l.scope?.split('/').length === 3))
26
+ return { said: [say('nothing-offered', { source })] };
27
+ return drafted(context, place, source, file, [...mine.map((l) => canonical(l)), canonical({ at: 'policy:head/connect', by: 'target', form: 'alphabet', measure: 'reads', role: 'render', scope: `${v.region}/${source}`, value: [`${v.keys}/${source}/**`, ...issued.flatMap((at) => [at, `${at}/**`]), `${v.offers}/${source}/**|read/**|${v.region}/${source}|write/**`].join('|') })]);
28
+ };
29
+ const drafted = async (context, place, source, file, lines) => {
30
+ const say = sayIn(context.lock), draft = file.replace(new RegExp(`${EXTENSION.replace('.', '\\.')}$`), `.draft${EXTENSION}`);
31
+ context.files.lay(place, draft, new TextEncoder().encode(`${lines.join('\n')}\n`));
32
+ const pinned = await context.pinned(place, draft);
33
+ context.files.drop(place, draft);
34
+ if (pinned === undefined || 'refused' in pinned)
35
+ return { said: [pinned === undefined ? say('nothing-offered', { source }) : say('unsettled', { source, why: pinned.refused })] };
36
+ context.files.lay(place, file, new TextEncoder().encode(pinned.bytes));
37
+ return { pin: pinned.pin, lines: pinned.lines };
38
+ };
39
+ /**
40
+ * A source pinned in a place: compiled once per digest of what it read — the place's other pins and what was laid for it
41
+ * — so the same input finds its compiled standing by name, and pinned from the place's store: a new pin lands with the old
42
+ * one's withdraw, said at its own time, since a withdrawn saying stays withdrawn.
43
+ */
44
+ export const pinIn = (context, place, lands, line) => async (source, read) => {
45
+ const [say, digest, held] = [sayIn(context.lock), digestIn(context.lock), await context.policy(place)];
46
+ if ('refused' in held)
47
+ return { said: [say('unsettled', { source, why: held.refused })] };
48
+ const own = held.lines;
49
+ const input = digest([...own.filter((l) => l.scope?.startsWith('uses/') && l.scope !== `uses/${source}`).map((l) => `${l.scope} ${l.value}`), ...read].sort().join('\n'));
50
+ const file = `${members(valueOf(context.lock, 'place/compiled'))[0]}/${source}/${input.split(':')[1].slice(0, 16)}${EXTENSION}`;
51
+ const old = own.find((l) => l.scope === `uses/${source}`);
52
+ const pinned = ((kept) => (kept !== undefined && kept.pin === old?.value ? kept : undefined))(pinnedOf(context.lock, old?.needs === file ? context.files.take(join(place, file)) : undefined)) ?? await compiledOf(context, place, source, file);
53
+ if ('said' in pinned || (old?.needs === file && old.value === pinned.pin))
54
+ return pinned;
55
+ const withdraw = old === undefined ? [] : [canonical({ ...Object.fromEntries(Object.entries(old).filter(([k]) => !['sig', 'epoch'].includes(k))), by: 'target', value: 'withdraw' })];
56
+ const landed = await lands([...withdraw, line(`uses/${source}`, 'digest', pinned.pin, { needs: file, at: `policy:head/connect/${new Date(context.now()).toISOString()}` })]);
57
+ return 'refused' in landed ? { said: [say('not-landed', { why: landed.refused })] } : pinned;
58
+ };
59
+ /**
60
+ * The demands a document laid in a place pays: every other pin whose compile named a reference to it unresolved, as it
61
+ * was not there yet, recompiled now that it is — the input its old pin read and the place's pins as they now stand — and
62
+ * never reconnected. What each answers is said by the pin; one still unresolved stays a demand.
63
+ */
64
+ export const paidBy = async (context, place, pin, laid) => {
65
+ const [v, held] = [vocabOf(context.lock), await context.policy(place)];
66
+ if ('refused' in held)
67
+ return [];
68
+ const owed = held.lines.filter((l) => l.scope?.startsWith('uses/') && under(context.lock, 'place/compiled', l.needs)
69
+ && linesOf(context.files.take(join(place, l.needs)) ?? new Uint8Array()).some((r) => r.measure === v.unresolved && basename((r.value ?? '').split('#')[0]) === basename(laid)));
70
+ const paid = [];
71
+ for (const l of owed)
72
+ paid.push({ source: l.scope.slice('uses/'.length), pinned: await pin(l.scope.slice('uses/'.length), [`${l.scope} ${l.needs}`]) });
73
+ return paid;
74
+ };
@@ -0,0 +1,18 @@
1
+ import type { Said } from './lock.ts';
2
+ /** How many times the person allows an operation: their line for it, never more than their line for the whole source; none, none. */
3
+ export declare const allowedOf: (lines: readonly Said[], family: string, api: string, operation: string) => number;
4
+ /** Whether the person granted head to read a source, by the one sentence they signed for the place: the sources it names, until the day it names. */
5
+ export declare const grantedOf: (lines: readonly Said[], family: string, source: string, now: number) => boolean;
6
+ export declare const untilOf: (period: string, now: number) => string;
7
+ export declare const cappedOf: (lines: readonly Said[], family: string, source: string, operation: string) => boolean;
8
+ export declare const askedOf: (lines: readonly Said[], family: string, api: string, operation: string) => boolean;
9
+ export declare const isOpen: (lines: readonly Said[], intent: string, ends: readonly string[]) => boolean;
10
+ /**
11
+ * The acts of an operation as its receipts say them: every end it got, a receipt or a look that closed an intent — a
12
+ * call that may have happened counts against the allowance as one that did, even when the same request ran again —
13
+ * and whether this request has an intent still open.
14
+ */
15
+ export declare const actsOf: (lines: readonly Said[], family: string, ends: readonly string[], api: string, operation: string, request: string) => {
16
+ done: number;
17
+ open: boolean;
18
+ };
@@ -0,0 +1,18 @@
1
+ const hi = (lines, scope) => ((line) => (line === undefined ? undefined : Number((line.value ?? '').split('..')[1])))(lines.find((l) => l.scope === scope && l.measure === 'calls'));
2
+ /** How many times the person allows an operation: their line for it, never more than their line for the whole source; none, none. */
3
+ export const allowedOf = (lines, family, api, operation) => Math.min(hi(lines, `${family}/${api}/${operation}`) ?? 0, hi(lines, `${family}/${api}`) ?? Infinity);
4
+ /** Whether the person granted head to read a source, by the one sentence they signed for the place: the sources it names, until the day it names. */
5
+ export const grantedOf = (lines, family, source, now) => (lines.find((l) => l.scope === family && l.measure === 'sources')?.value ?? '').split('|').includes(source) && Date.parse(lines.find((l) => l.scope === family && l.measure === 'until')?.value ?? '') > now;
6
+ export const untilOf = (period, now) => new Date(now + Number(period) * 86_400_000).toISOString().slice(0, 10);
7
+ export const cappedOf = (lines, family, source, operation) => hi(lines, `${family}/${source}/${operation}`) !== undefined || hi(lines, `${family}/${source}`) !== undefined;
8
+ export const askedOf = (lines, family, api, operation) => lines.some((l) => l.scope === `${family}/${api}/${operation}` && l.value === 'present');
9
+ export const isOpen = (lines, intent, ends) => lines.filter((l) => l.scope === intent && l.value === 'present').length > lines.filter((l) => ends.some((end) => l.scope === `${intent}/${end}`)).length;
10
+ /**
11
+ * The acts of an operation as its receipts say them: every end it got, a receipt or a look that closed an intent — a
12
+ * call that may have happened counts against the allowance as one that did, even when the same request ran again —
13
+ * and whether this request has an intent still open.
14
+ */
15
+ export const actsOf = (lines, family, ends, api, operation, request) => {
16
+ const under = `${family}/${api}/${operation}/`;
17
+ return { done: lines.filter((l) => l.scope?.startsWith(under) && ends.some((end) => l.scope.endsWith(`/${end}`))).length, open: isOpen(lines, `${under}${request.slice(0, 16)}`, ends) };
18
+ };
@@ -0,0 +1,102 @@
1
+ import { type Said } from './lock.ts';
2
+ import type { Printed } from '../ports/bound.ts';
3
+ export type Outcome = {
4
+ readonly lines: readonly string[];
5
+ } | {
6
+ readonly refused: string;
7
+ };
8
+ import type { KeyStore } from '../ports/keychain.ts';
9
+ import type { Fetched, Prepared } from '../ports/adapters/request.ts';
10
+ import type * as Documents from '../ports/documents.ts';
11
+ import type { Call } from './read.ts';
12
+ import type { AccessAdapter, Answered, Attachment, Choice, Field, Held } from '../access/contract.ts';
13
+ export type Asked = Readonly<Record<string, string | undefined>>;
14
+ export interface Context {
15
+ readonly lock: readonly Said[];
16
+ readonly bound: (cwd: string, args: readonly string[]) => Promise<Printed | undefined>;
17
+ readonly roots: () => Promise<readonly string[]>;
18
+ readonly keys: KeyStore;
19
+ readonly device: (role: string) => string | undefined;
20
+ readonly standing: (place: string) => Promise<{
21
+ readonly lines: readonly Said[];
22
+ } | {
23
+ readonly refused: string;
24
+ }>;
25
+ readonly policy: (place: string) => Promise<{
26
+ readonly lines: readonly Said[];
27
+ } | {
28
+ readonly refused: string;
29
+ }>;
30
+ readonly world: (place: string, view: string) => Promise<{
31
+ readonly lines: readonly Said[];
32
+ } | {
33
+ readonly refused: string;
34
+ }>;
35
+ readonly adapter: (origin: string) => Promise<{
36
+ readonly prepare: (call: Call, args: Readonly<Record<string, unknown>>, lock: readonly Said[]) => Prepared;
37
+ readonly post: (url: string, body: Readonly<Record<string, unknown>>, most: number, wait: number, as?: 'form' | 'json') => Promise<Answered>;
38
+ readonly published: (url: string, digest: string, most: number, wait: number) => Promise<Uint8Array | undefined>;
39
+ readonly fetched: (url: string, attachment: Attachment | undefined, accept: string | undefined, most: number, wait: number) => Promise<Fetched>;
40
+ readonly get: (url: string, wait: number) => Promise<{
41
+ readonly bytes: Uint8Array;
42
+ readonly type: string;
43
+ } | undefined>;
44
+ } | undefined>;
45
+ readonly access: (kind: string) => Promise<AccessAdapter | undefined>;
46
+ readonly person: {
47
+ readonly consent: (message: string, word: string) => Promise<'yes' | 'no' | 'none'>;
48
+ readonly enter: (title: string, fields: readonly Field[]) => Promise<Held | undefined>;
49
+ readonly choose: (message: string, choices: readonly Choice[]) => Promise<Held | undefined>;
50
+ readonly visit: (message: string, to: (back: string) => string | undefined | Promise<string | undefined>) => Promise<Held | undefined>;
51
+ };
52
+ readonly files: {
53
+ readonly lay: (place: string, rel: string, bytes: Uint8Array) => void;
54
+ readonly drop: (place: string, rel: string) => void;
55
+ readonly has: (place: string, rel: string) => boolean;
56
+ readonly list: (place: string, rel: string) => readonly string[];
57
+ readonly read: typeof Documents;
58
+ readonly take: (path: string) => Uint8Array | undefined;
59
+ readonly own: (rel: string) => Uint8Array | undefined;
60
+ };
61
+ readonly home: () => string;
62
+ readonly personal: () => string;
63
+ readonly found: (place: string, account: string) => Promise<Outcome>;
64
+ readonly signRoot: (place: string, lot: readonly string[], account: string) => Promise<Outcome>;
65
+ readonly landRoot: (place: string, lot: readonly string[], account: string) => Promise<Outcome>;
66
+ readonly settle: () => Promise<readonly string[]>;
67
+ readonly pinned: (place: string, file: string) => Promise<{
68
+ readonly pin: string;
69
+ readonly bytes: string;
70
+ readonly lines: readonly Said[];
71
+ } | {
72
+ readonly refused: string;
73
+ } | undefined>;
74
+ readonly cache: {
75
+ readonly keep: (bytes: Uint8Array) => string;
76
+ readonly held: (digest: string) => Uint8Array | undefined;
77
+ };
78
+ readonly signAs: (place: string, lot: readonly string[], role: string, lines: readonly Said[]) => Promise<Outcome>;
79
+ readonly landAs: (place: string, lot: readonly string[], role: string, lines: readonly Said[]) => Promise<Outcome>;
80
+ readonly land: (place: string, signed: readonly string[]) => Promise<Outcome>;
81
+ readonly told: (place: string, signed: readonly string[]) => Promise<readonly string[]>;
82
+ readonly elicit: (message: string) => Promise<'yes' | 'no' | 'none'>;
83
+ readonly now: () => number;
84
+ readonly machine: () => number;
85
+ }
86
+ export type Answer = (asked: Asked, context: Context) => Promise<readonly string[]>;
87
+ /** What answers a line of the surface: the module named as the line, in the folder of its family. */
88
+ export declare const answerOf: (family: "tools" | "resources", name: string) => Promise<Answer>;
89
+ export declare const placeOf: (asked: Asked, context: Context) => Promise<string | undefined>;
90
+ export declare const inPlace: (asked: Asked, context: Context, then: (place: string) => Promise<readonly string[]>) => Promise<readonly string[]>;
91
+ /** The person's yes to exactly what a message says; anything else is what head says of it, and nothing is done. */
92
+ export declare const unless: (context: Context, message: string) => Promise<readonly string[] | undefined>;
93
+ export declare const printedOr: (context: Context, printed: Printed | undefined) => readonly string[];
94
+ /** The lines a place stands on, or what head says when bound refused to fold them: a refusal is never an empty standing. */
95
+ export declare const standingOr: (context: Context, place: string, of?: Context["standing"]) => Promise<{
96
+ readonly lines: readonly Said[];
97
+ } | {
98
+ readonly said: readonly string[];
99
+ }>;
100
+ export declare const baseOf: (lines: readonly Said[]) => string;
101
+ export declare const placeNameOf: (place: string) => string;
102
+ export declare const within: (context: Context, place: string, at: string) => Promise<boolean>;
@@ -0,0 +1,26 @@
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 { createHash } from 'node:crypto';
10
+ import { canonical, LOCK } from '@lapxo/topos/wire';
11
+ import { valueOf } from "./lock.js";
12
+ import { basename, isAbsolute, resolve, sep } from 'node:path';
13
+ import { sayIn } from "./surface.js";
14
+ /** What answers a line of the surface: the module named as the line, in the folder of its family. */
15
+ export const answerOf = async (family, name) => (await import(__rewriteRelativeImportExtension(new URL(`../${family}/${name}${import.meta.url.slice(import.meta.url.lastIndexOf('.'))}`, import.meta.url).href))).answer;
16
+ export const placeOf = async (asked, context) => asked['place'] || (await context.roots())[0] || ((held) => ('refused' in held ? undefined
17
+ : ((named) => (named.length === 1 ? named[0] : undefined))(held.lines.filter((l) => l.scope?.startsWith(`${valueOf(context.lock, 'home/family')}/`) && l.value !== 'withdraw').map((l) => l.value))))(context.files.has(context.home(), LOCK) ? await context.policy(context.home()) : { refused: '' });
18
+ export const inPlace = async (asked, context, then) => ((place) => (place === undefined ? [sayIn(context.lock)('no-place')] : then(place)))(await placeOf(asked, context));
19
+ /** The person's yes to exactly what a message says; anything else is what head says of it, and nothing is done. */
20
+ export const unless = async (context, message) => ((consent) => (consent === 'yes' ? undefined : [sayIn(context.lock)(consent === 'none' ? 'cannot-ask' : 'no-yes')]))(await context.elicit(message));
21
+ export const printedOr = (context, printed) => (printed === undefined ? [sayIn(context.lock)('unreachable')] : [...printed.out, ...printed.err]);
22
+ /** The lines a place stands on, or what head says when bound refused to fold them: a refusal is never an empty standing. */
23
+ export const standingOr = async (context, place, of = context.standing) => ((held) => ('refused' in held ? { said: [sayIn(context.lock)('unfolded', { place, why: held.refused })] } : held))(await of(place));
24
+ export const baseOf = (lines) => createHash('sha256').update(lines.map((l) => canonical(l)).sort().join('\n')).digest('hex').slice(0, 16);
25
+ export const placeNameOf = (place) => basename(place);
26
+ export const within = async (context, place, at) => isAbsolute(at) && [place, ...(await context.roots())].some((root) => resolve(at).startsWith(`${resolve(root)}${sep}`));
@@ -0,0 +1,26 @@
1
+ import type { Said } from './lock.ts';
2
+ export interface Device {
3
+ readonly account: string;
4
+ readonly service: string;
5
+ readonly signer: string;
6
+ }
7
+ export declare const deviceOf: (lock: readonly Said[], role?: string) => Device;
8
+ /**
9
+ * The drafted lines a place signs to admit a key: each device/<role>/<measure> line of head-s lock, said of keys/<id>, and
10
+ * its public half. The id is the role's own unless whoever admits names it; so is the coverage, unless they give it.
11
+ */
12
+ export declare const admissionOf: (lock: readonly Said[], publicKey: string, role?: string, as?: {
13
+ readonly id?: string;
14
+ readonly coverage?: string;
15
+ readonly at?: string;
16
+ }) => readonly string[];
17
+ export declare const algorithmOf: (lines: readonly Said[]) => string;
18
+ /** The id a place admits a public key under, as its own lines say: a key is who the place says it is, and a key it never admitted is no one there. */
19
+ export declare const idIn: (lines: readonly Said[], publicKey: string | undefined) => string | undefined;
20
+ export declare const coverageOf: (lines: readonly Said[], id: string | undefined) => readonly string[];
21
+ export declare const covers: (lines: readonly Said[], id: string | undefined, scope: string) => boolean;
22
+ /**
23
+ * What head says instead of asking, when this device's key may not sign every line it would ask for in a place: what the
24
+ * place admits it for and the first scope outside that, then the lines whoever configures the place can sign themselves.
25
+ */
26
+ export declare const uncoveredIn: (lock: readonly Said[], lines: readonly Said[], publicKey: string | undefined, place: string, lot: readonly string[], scopes?: readonly string[]) => readonly string[] | undefined;
@@ -0,0 +1,27 @@
1
+ import { canonical, matches, parse } from '@lapxo/topos/wire';
2
+ import { members, said, valueOf } from "./lock.js";
3
+ import { sayIn } from "./surface.js";
4
+ const HEAD = 'head';
5
+ export const deviceOf = (lock, role = HEAD) => ({ account: valueOf(lock, `device/${role}/account`), service: valueOf(lock, 'device/service'), signer: valueOf(lock, `device/${role}/signer`) });
6
+ /**
7
+ * The drafted lines a place signs to admit a key: each device/<role>/<measure> line of head-s lock, said of keys/<id>, and
8
+ * its public half. The id is the role's own unless whoever admits names it; so is the coverage, unless they give it.
9
+ */
10
+ export const admissionOf = (lock, publicKey, role = HEAD, as = {}) => ((id, at) => [
11
+ ...['class', 'coverage', 'resolution', 'signer'].flatMap((measure) => ((line) => (line === undefined ? [] : [canonical({ at, by: 'target', form: line.form ?? 'alphabet', measure, role: 'writes', scope: `keys/${id}`, value: measure === 'coverage' && as.coverage ? as.coverage : line.value ?? '' })]))(said(lock, `device/${role}/${measure}`))),
12
+ canonical({ at, by: 'target', form: 'alphabet', measure: 'public-key', role: 'writes', scope: `keys/${id}`, value: publicKey }),
13
+ ])(as.id ?? valueOf(lock, `device/${role}/account`), as.at ?? 'policy:head/device');
14
+ export const algorithmOf = (lines) => ((era) => (lines.find((l) => l.scope === 'wire/signature-algorithms')?.value ?? '').split('|').find((one) => one.endsWith(`:${era}`)) ?? '')(valueOf(lines, 'wire/era'));
15
+ /** The id a place admits a public key under, as its own lines say: a key is who the place says it is, and a key it never admitted is no one there. */
16
+ export const idIn = (lines, publicKey) => publicKey === undefined ? undefined : lines.find((l) => l.scope?.startsWith('keys/') && l.measure === 'public-key' && l.value === publicKey)?.scope.slice('keys/'.length);
17
+ export const coverageOf = (lines, id) => id === undefined ? [] : lines.filter((l) => l.scope === `keys/${id}` && l.measure === 'coverage').flatMap((l) => members(l.value ?? ''));
18
+ export const covers = (lines, id, scope) => coverageOf(lines, id).some((glob) => matches(glob, scope));
19
+ /**
20
+ * What head says instead of asking, when this device's key may not sign every line it would ask for in a place: what the
21
+ * place admits it for and the first scope outside that, then the lines whoever configures the place can sign themselves.
22
+ */
23
+ export const uncoveredIn = (lock, lines, publicKey, place, lot, scopes = []) => {
24
+ const id = idIn(lines, publicKey);
25
+ const outside = [...scopes, ...lot.map((line) => ((got) => (got.kind === 'fact' ? got.value.fields['scope'] ?? '' : ''))(parse(line)))].find((scope) => !covers(lines, id, scope));
26
+ return outside === undefined ? undefined : [sayIn(lock)('not-covered', { place, coverage: coverageOf(lines, id).join('|') || 'nothing', scope: outside }), ...lot];
27
+ };
@@ -0,0 +1,11 @@
1
+ export type Said = Readonly<Record<string, string>>;
2
+ /** The lines head's own lock declares, decoded and nothing more: a withdraw has no place in it, and one there is refused, never folded here. */
3
+ export declare const declaredOf: (text: string) => readonly Said[];
4
+ export declare const said: (lock: readonly Said[], scope: string) => Said | undefined;
5
+ export declare const valueOf: (lock: readonly Said[], scope: string) => string;
6
+ export declare const aboutOf: (lock: readonly Said[], scope: string) => string;
7
+ export declare const members: (value: string) => readonly string[];
8
+ /** Whether a path a line needs lies under one of the folders a lock word names: a place's own, or one an older head kept. */
9
+ export declare const under: (lock: readonly Said[], scope: string, path: string | undefined) => boolean;
10
+ /** The lines exactly one step below a family: tool/look under tool, never tool/look/place. */
11
+ export declare const below: (lock: readonly Said[], family: string) => readonly Said[];
@@ -0,0 +1,13 @@
1
+ import { alphabet, parse, steps } from '@lapxo/topos/wire';
2
+ /** The lines head's own lock declares, decoded and nothing more: a withdraw has no place in it, and one there is refused, never folded here. */
3
+ export const declaredOf = (text) => text.split('\n').flatMap((line) => ((got) => (got.kind === 'fact' ? [got.value.fields] : []))(parse(line)))
4
+ .map((f) => { if (f.value === 'withdraw')
5
+ throw Error(`REFUSE·lock head's own lock withdraws ${f.scope}: its lines are declared, never folded by head`); return f; });
6
+ export const said = (lock, scope) => lock.find((f) => f.scope === scope);
7
+ export const valueOf = (lock, scope) => said(lock, scope)?.value ?? '';
8
+ export const aboutOf = (lock, scope) => said(lock, scope)?.about ?? '';
9
+ export const members = (value) => (value === '' ? [] : alphabet(value).members);
10
+ /** Whether a path a line needs lies under one of the folders a lock word names: a place's own, or one an older head kept. */
11
+ export const under = (lock, scope, path) => members(valueOf(lock, scope)).some((at) => path?.startsWith(`${at}/`) === true);
12
+ /** The lines exactly one step below a family: tool/look under tool, never tool/look/place. */
13
+ export const below = (lock, family) => lock.filter((f) => ((at) => at.length === 2 && at[0] === family)(steps(f.scope ?? '')));