@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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/TARGET.bound +443 -0
  3. package/dist/src/access/contract.d.ts +69 -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 +37 -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 +17 -0
  12. package/dist/src/core/act.js +17 -0
  13. package/dist/src/core/answer.d.ts +90 -0
  14. package/dist/src/core/answer.js +23 -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/keys.d.ts +14 -0
  18. package/dist/src/core/keys.js +20 -0
  19. package/dist/src/core/lock.d.ts +9 -0
  20. package/dist/src/core/lock.js +11 -0
  21. package/dist/src/core/read.d.ts +51 -0
  22. package/dist/src/core/read.js +60 -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 +45 -0
  26. package/dist/src/core/surface.js +32 -0
  27. package/dist/src/core/worlds.d.ts +10 -0
  28. package/dist/src/core/worlds.js +9 -0
  29. package/dist/src/flows/act.d.ts +7 -0
  30. package/dist/src/flows/act.js +43 -0
  31. package/dist/src/flows/answer.d.ts +12 -0
  32. package/dist/src/flows/answer.js +25 -0
  33. package/dist/src/flows/call.d.ts +50 -0
  34. package/dist/src/flows/call.js +76 -0
  35. package/dist/src/flows/device.d.ts +26 -0
  36. package/dist/src/flows/device.js +37 -0
  37. package/dist/src/flows/plan.d.ts +8 -0
  38. package/dist/src/flows/plan.js +51 -0
  39. package/dist/src/flows/read.d.ts +9 -0
  40. package/dist/src/flows/read.js +85 -0
  41. package/dist/src/flows/world.d.ts +12 -0
  42. package/dist/src/flows/world.js +29 -0
  43. package/dist/src/index.d.ts +2 -0
  44. package/dist/src/index.js +119 -0
  45. package/dist/src/places/admit.d.ts +7 -0
  46. package/dist/src/places/admit.js +32 -0
  47. package/dist/src/places/found.d.ts +6 -0
  48. package/dist/src/places/found.js +59 -0
  49. package/dist/src/ports/access/flow.d.ts +13 -0
  50. package/dist/src/ports/access/flow.js +120 -0
  51. package/dist/src/ports/access/page.d.ts +12 -0
  52. package/dist/src/ports/access/page.js +50 -0
  53. package/dist/src/ports/access/person.d.ts +25 -0
  54. package/dist/src/ports/access/person.js +34 -0
  55. package/dist/src/ports/adapters/request.d.ts +38 -0
  56. package/dist/src/ports/adapters/request.js +67 -0
  57. package/dist/src/ports/bound.d.ts +51 -0
  58. package/dist/src/ports/bound.js +68 -0
  59. package/dist/src/ports/cache.d.ts +4 -0
  60. package/dist/src/ports/cache.js +14 -0
  61. package/dist/src/ports/keychain.d.ts +7 -0
  62. package/dist/src/ports/keychain.js +11 -0
  63. package/dist/src/ports/place.d.ts +10 -0
  64. package/dist/src/ports/place.js +26 -0
  65. package/dist/src/ports/process.d.ts +20 -0
  66. package/dist/src/ports/process.js +49 -0
  67. package/dist/src/ports/signers.d.ts +6 -0
  68. package/dist/src/ports/signers.js +14 -0
  69. package/dist/src/ports/stores/credential-manager.d.ts +3 -0
  70. package/dist/src/ports/stores/credential-manager.js +22 -0
  71. package/dist/src/ports/stores/keychain.d.ts +3 -0
  72. package/dist/src/ports/stores/keychain.js +16 -0
  73. package/dist/src/ports/stores/secret-tool.d.ts +3 -0
  74. package/dist/src/ports/stores/secret-tool.js +16 -0
  75. package/dist/src/resources/view.d.ts +1 -0
  76. package/dist/src/resources/view.js +1 -0
  77. package/dist/src/signer.d.ts +2 -0
  78. package/dist/src/signer.js +14 -0
  79. package/dist/src/sources/access.d.ts +8 -0
  80. package/dist/src/sources/access.js +34 -0
  81. package/dist/src/sources/connect.d.ts +10 -0
  82. package/dist/src/sources/connect.js +68 -0
  83. package/dist/src/tools/access.d.ts +1 -0
  84. package/dist/src/tools/access.js +1 -0
  85. package/dist/src/tools/act.d.ts +1 -0
  86. package/dist/src/tools/act.js +1 -0
  87. package/dist/src/tools/admit.d.ts +1 -0
  88. package/dist/src/tools/admit.js +1 -0
  89. package/dist/src/tools/card.d.ts +3 -0
  90. package/dist/src/tools/card.js +13 -0
  91. package/dist/src/tools/connect.d.ts +1 -0
  92. package/dist/src/tools/connect.js +1 -0
  93. package/dist/src/tools/key.d.ts +3 -0
  94. package/dist/src/tools/key.js +11 -0
  95. package/dist/src/tools/land.d.ts +1 -0
  96. package/dist/src/tools/land.js +1 -0
  97. package/dist/src/tools/look.d.ts +8 -0
  98. package/dist/src/tools/look.js +40 -0
  99. package/dist/src/tools/place.d.ts +1 -0
  100. package/dist/src/tools/place.js +1 -0
  101. package/dist/src/tools/plan.d.ts +1 -0
  102. package/dist/src/tools/plan.js +1 -0
  103. package/dist/src/walk/context.d.ts +7 -0
  104. package/dist/src/walk/context.js +23 -0
  105. package/package.json +35 -4
  106. package/README.md +0 -3
@@ -0,0 +1,51 @@
1
+ import type { Said } from './lock.ts';
2
+ export type Plan = {
3
+ readonly kind: 'said';
4
+ readonly key: string;
5
+ readonly fields: Readonly<Record<string, string>>;
6
+ };
7
+ export type Fields = Readonly<Record<string, readonly string[]>>;
8
+ export interface Access {
9
+ readonly kind: string;
10
+ readonly scheme: string;
11
+ readonly params: Fields;
12
+ readonly account: string | undefined;
13
+ readonly flow: string;
14
+ }
15
+ export interface Call {
16
+ readonly kind: 'call';
17
+ readonly source: string;
18
+ readonly operation: string;
19
+ readonly origin: string;
20
+ readonly carry: Fields;
21
+ readonly form: Fields;
22
+ readonly classes: readonly string[];
23
+ readonly access: Access;
24
+ }
25
+ export declare const fieldsOf: (lines: readonly Said[], scope: string) => Fields;
26
+ export declare const vocabOf: (lock: readonly Said[]) => {
27
+ offers: string;
28
+ keys: string;
29
+ region: string;
30
+ class: string;
31
+ access: string;
32
+ origin: string;
33
+ origins: string;
34
+ issuers: string;
35
+ unresolved: string;
36
+ unparsed: string;
37
+ unsupported: string;
38
+ collision: string;
39
+ scheme: string;
40
+ form: string;
41
+ carry: string;
42
+ anonymous: string;
43
+ unstated: string;
44
+ callable: string;
45
+ split: string;
46
+ pick: string;
47
+ };
48
+ export type Vocab = ReturnType<typeof vocabOf>;
49
+ export declare const stepOf: (lines: readonly Said[], lock: readonly Said[], source: string, operation: string) => Call | Plan;
50
+ export declare const planRead: (lines: readonly Said[], lock: readonly Said[], source: string, operation: string, place: string) => Call | Plan;
51
+ export declare const planAct: (lines: readonly Said[], lock: readonly Said[], source: string, operation: string, place: string) => Call | Plan;
@@ -0,0 +1,60 @@
1
+ import { alphabet } from '@lapxo/topos/wire';
2
+ import { valueOf } from "./lock.js";
3
+ const said = (key, fields = {}) => ({ kind: 'said', key, fields });
4
+ const membersOf = (line) => (line === undefined ? undefined : alphabet(line.value ?? '').members);
5
+ export const fieldsOf = (lines, scope) => Object.fromEntries(lines.filter((l) => l.scope === scope && l.measure).map((l) => [l.measure, membersOf(l) ?? []]));
6
+ export const vocabOf = (lock) => ((w) => ({
7
+ offers: w('offers'), keys: w('keys'), region: w('region'), class: w('class'), access: w('access'), origin: w('origin'), origins: w('origins'), issuers: w('issuers'), unresolved: w('unresolved'), unparsed: w('unparsed'), unsupported: w('unsupported'), collision: w('collision'),
8
+ scheme: w('scheme'), form: w('form'), carry: w('carry'), anonymous: w('anonymous'), unstated: w('unstated'), callable: w('callable'), split: w('split'), pick: w('pick'),
9
+ }))((k) => valueOf(lock, `vocab/${k}`));
10
+ /** Where a source is reached: the origin the person picked, or the one its document declares when it declares one; several and none picked is no call, and none is ever a default. */
11
+ const originOf = (lines, v, source) => {
12
+ const at = fieldsOf(lines, `${v.region}/${source}`);
13
+ const picked = lines.find((l) => l.scope === `${v.pick}/${source}`)?.value;
14
+ if (picked !== undefined)
15
+ return picked;
16
+ const declared = at[v.origins] ?? [];
17
+ return declared.length === 1 ? declared[0] : said(declared.length ? 'pick-origin' : 'no-origin', { source, origins: declared.join(', ') });
18
+ };
19
+ /** How access is proven for an offer: its first declared alternative that says how what it obtains rides a request — anonymous, or a scheme whose flow the one adapter runs, its own or its issuer's, kept once per issuer; unstated, or none that says, is no call, named. */
20
+ const accessOf = (lines, lock, v, source, operation, place) => {
21
+ const ways = fieldsOf(lines, `${v.offers}/${source}/${operation}`)[v.access] ?? [v.unstated];
22
+ if (ways.includes(v.unstated))
23
+ return said('access-unstated', { source, operation });
24
+ const kindOf = (way) => {
25
+ if (way === v.anonymous)
26
+ return [way, v.anonymous, {}, '', undefined];
27
+ const issuer = fieldsOf(lines, `${v.keys}/${source}/${way}`)['issuer']?.[0];
28
+ const flow = issuer ?? `${source}/${way}`;
29
+ const params = fieldsOf(lines, `${v.keys}/${flow}`);
30
+ return [way, params[v.scheme] ? valueOf(lock, 'access/adapter') : '', params, flow, issuer];
31
+ };
32
+ const kinds = ways.map(kindOf);
33
+ const usable = kinds.find(([, kind]) => kind !== '');
34
+ if (usable === undefined)
35
+ return said('no-adapter', { source, operation, kinds: ways.join(', ') });
36
+ const [scheme, kind, params, flow, issuer] = usable;
37
+ return { kind, scheme, params, flow, account: kind === v.anonymous ? undefined : valueOf(lock, issuer ? 'access/issuer-account' : 'access/account').split('{place}').join(place).split('{source}').join(source).split('{issuer}').join(issuer ?? '') };
38
+ };
39
+ export const stepOf = (lines, lock, source, operation) => {
40
+ const v = vocabOf(lock);
41
+ const offer = `${v.offers}/${source}/${operation}`;
42
+ const classes = fieldsOf(lines, offer)[v.class];
43
+ if (classes === undefined)
44
+ return said('unknown-operation', { source, operation });
45
+ const origin = originOf(lines, v, source);
46
+ return typeof origin !== 'string' ? origin : { kind: 'call', source, operation, origin, carry: fieldsOf(lines, `${offer}/${v.carry}`), form: fieldsOf(lines, `${offer}/${v.form}`), classes, access: { kind: v.anonymous, scheme: '', params: {}, account: undefined, flow: '' } };
47
+ };
48
+ /** The call an offer would make, from the place's lines alone, with the access it is proven by; or why there is none. An offer read two ways is never called. */
49
+ const callOf = (lines, lock, source, operation, place) => {
50
+ const step = stepOf(lines, lock, source, operation);
51
+ if (step.kind === 'said')
52
+ return step;
53
+ if (step.classes.includes(vocabOf(lock).split))
54
+ return said('split-offer', { source, operation });
55
+ const access = accessOf(lines, lock, vocabOf(lock), source, operation, place);
56
+ return 'key' in access ? access : { ...step, access };
57
+ };
58
+ const isCallable = (lock, classes) => classes.length === 1 && classes[0] === vocabOf(lock).callable;
59
+ export const planRead = (lines, lock, source, operation, place) => ((call) => (call.kind === 'call' && !isCallable(lock, call.classes) ? said('not-callable', { source, operation, class: call.classes.join(', ') }) : call))(callOf(lines, lock, source, operation, place));
60
+ export const planAct = (lines, lock, source, operation, place) => ((call) => (call.kind === 'call' && isCallable(lock, call.classes) ? said('is-read', { source, operation }) : call))(callOf(lines, lock, source, operation, place));
@@ -0,0 +1,13 @@
1
+ import type { Said } from './lock.ts';
2
+ /** The scope a read is kept under: the source, the operation, and the first steps of the digest of the request and whom it was made as, so only the same request made as the same principal finds it again. The drafted receipt of one read names what was asked, the digest of what came back, and when; the device key signs it. */
3
+ export declare const scopeOf: (family: string, api: string, operation: string, request: string, principal: string) => string;
4
+ export declare const draftOf: (family: string, at: string, scope: string, digest: string, when: string) => string;
5
+ /** What a typed answer stands on besides its body: the worlds the place uses and the code that interprets them, and what the source compiles to; what is kept under it holds only while these stand. */
6
+ export declare const basisOf: (lines: readonly Said[], word: (scope: string) => string, source: string) => string;
7
+ /** Head's own receipts under a scope, or under any scope below it, newest first: each once, by when it was made and what it names; a duplicate delivery is the same receipt. */
8
+ export declare const receiptsOf: (lines: readonly Said[], at: string, scope: string, below?: boolean) => readonly {
9
+ readonly scope: string;
10
+ readonly at: string;
11
+ readonly digest: string;
12
+ }[];
13
+ export declare const spentOf: (lines: readonly Said[], family: string, at: string, api: string, now: number) => number;
@@ -0,0 +1,12 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { canonical } from '@lapxo/topos/wire';
3
+ /** The scope a read is kept under: the source, the operation, and the first steps of the digest of the request and whom it was made as, so only the same request made as the same principal finds it again. The drafted receipt of one read names what was asked, the digest of what came back, and when; the device key signs it. */
4
+ export const scopeOf = (family, api, operation, request, principal) => `${family}/${api}/${operation}/${createHash('sha256').update(`${request}\n${principal}`).digest('hex').slice(0, 16)}`;
5
+ export const draftOf = (family, at, scope, digest, when) => canonical({ at: `${at}:${when}`, by: 'target', form: 'alphabet', measure: 'digest', role: 'writes', scope, value: digest });
6
+ /** What a typed answer stands on besides its body: the worlds the place uses and the code that interprets them, and what the source compiles to; what is kept under it holds only while these stand. */
7
+ export const basisOf = (lines, word, source) => createHash('sha256').update(lines.filter((l) => l.scope?.startsWith('uses/') || l.scope?.startsWith('dep/')
8
+ || l.scope === `${word('vocab/region')}/${source}` || [word('vocab/offers'), word('vocab/keys')].some((family) => l.scope?.startsWith(`${family}/${source}/`))).map((l) => canonical(l)).sort().join('\n')).digest('hex').slice(0, 16);
9
+ /** Head's own receipts under a scope, or under any scope below it, newest first: each once, by when it was made and what it names; a duplicate delivery is the same receipt. */
10
+ export const receiptsOf = (lines, at, scope, below = false) => [...new Map(lines.filter((l) => (l.scope === scope || (below && l.scope?.startsWith(`${scope}/`))) && l.measure === 'digest' && l.at?.startsWith(`${at}:`) && l.value?.startsWith('sha256:'))
11
+ .map((l) => [`${l.at} ${l.value}`, { scope: l.scope, at: l.at.slice(at.length + 1), digest: l.value }])).values()].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : 0));
12
+ export const spentOf = (lines, family, at, api, now) => lines.filter((l) => l.scope?.startsWith(`${family}/${api}/`) && l.at?.startsWith(`${at}:`) && now - Date.parse(l.at.slice(at.length + 1)) < 3_600_000).length;
@@ -0,0 +1,45 @@
1
+ import type { Said } from './lock.ts';
2
+ export interface Field {
3
+ readonly name: string;
4
+ readonly about: string;
5
+ readonly type: string;
6
+ readonly required: boolean;
7
+ }
8
+ export interface Tool {
9
+ readonly name: string;
10
+ readonly about: string;
11
+ readonly fields: readonly Field[];
12
+ readonly hints: readonly string[];
13
+ }
14
+ export interface Resource {
15
+ readonly name: string;
16
+ readonly about: string;
17
+ readonly template: string;
18
+ readonly mime: string;
19
+ }
20
+ export interface Prompt {
21
+ readonly name: string;
22
+ readonly about: string;
23
+ readonly text: string;
24
+ readonly line: string;
25
+ readonly fields: readonly Field[];
26
+ }
27
+ export interface Surface {
28
+ readonly name: string;
29
+ readonly version: string;
30
+ readonly tools: readonly Tool[];
31
+ readonly resources: readonly Resource[];
32
+ readonly prompts: readonly Prompt[];
33
+ }
34
+ /** Everything a client is offered, read off the lock alone: its tools, resources and prompts, each with the words a model sees. */
35
+ export declare const surfaceOf: (lock: readonly Said[]) => Surface;
36
+ export declare const fill: (text: string, asked: Readonly<Record<string, string | undefined>>) => string;
37
+ export declare const sayIn: (lock: readonly Said[]) => (key: string, fields?: Readonly<Record<string, string | undefined>>) => string;
38
+ export declare const refusedOn: (lock: readonly Said[], platform: string) => string | undefined;
39
+ /** A red, when what head measured ran past a ceiling of its lock in whole seconds: said with the coordinate it ran at; within it, nothing. */
40
+ export declare const redIn: (lock: readonly Said[]) => (scope: string, coordinate: string, started: number, now: number) => readonly string[];
41
+ /**
42
+ * The fields a URI gives a resource template, or none when it is not one of its URIs: {name} matches one path step,
43
+ * {?a,b} names query fields any of which may be absent, in any order, each decoded.
44
+ */
45
+ export declare const matchTemplate: (template: string, uri: string) => Readonly<Record<string, string>> | undefined;
@@ -0,0 +1,32 @@
1
+ import { aboutOf, below, members, valueOf } from "./lock.js";
2
+ const nameOf = (line) => line.scope.split('/')[1];
3
+ const fieldsOf = (lock, scope) => ((required) => members(valueOf(lock, scope)).map((name) => ({
4
+ name, about: aboutOf(lock, `${scope}/${name}`), type: valueOf(lock, `${scope}/${name}`), required: required.includes(name)
5
+ })))(members(valueOf(lock, `${scope}/required`)));
6
+ /** Everything a client is offered, read off the lock alone: its tools, resources and prompts, each with the words a model sees. */
7
+ export const surfaceOf = (lock) => ({
8
+ name: valueOf(lock, 'name'), version: valueOf(lock, 'version'),
9
+ tools: below(lock, 'tool').map((line) => ({ name: nameOf(line), about: line.about ?? '', fields: fieldsOf(lock, line.scope), hints: members(valueOf(lock, `${line.scope}/hints`)) })),
10
+ resources: below(lock, 'resource').map((line) => ({ name: nameOf(line), about: line.about ?? '', template: line.value ?? '', mime: valueOf(lock, `${line.scope}/mime`) })),
11
+ prompts: below(lock, 'prompt').map((line) => ({ name: nameOf(line), about: line.about ?? '', text: aboutOf(lock, `${line.scope}/text`), line: aboutOf(lock, `${line.scope}/line`), fields: fieldsOf(lock, line.scope) })),
12
+ });
13
+ export const fill = (text, asked) => Object.entries(asked).reduce((at, [name, value]) => (value === undefined ? at : at.split(`{${name}}`).join(value)), text);
14
+ export const sayIn = (lock) => (key, fields = {}) => fill(aboutOf(lock, `said/${key}`), fields);
15
+ export const refusedOn = (lock, platform) => ((supported) => (supported.includes(platform) ? undefined : sayIn(lock)('platform', { platform, supported: supported.join('|') })))(members(valueOf(lock, 'platform/supported')));
16
+ /** A red, when what head measured ran past a ceiling of its lock in whole seconds: said with the coordinate it ran at; within it, nothing. */
17
+ export const redIn = (lock) => (scope, coordinate, started, now) => ((seconds, ceiling) => (seconds > Number(ceiling.split('..')[1]) ? [sayIn(lock)('red', { coordinate, seconds: String(seconds), scope, ceiling })] : []))(Math.floor((now - started) / 1000), valueOf(lock, scope));
18
+ /**
19
+ * The fields a URI gives a resource template, or none when it is not one of its URIs: {name} matches one path step,
20
+ * {?a,b} names query fields any of which may be absent, in any order, each decoded.
21
+ */
22
+ export const matchTemplate = (template, uri) => {
23
+ const [path = '', query = ''] = template.split('{?');
24
+ const names = [...path.matchAll(/\{(\w+)\}/g)].map((m) => m[1]);
25
+ const pattern = new RegExp(`^${path.split(/\{\w+\}/).map((part) => part.replace(/[.*+?^$()|[\]\\]/g, '\\$&')).join('([^/?#]+)')}(?:\\?(.*))?$`);
26
+ const got = pattern.exec(uri);
27
+ if (got === null)
28
+ return undefined;
29
+ const params = new URLSearchParams(got[names.length + 1] ?? '');
30
+ return { ...Object.fromEntries(names.map((name, i) => [name, decodeURIComponent(got[i + 1])])),
31
+ ...Object.fromEntries(query.replace('}', '').split(',').filter(Boolean).flatMap((name) => ((v) => (v === null ? [] : [[name, v]]))(params.get(name)))) };
32
+ };
@@ -0,0 +1,10 @@
1
+ import type { Said } from './lock.ts';
2
+ /** The facts bytes hold, in their order. */
3
+ export declare const linesOf: (bytes: Uint8Array) => readonly Said[];
4
+ /** Bytes named by their digest, under the algorithm the wire names: a pinned standing is exactly the bytes its pin hashes. */
5
+ export declare const digestIn: (lock: readonly Said[]) => (bytes: Uint8Array | string) => string;
6
+ /** A compiled standing as a place holds it: its pin, the digest of its bytes, and its lines; none when the place holds no such file. */
7
+ export declare const pinnedOf: (lock: readonly Said[], bytes: Uint8Array | undefined) => {
8
+ readonly pin: string;
9
+ readonly lines: readonly Said[];
10
+ } | undefined;
@@ -0,0 +1,9 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { parse } from '@lapxo/topos/wire';
3
+ import { valueOf } from "./lock.js";
4
+ /** The facts bytes hold, in their order. */
5
+ export const linesOf = (bytes) => new TextDecoder().decode(bytes).split('\n').flatMap((line) => ((got) => (got.kind === 'fact' ? [got.value.fields] : []))(parse(line)));
6
+ /** Bytes named by their digest, under the algorithm the wire names: a pinned standing is exactly the bytes its pin hashes. */
7
+ export const digestIn = (lock) => (bytes) => ((algorithm) => `${algorithm}:${createHash(algorithm).update(bytes).digest('hex')}`)(valueOf(lock, 'wire/digest-algorithms'));
8
+ /** A compiled standing as a place holds it: its pin, the digest of its bytes, and its lines; none when the place holds no such file. */
9
+ export const pinnedOf = (lock, bytes) => (bytes === undefined ? undefined : { pin: digestIn(lock)(bytes), lines: linesOf(bytes) });
@@ -0,0 +1,7 @@
1
+ import type { Asked, Context } from '../core/answer.ts';
2
+ /**
3
+ * One offer that is not of the class head reads: within a cap the person signed it is called as far as the cap goes;
4
+ * without one, each call asks the person once, and their yes is signed for exactly that request. The intent lands before
5
+ * the call, the receipt after; a call that never answered stays open until a look of what it would have changed closes it.
6
+ */
7
+ export declare const act: (asked: Asked, context: Context) => Promise<readonly string[]>;
@@ -0,0 +1,43 @@
1
+ import { actsOf, allowedOf, askedOf, cappedOf } from "../core/act.js";
2
+ import { planAct } from "../core/read.js";
3
+ import { draftOf } from "../core/receipt.js";
4
+ import { sayIn } from "../core/surface.js";
5
+ import { canonical } from '@lapxo/topos/wire';
6
+ import { called } from "./call.js";
7
+ /**
8
+ * One offer that is not of the class head reads: within a cap the person signed it is called as far as the cap goes;
9
+ * without one, each call asks the person once, and their yes is signed for exactly that request. The intent lands before
10
+ * the call, the receipt after; a call that never answered stays open until a look of what it would have changed closes it.
11
+ */
12
+ export const act = (asked, context) => called(asked, context, planAct, async ({ word, say, lines, source, operation, prepared, typed, attach, send, lands, widen, uncovered }) => {
13
+ const [family, done] = [word('act/family'), word('act/done')];
14
+ const allowed = allowedOf(lines, word('allow/family'), source, operation);
15
+ const scope = `${family}/${source}/${operation}/${prepared.key.slice(0, 16)}`;
16
+ const acts = actsOf(lines, family, [done, word('act/closed')], source, operation, prepared.key);
17
+ if (acts.open)
18
+ return say('open', { source, operation, intent: scope });
19
+ const stopped = uncovered('head', [scope, `${scope}/${done}`]);
20
+ if (stopped)
21
+ return stopped;
22
+ const capped = cappedOf(lines, word('allow/family'), source, operation);
23
+ if (capped && acts.done >= allowed)
24
+ return say('not-allowed', { source, operation, allowed: String(allowed), done: String(acts.done) });
25
+ if (!capped || askedOf(lines, word('ask/family'), source, operation)) {
26
+ const yes = await widen(sayIn(context.lock)('elicit-act', { source, operation, call: prepared.describe }), [canonical({ at: 'policy:person/allows', by: 'target', form: 'alphabet', measure: 'status', role: 'writes', scope: `${word('allow/family')}/${source}/${operation}/${prepared.key.slice(0, 16)}`, value: 'present' })]);
27
+ if ('said' in yes)
28
+ return yes.said;
29
+ }
30
+ const attached = await attach();
31
+ if ('said' in attached)
32
+ return attached.said;
33
+ const when = () => new Date(context.now()).toISOString();
34
+ const intended = await lands(canonical({ at: `${word('receipt/at')}:${when()}`, by: 'target', form: 'alphabet', measure: 'status', role: 'writes', scope, value: 'present' }));
35
+ if ('refused' in intended)
36
+ return say('not-landed', { why: intended.refused });
37
+ const got = await send(attached.attachment);
38
+ if (got.kind !== 'got')
39
+ return say('act-failed', { source, operation, intent: scope });
40
+ const landed = await lands(draftOf(family, word('receipt/at'), `${scope}/${done}`, context.cache.keep(Buffer.concat([Buffer.from(`${got.status}\n`), got.bytes])), when()));
41
+ const answer = await typed(String(got.status), got.bytes);
42
+ return 'refused' in landed ? [...answer, ...say('not-landed', { why: landed.refused })] : [...answer, ...intended.lines, ...landed.lines];
43
+ });
@@ -0,0 +1,12 @@
1
+ import type { Context } from '../core/answer.ts';
2
+ import type { Said } from '../core/lock.ts';
3
+ /**
4
+ * What a source answered, as the world types it: the body is laid in the place's answers for as long as bound reads it,
5
+ * then taken away, and only the lines the world answers for this request come back — a value of each declared field, the
6
+ * counts, what it held back. A place that names no view to type it gets no answer at all, never the body.
7
+ */
8
+ export declare const typedAnswer: (context: Context, place: string, lines: readonly Said[], source: string, operation: string, key: string, body: Uint8Array) => Promise<{
9
+ readonly lines: readonly string[];
10
+ } | {
11
+ readonly untyped: string;
12
+ }>;
@@ -0,0 +1,25 @@
1
+ import { canonical } from '@lapxo/topos/wire';
2
+ import { valueOf } from "../core/lock.js";
3
+ /**
4
+ * What a source answered, as the world types it: the body is laid in the place's answers for as long as bound reads it,
5
+ * then taken away, and only the lines the world answers for this request come back — a value of each declared field, the
6
+ * counts, what it held back. A place that names no view to type it gets no answer at all, never the body.
7
+ */
8
+ export const typedAnswer = async (context, place, lines, source, operation, key, body) => {
9
+ const view = valueOf(context.lock, 'place/answer');
10
+ if (!lines.some((line) => line.scope === `view/${view}`))
11
+ return { untyped: '' };
12
+ const [request, family] = [key.slice(0, 16), valueOf(context.lock, 'vocab/answer')];
13
+ const at = `${valueOf(context.lock, 'place/answers')}/${source}/${encodeURIComponent(operation).replaceAll('.', '%2E')}/${request}.json`;
14
+ context.files.lay(place, at, body);
15
+ try {
16
+ const got = await context.world(place, view);
17
+ if ('refused' in got)
18
+ return { untyped: got.refused };
19
+ const mine = got.lines.filter((line) => line.scope === `${family}/${request}` || line.scope?.startsWith(`${family}/${request}/`));
20
+ return mine.length ? { lines: mine.map((line) => canonical(line)).sort() } : { untyped: '' };
21
+ }
22
+ finally {
23
+ context.files.drop(place, at);
24
+ }
25
+ };
@@ -0,0 +1,50 @@
1
+ import type { Asked, Context, Outcome } from '../core/answer.ts';
2
+ import type { Said } from '../core/lock.ts';
3
+ import type { Call, Plan } from '../core/read.ts';
4
+ import type { Attachment } from '../access/contract.ts';
5
+ import type { Fetched, Prepared } from '../ports/adapters/request.ts';
6
+ /**
7
+ * The origin of a source a person picks once for a place, when its document declares several: offered as a choice among
8
+ * them, none a default, and kept as the person's line; the lines a call stands on, with it.
9
+ */
10
+ export declare const pickedOrigin: (context: Context, place: string, lines: readonly Said[], source: string, plan: Call | Plan) => Promise<{
11
+ readonly lines: readonly Said[];
12
+ } | {
13
+ readonly said: readonly string[];
14
+ }>;
15
+ /** Calls of one place take turns, so one asked while another lands sees its receipt; nothing waits on a timer. */
16
+ export declare const inTurn: <T>(place: string, act: () => Promise<T>) => Promise<T>;
17
+ export interface Frame {
18
+ readonly say: (key: string, fields?: Readonly<Record<string, string | undefined>>) => readonly string[];
19
+ readonly lands: (...lot: readonly string[]) => Promise<Outcome>;
20
+ readonly most: (scope: string) => number;
21
+ readonly place: string;
22
+ readonly lines: readonly Said[];
23
+ readonly source: string;
24
+ readonly operation: string;
25
+ readonly plan: Call;
26
+ readonly prepared: Extract<Prepared, {
27
+ key: string;
28
+ }>;
29
+ readonly widen: (message: string, lot: readonly string[]) => Promise<{
30
+ readonly lines: readonly string[];
31
+ } | {
32
+ readonly said: readonly string[];
33
+ }>;
34
+ readonly word: (scope: string) => string;
35
+ readonly typed: (status: string, bytes: Uint8Array) => Promise<readonly string[]>;
36
+ readonly attach: () => Promise<{
37
+ readonly attachment: Attachment | undefined;
38
+ readonly principal: string | undefined;
39
+ } | {
40
+ readonly said: readonly string[];
41
+ }>;
42
+ readonly send: (attachment: Attachment | undefined) => Promise<Fetched>;
43
+ readonly uncovered: (role: string, scopes: readonly string[]) => readonly string[] | undefined;
44
+ }
45
+ /**
46
+ * The steps a read and an act share: the place, its turn, the lines it stands on once bound settled them, the offer's
47
+ * plan, the transport its origin names and the request it prepares; then what is particular, given a frame that says
48
+ * by the lock, attaches access when asked, types what came back and lands a line under head's key.
49
+ */
50
+ export declare const called: (asked: Asked, context: Context, planOf: (lines: readonly Said[], lock: readonly Said[], source: string, operation: string, place: string) => Call | Plan, then: (frame: Frame) => Promise<readonly string[]>) => Promise<readonly string[]>;
@@ -0,0 +1,76 @@
1
+ import { inPlace, placeNameOf, unless } from "../core/answer.js";
2
+ import { fieldsOf, vocabOf } from "../core/read.js";
3
+ import { canonical } from '@lapxo/topos/wire';
4
+ import { sayIn } from "../core/surface.js";
5
+ import { valueOf } from "../core/lock.js";
6
+ import { uncoveredIn } from "../core/device.js";
7
+ import { widening } from "./device.js";
8
+ import { attachmentFor } from "../access/obtain.js";
9
+ import { typedAnswer } from "./answer.js";
10
+ import { standingFor } from "./world.js";
11
+ /**
12
+ * The origin of a source a person picks once for a place, when its document declares several: offered as a choice among
13
+ * them, none a default, and kept as the person's line; the lines a call stands on, with it.
14
+ */
15
+ export const pickedOrigin = async (context, place, lines, source, plan) => {
16
+ if (plan.kind !== 'said' || plan.key !== 'pick-origin')
17
+ return { lines };
18
+ const v = vocabOf(context.lock);
19
+ const origins = fieldsOf(lines, `${v.region}/${source}`)[v.origins] ?? [];
20
+ const line = (value) => ({ at: 'policy:person/picks', by: 'target', form: 'alphabet', measure: 'id', role: 'writes', scope: `${v.pick}/${source}`, value });
21
+ const by = widening(context, place, lines, origins.map((o) => canonical(line(o))));
22
+ if ('said' in by)
23
+ return { said: by.said };
24
+ const chosen = (await context.person.choose(sayIn(context.lock)('pick', { source }), [{ name: v.origin, options: origins.map((o) => ({ value: o, label: o })) }]))?.[v.origin];
25
+ if (chosen === undefined || !origins.includes(chosen))
26
+ return { said: [sayIn(context.lock)(plan.key, plan.fields)] };
27
+ const landed = await by.land([canonical(line(chosen))]);
28
+ return 'refused' in landed ? { said: [sayIn(context.lock)('not-landed', { why: landed.refused })] } : { lines: [...lines, line(chosen)] };
29
+ };
30
+ const turns = new Map();
31
+ /** Calls of one place take turns, so one asked while another lands sees its receipt; nothing waits on a timer. */
32
+ export const inTurn = (place, act) => { const next = (turns.get(place) ?? Promise.resolve()).then(act, act); turns.set(place, next.catch(() => undefined)); return next; };
33
+ /**
34
+ * The steps a read and an act share: the place, its turn, the lines it stands on once bound settled them, the offer's
35
+ * plan, the transport its origin names and the request it prepares; then what is particular, given a frame that says
36
+ * by the lock, attaches access when asked, types what came back and lands a line under head's key.
37
+ */
38
+ export const called = async (asked, context, planOf, then) => {
39
+ const say = (key, fields = {}) => [sayIn(context.lock)(key, fields)];
40
+ const [source, operation] = [asked['source'] ?? '', asked['operation'] ?? ''];
41
+ return inPlace(asked, context, (place) => inTurn(place, async () => {
42
+ const held = await standingFor(context, place);
43
+ if ('unsettled' in held)
44
+ return say('unsettled', { source, why: held.unsettled });
45
+ const widen = async (message, lot) => {
46
+ const by = widening(context, place, held.lines, lot);
47
+ if ('said' in by)
48
+ return { said: by.said };
49
+ const refused = await unless(context, message);
50
+ if (refused)
51
+ return { said: refused };
52
+ const landed = await by.land(lot);
53
+ return 'refused' in landed ? { said: say('not-landed', { why: landed.refused }) } : { lines: landed.lines };
54
+ };
55
+ const picked = await pickedOrigin(context, place, held.lines, source, planOf(held.lines, context.lock, source, operation, placeNameOf(place)));
56
+ if ('said' in picked)
57
+ return picked.said;
58
+ const lines = picked.lines;
59
+ const plan = planOf(lines, context.lock, source, operation, placeNameOf(place));
60
+ if (plan.kind === 'said')
61
+ return say(plan.key, plan.fields);
62
+ const prepared = ((transport) => transport?.prepare(plan, JSON.parse(asked['args'] || '{}'), context.lock))(await context.adapter(plan.origin));
63
+ if (prepared === undefined)
64
+ return say('no-transport', { source, origin: plan.origin });
65
+ if ('missing' in prepared)
66
+ return say('missing-param', { operation, param: prepared.missing });
67
+ if ('refused' in prepared)
68
+ return say(prepared.refused, { source, origin: plan.origin });
69
+ const most = (scope) => Number((lines.find((l) => l.scope === scope)?.value ?? '0..0').split('..')[1]);
70
+ const typed = async (status, bytes) => ((got) => ['lines' in got ? say('answered', { source, operation, status })[0] : say('untyped', { source, operation, status, why: got.untyped })[0], ...('lines' in got ? got.lines : [])])(await typedAnswer(context, place, lines, source, operation, prepared.key, bytes));
71
+ const lands = (...lot) => context.landAs(place, lot, 'head', lines);
72
+ const attach = async () => ((got) => ('said' in got ? { said: say(got.said, { source, operation, kinds: plan.access.kind, input: got.input }) } : got))(await attachmentFor(context, place, lines, plan));
73
+ const uncovered = (role, scopes) => uncoveredIn(context.lock, lines, context.device(role), place, [], scopes);
74
+ return then({ widen, word: (scope) => valueOf(context.lock, scope), say, place, lines, source, operation, plan, prepared, most, typed, attach, send: (attachment) => prepared.send(attachment, most('read/response-bytes')), lands, uncovered });
75
+ }));
76
+ };
@@ -0,0 +1,26 @@
1
+ import type { Context, Outcome } from '../core/answer.ts';
2
+ import type { Said } from '../core/lock.ts';
3
+ /** A key of this device by its role: read from the keychain, or made there once; only its public half is ever handed on. */
4
+ export declare const deviceKey: (context: Context, role?: string) => string | undefined;
5
+ /** The keychain account of a place's root, when this keychain holds it — who configures the place is here; else what head says of that. */
6
+ export declare const rootIn: (context: Context, place: string) => {
7
+ readonly account: string;
8
+ } | {
9
+ readonly said: readonly string[];
10
+ };
11
+ /**
12
+ * Who signs a widening of a place: its root, when this keychain holds it and the place admits this device's person to
13
+ * witness it — the line is then the place's own policy, in its lock, binding every device it admits, and names as its
14
+ * witness the person who said yes, by the key the place knows them by; else this device's person key, when the place
15
+ * admits it for every line — an attestation only this device reads, once per scope and measure the place does not state,
16
+ * since a second needs a fold bound does not publish; else no one here, and the lines are handed on. A place that pins a
17
+ * lock as its parent has nothing signed here: what the parent would cut is bound's to judge.
18
+ */
19
+ type By = {
20
+ readonly sign: (lot: readonly string[]) => Promise<Outcome>;
21
+ readonly land: (lot: readonly string[]) => Promise<Outcome>;
22
+ };
23
+ export declare const widening: (context: Context, place: string, lines: readonly Said[], lot: readonly string[]) => By | {
24
+ readonly said: readonly string[];
25
+ };
26
+ export {};
@@ -0,0 +1,37 @@
1
+ import { placeNameOf } from "../core/answer.js";
2
+ import { canonical, parse } from '@lapxo/topos/wire';
3
+ import { deviceOf, idIn, uncoveredIn } from "../core/device.js";
4
+ import { newKey, publicOf } from "../core/keys.js";
5
+ import { members, valueOf } from "../core/lock.js";
6
+ import { fill, sayIn } from "../core/surface.js";
7
+ import { adoptsIn } from "../adopt/adopt.js";
8
+ /** A key of this device by its role: read from the keychain, or made there once; only its public half is ever handed on. */
9
+ export const deviceKey = (context, role) => ((account) => {
10
+ const held = context.keys.read(account);
11
+ const secret = held ?? ((made) => { context.keys.write(account, made.secret); return context.keys.read(account) === made.secret ? made.secret : undefined; })(newKey());
12
+ return secret === undefined ? undefined : publicOf(secret);
13
+ })(deviceOf(context.lock, role).account);
14
+ /** The keychain account of a place's root, when this keychain holds it — who configures the place is here; else what head says of that. */
15
+ export const rootIn = (context, place) => ((account) => (context.keys.read(account) === undefined ? { said: [sayIn(context.lock)('no-root', { place })] } : { account }))(fill(valueOf(context.lock, 'founding/root-account'), { place: placeNameOf(place) }));
16
+ export const widening = (context, place, lines, lot) => {
17
+ const parent = adoptsIn(context.lock, lines);
18
+ if (parent !== undefined)
19
+ return { said: [sayIn(context.lock)('parent-lock', { place, parent })] };
20
+ const [root, witness, field] = [rootIn(context, place), idIn(lines, context.device('person')), valueOf(context.lock, 'widening/witness')];
21
+ const wire = lines.filter((l) => l.scope === 'wire/fields');
22
+ const witnessed = (signed) => signed.map((line) => ((got) => (got.kind === 'fact' ? canonical({ ...got.value.fields, [field]: witness }) : line))(parse(line)));
23
+ if ('account' in root && witness !== undefined) {
24
+ if (wire.some((l) => members(l.value ?? '').includes(field)))
25
+ return { sign: (signed) => context.signRoot(place, witnessed(signed), root.account), land: (signed) => context.landRoot(place, witnessed(signed), root.account) };
26
+ if (lot.every((line) => / scope=wire\/fields /.test(line)))
27
+ return { sign: (signed) => context.signRoot(place, signed, root.account), land: (signed) => context.landRoot(place, signed, root.account) };
28
+ const [named, bare] = [[...new Set([...wire.flatMap((l) => members(l.value ?? '')), field])].join('|'), (l) => Object.fromEntries(Object.entries(l).filter(([k]) => !['sig', 'epoch'].includes(k)))];
29
+ return { said: [sayIn(context.lock)('unwitnessed', { place }), ...wire.map((l) => canonical({ ...bare(l), by: 'target', value: 'withdraw' })),
30
+ canonical({ at: `policy:head/wire/${new Date(context.now()).toISOString()}`, by: 'target', form: 'alphabet', measure: 'id', role: 'reads', scope: 'wire/fields', value: named })] };
31
+ }
32
+ const stopped = uncoveredIn(context.lock, lines, context.device('person'), place, lot);
33
+ if (stopped)
34
+ return { said: stopped };
35
+ const again = lot.flatMap((line) => ((got) => (got.kind === 'fact' && lines.some((l) => l.scope === got.value.fields.scope && l.measure === got.value.fields.measure) ? [got.value.fields.scope ?? ''] : []))(parse(line)))[0];
36
+ return again !== undefined ? { said: [sayIn(context.lock)('attested-again', { place, scope: again })] } : { sign: (signed) => context.signAs(place, signed, 'person', lines), land: (signed) => context.landAs(place, signed, 'person', lines) };
37
+ };
@@ -0,0 +1,8 @@
1
+ import type { Asked, Context } from '../core/answer.ts';
2
+ /**
3
+ * A proposal the person signs: drafted lines shown to the person with the place and the base they are made against; on
4
+ * their yes to exactly those, signed by their key; what bound prints folding it as if landed comes back as it printed it. Nothing lands; the lot is kept by its digest.
5
+ */
6
+ export declare const planLot: (asked: Asked, context: Context) => Promise<readonly string[]>;
7
+ /** A planned lot landed by its digest, only while the place stands on the base it was planned against. */
8
+ export declare const landLot: (asked: Asked, context: Context) => Promise<readonly string[]>;
@@ -0,0 +1,51 @@
1
+ import { baseOf, inPlace, standingOr } from "../core/answer.js";
2
+ import { sayIn } from "../core/surface.js";
3
+ import { widening } from "./device.js";
4
+ import { parse } from '@lapxo/topos/wire';
5
+ /**
6
+ * A proposal the person signs: drafted lines shown to the person with the place and the base they are made against; on
7
+ * their yes to exactly those, signed by their key; what bound prints folding it as if landed comes back as it printed it. Nothing lands; the lot is kept by its digest.
8
+ */
9
+ export const planLot = (asked, context) => inPlace(asked, context, async (place) => {
10
+ const say = sayIn(context.lock);
11
+ const lot = (asked['lines'] ?? '').split('\n').filter(Boolean);
12
+ if (!lot.length || lot.some((line) => ((got) => got.kind !== 'fact' || got.value.fields['sig'] !== undefined || got.value.fields['by'] !== 'target')(parse(line))))
13
+ return [say('bad-lot')];
14
+ const held = await standingOr(context, place);
15
+ if ('said' in held)
16
+ return held.said;
17
+ const base = baseOf(held.lines);
18
+ const by = widening(context, place, held.lines, lot);
19
+ if ('said' in by)
20
+ return by.said;
21
+ const consent = await context.elicit(say('elicit', { count: String(lot.length), place, base, lines: lot.join('\n') }));
22
+ if (consent === 'none')
23
+ return [say('sign-yourself', { place }), ...lot];
24
+ if (consent === 'no')
25
+ return [say('no-yes')];
26
+ const signed = await by.sign(lot);
27
+ if ('refused' in signed)
28
+ return [signed.refused];
29
+ const now = await standingOr(context, place);
30
+ if ('said' in now)
31
+ return now.said;
32
+ if (baseOf(now.lines) !== base)
33
+ return [say('base-moved', { base })];
34
+ const kept = context.cache.keep(Buffer.from(JSON.stringify({ place, base, signed: signed.lines })));
35
+ return [say('planned', { lot: kept, base }), ...(await context.told(place, signed.lines))];
36
+ });
37
+ /** A planned lot landed by its digest, only while the place stands on the base it was planned against. */
38
+ export const landLot = async (asked, context) => {
39
+ const say = sayIn(context.lock);
40
+ const bytes = context.cache.held(asked['lot'] ?? '');
41
+ if (bytes === undefined)
42
+ return [say('no-lot', { lot: asked['lot'] ?? '' })];
43
+ const kept = JSON.parse(Buffer.from(bytes).toString('utf8'));
44
+ const now = await standingOr(context, kept.place);
45
+ if ('said' in now)
46
+ return now.said;
47
+ if (baseOf(now.lines) !== kept.base)
48
+ return [say('base-moved', { base: kept.base })];
49
+ const landed = await context.land(kept.place, kept.signed);
50
+ return 'refused' in landed ? [landed.refused] : [say('landed', { count: String(landed.lines.length) }), ...landed.lines];
51
+ };
@@ -0,0 +1,9 @@
1
+ import type { Asked, Context } from '../core/answer.ts';
2
+ /**
3
+ * A read of one offer the place declares of the class head reads: the response comes back once, typed by the world,
4
+ * and both are kept — the body's digest and the typed answer's, receipts the device key signs. Asked again as the same
5
+ * principal, while the place stands on what typed it, the person's grant holds now and the newest read is younger than
6
+ * the reuse the place allows, it answers from what was kept: nothing called. Asked for history, it shows what was kept
7
+ * of that request as of when it was read — evidence of that read, never a current answer — where the place allows it.
8
+ */
9
+ export declare const readSource: (asked: Asked, context: Context) => Promise<readonly string[]>;