@lapxo/head 0.1.7 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -14
- package/TARGET.bound +237 -31
- package/dist/src/adopt/adopt.d.ts +2 -2
- package/dist/src/adopt/adopt.js +20 -6
- package/dist/src/ahead/read.d.ts +2 -2
- package/dist/src/ahead/read.js +2 -4
- package/dist/src/answers/answered.js +1 -1
- package/dist/src/compile/pin.d.ts +1 -1
- package/dist/src/compile/pin.js +5 -3
- package/dist/src/core/answer.d.ts +12 -0
- package/dist/src/core/answer.js +1 -1
- package/dist/src/core/association.d.ts +16 -0
- package/dist/src/core/association.js +34 -0
- package/dist/src/core/composition.d.ts +32 -0
- package/dist/src/core/composition.js +28 -0
- package/dist/src/core/package.d.ts +1 -0
- package/dist/src/core/package.js +17 -4
- package/dist/src/core/read.d.ts +2 -1
- package/dist/src/core/read.js +2 -1
- package/dist/src/core/receiver.d.ts +11 -0
- package/dist/src/core/receiver.js +122 -0
- package/dist/src/core/share-bundle.d.ts +9 -0
- package/dist/src/core/share-bundle.js +56 -0
- package/dist/src/doors/head.d.ts +1 -0
- package/dist/src/doors/head.js +11 -3
- package/dist/src/flows/answer.js +7 -3
- package/dist/src/flows/call.d.ts +1 -1
- package/dist/src/flows/call.js +28 -4
- package/dist/src/flows/device.d.ts +14 -5
- package/dist/src/flows/device.js +15 -7
- package/dist/src/flows/plan.d.ts +2 -0
- package/dist/src/flows/plan.js +16 -2
- package/dist/src/index.js +3 -31
- package/dist/src/links/birth.d.ts +6 -0
- package/dist/src/links/birth.js +48 -0
- package/dist/src/links/inspect.d.ts +1 -1
- package/dist/src/links/inspect.js +23 -12
- package/dist/src/links/link.d.ts +2 -2
- package/dist/src/links/link.js +7 -10
- package/dist/src/onboard/first.js +1 -1
- package/dist/src/onboard/owed.js +1 -1
- package/dist/src/places/admit.js +1 -1
- package/dist/src/places/found.js +3 -1
- package/dist/src/ports/adapters/bundle.d.ts +44 -0
- package/dist/src/ports/adapters/bundle.js +173 -0
- package/dist/src/ports/adapters/configured.d.ts +35 -0
- package/dist/src/ports/adapters/configured.js +67 -0
- package/dist/src/ports/adapters/mcp.d.ts +59 -0
- package/dist/src/ports/adapters/mcp.js +122 -0
- package/dist/src/ports/adapters/network.d.ts +12 -0
- package/dist/src/ports/adapters/network.js +67 -0
- package/dist/src/ports/adapters/protocol.d.ts +6 -0
- package/dist/src/ports/adapters/protocol.js +51 -0
- package/dist/src/ports/adapters/remote.d.ts +13 -0
- package/dist/src/ports/adapters/remote.js +33 -0
- package/dist/src/ports/bound.d.ts +0 -1
- package/dist/src/ports/bound.js +14 -15
- package/dist/src/ports/host.d.ts +18 -0
- package/dist/src/ports/host.js +58 -9
- package/dist/src/sources/connect.d.ts +1 -0
- package/dist/src/sources/connect.js +15 -11
- package/dist/src/tools/card.js +3 -2
- package/dist/src/use/named.js +3 -3
- package/dist/src/verbs/ask.js +7 -1
- package/dist/src/verbs/card.js +2 -2
- package/dist/src/verbs/do.js +2 -1
- package/package.json +6 -5
- package/runtime.bundle.json +24026 -0
- package/dist/src/ports/reading.d.ts +0 -8
- package/dist/src/ports/reading.js +0 -14
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { configuredProtocol } from "../ports/adapters/configured.js";
|
|
2
|
+
import { dirname, basename } from 'node:path';
|
|
3
|
+
import { canonical, parse } from '@lapxo/topos/wire';
|
|
4
|
+
import { compose } from "./composition.js";
|
|
5
|
+
import { sayIn } from "./surface.js";
|
|
6
|
+
import { digestIn, linesOf } from "./worlds.js";
|
|
7
|
+
import { foreignIn, sharePath } from "./package.js";
|
|
8
|
+
import { valueOf } from "./lock.js";
|
|
9
|
+
/** Refusals expose only declared public bundle codes, never host paths or parsed payloads. */
|
|
10
|
+
export const bundleRefusal = (context, error) => [sayIn(context.lock)('bundle-refused', { why: error instanceof Error && /^bundle\/[a-z-]+$/.test(error.message) ? error.message : 'bundle/invalid' })];
|
|
11
|
+
/** Export only the existing public package. Connecting a document remains the consented connect action; it is never hidden in export. */
|
|
12
|
+
export async function shareBundle(asked, context, read) {
|
|
13
|
+
const to = asked['to'] ?? '', say = sayIn(context.lock), suffix = valueOf(context.lock, 'share/bundle-suffix');
|
|
14
|
+
if (!suffix || !to.endsWith(suffix))
|
|
15
|
+
return undefined;
|
|
16
|
+
if (!context.bundle || !await sharePath(context, asked['place'] ?? '', to) || context.files.has(dirname(to), basename(to)))
|
|
17
|
+
return [say('bundle-refused', { why: 'bundle/unavailable-or-destination' })];
|
|
18
|
+
try {
|
|
19
|
+
const payload = asked['args'] ? JSON.parse(asked['args']) : undefined;
|
|
20
|
+
let material = payload ? await read() : undefined;
|
|
21
|
+
if (material) {
|
|
22
|
+
for (const choice of payload.sources)
|
|
23
|
+
if (!material.sources.some(source => source.name === choice.source)) {
|
|
24
|
+
const url = choice.document && URL.canParse(choice.document) ? new URL(choice.document) : undefined;
|
|
25
|
+
if (!url || url.protocol !== valueOf(context.lock, 'use/scheme') || url.href !== url.origin + url.pathname)
|
|
26
|
+
throw Error('bundle/source-document');
|
|
27
|
+
return [say(choice.protocol === configuredProtocol.id ? configuredProtocol.sourceNeeded : 'bundle-source-needed', { source: choice.source, document: url.href })];
|
|
28
|
+
}
|
|
29
|
+
const lock = material.files.get('TARGET.bound');
|
|
30
|
+
if (!lock || new TextDecoder().decode(lock).split('\n').filter(Boolean).some(line => parse(line).kind !== 'fact') || foreignIn(linesOf(lock)))
|
|
31
|
+
throw Error('bundle/public-lock');
|
|
32
|
+
const declared = linesOf(lock);
|
|
33
|
+
for (const source of material.sources)
|
|
34
|
+
if (!declared.some(line => line.scope === `uses/${source.name}` && line.value === source.pin) || !declared.some(line => line.scope === `sources/${source.name}` && line.value === source.document))
|
|
35
|
+
throw Error('bundle/source-pin');
|
|
36
|
+
const made = compose(payload, material.sources, Number(valueOf(context.lock, 'share/bundle-selections').split('..')[1])), selected = new Set(made.entries.map(entry => entry.source));
|
|
37
|
+
const lines = linesOf(lock).filter(line => !line.needs || selected.has(line.scope.split('/')[1]));
|
|
38
|
+
if (lines.some(line => line.restsOn?.split('|').some(parent => !selected.has(parent.split('/')[1]))))
|
|
39
|
+
throw Error('bundle/unselected-parent');
|
|
40
|
+
const retained = new Map(lines.filter(line => line.needs).map(line => [line.needs, material.files.get(line.needs)]));
|
|
41
|
+
retained.set('TARGET.bound', Buffer.from(lines.map(line => canonical(line)).join('\n') + '\n'));
|
|
42
|
+
material = { sources: material.sources.filter(source => selected.has(source.name)), files: retained };
|
|
43
|
+
for (const line of lines.filter(line => line.needs && line.measure === 'digest')) {
|
|
44
|
+
const bytes = material.files.get(line.needs);
|
|
45
|
+
if (!bytes || digestIn(context.lock)(bytes) !== line.value)
|
|
46
|
+
throw Error('bundle/public-digest');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const got = await context.bundle(material && payload ? { recipe: payload, ...material } : undefined);
|
|
50
|
+
context.files.lay(dirname(to), basename(to), got.bytes);
|
|
51
|
+
return [say('bundle-shared', { document: to, digest: got.digest, sources: got.sources.join(', ') })];
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return bundleRefusal(context, error);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/dist/src/doors/head.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface Door extends Client {
|
|
|
5
5
|
readonly roots: () => Promise<readonly string[]>;
|
|
6
6
|
readonly client: () => string | undefined;
|
|
7
7
|
readonly name: string;
|
|
8
|
+
readonly network?: Parameters<typeof import('../ports/adapters/remote.ts').remoteAt>[2];
|
|
8
9
|
}
|
|
9
10
|
export declare const askedOf: (raw: Readonly<Record<string, unknown>> | undefined) => Asked;
|
|
10
11
|
/** head behind a door: its lock read, every call answered one at a time and each answer's next read ahead, the same for every door. */
|
package/dist/src/doors/head.js
CHANGED
|
@@ -13,7 +13,7 @@ import { aboutOf, members, valueOf } from "../core/lock.js";
|
|
|
13
13
|
import { sayIn } from "../core/surface.js";
|
|
14
14
|
import { matchTemplate, refusedOn, shapeOf, surfaceOf } from "../ports/surface.js";
|
|
15
15
|
import { birth, closing, founded, land, landAct, limits, pinnedAt, policyAt, run, settledView, sign, standingAt, told, words } from "../ports/bound.js";
|
|
16
|
-
import { configureHost } from "../ports/host.js";
|
|
16
|
+
import { configureHost, authorityAt } from "../ports/host.js";
|
|
17
17
|
import { held, keep } from "../ports/cache.js";
|
|
18
18
|
import { dropAt, hasAt, layAt, listAt, takeAt } from "../ports/place.js";
|
|
19
19
|
import * as documents from "../ports/documents.js";
|
|
@@ -68,10 +68,18 @@ export const headOf = (lock, root, ext, door) => {
|
|
|
68
68
|
const acted = (place, root, done) => done.then((got) => { if ('lines' in got && !got.lines.every((l) => ((f) => f.kind === 'fact' && read(f.value.fields['at']))(parse(l))))
|
|
69
69
|
moved.add(place); return got; }).finally(() => { forget(place); if (root)
|
|
70
70
|
touched.add(place); });
|
|
71
|
+
const covered = async (place, lot, then) => {
|
|
72
|
+
const checked = await authorityAt(place, valueOf(lock, 'founding/root').split('/')[1], 'record-coverage', lot, Date.now(), signal);
|
|
73
|
+
return 'refused' in checked ? { refused: checked.refused } : checked.decision !== 'allow' ? { refused: `REFUSE·authority-preflight/${checked.decision} ${checked.needs.join('|')}` } : then();
|
|
74
|
+
};
|
|
71
75
|
return {
|
|
72
76
|
lock,
|
|
77
|
+
remote: async () => (await import(__rewriteRelativeImportExtension(new URL(`../ports/adapters/remote${ext}`, import.meta.url).href))).remoteAt({ pages: Number(valueOf(lock, 'share/bundle-selections').split('..')[1]), tools: Number(valueOf(lock, 'share/bundle-selections').split('..')[1]), bytes: Number(valueOf(lock, 'share/bundle-bytes').split('..')[1]), wireBytes: Number(valueOf(lock, 'share/bundle-bytes').split('..')[1]), milliseconds: limits.wait }, signal, door.network),
|
|
78
|
+
packaged: async () => (await import(__rewriteRelativeImportExtension(new URL(`../ports/adapters/bundle${ext}`, import.meta.url).href))).publicAt(root, Number(valueOf(lock, 'share/bundle-bytes').split('..')[1])),
|
|
79
|
+
bundle: async (payload) => (await import(__rewriteRelativeImportExtension(new URL(`../ports/adapters/bundle${ext}`, import.meta.url).href))).bundleAt(root, lock, payload, signal),
|
|
73
80
|
bound: (cwd, args) => run(runs, cwd, args, {}, signal),
|
|
74
81
|
standing: (place) => once('standing', place, () => standingAt(runs, place, (lines) => ['head', 'person'].flatMap((role) => ((id) => (id === undefined ? [] : [id]))(idIn(lines, device(role)))), (line) => read(line.at), signal)),
|
|
82
|
+
authority: (place, key, kind, records, presented) => authorityAt(place, key, kind, records, Date.now(), signal, presented),
|
|
75
83
|
policy: (place) => once('policy', place, () => policyAt(runs, place, signal)),
|
|
76
84
|
world: (place, view) => once(`world/${view}`, place, () => settledView(runs, place, view, signal)),
|
|
77
85
|
adapter: async (origin) => ((name) => (!/^[a-z-]+$/.test(name) ? undefined : import(__rewriteRelativeImportExtension(new URL(`../ports/adapters/${name}${ext}`, import.meta.url).href))))(URL.canParse(origin) ? valueOf(lock, `adapter/${new URL(origin).protocol.slice(0, -1)}`) : ''),
|
|
@@ -79,8 +87,8 @@ export const headOf = (lock, root, ext, door) => {
|
|
|
79
87
|
home: () => home,
|
|
80
88
|
personal: () => process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '',
|
|
81
89
|
found: (place, account) => acted(place, true, founded(runs, place, ...asRoot(account), signal)),
|
|
82
|
-
signRoot: (place, lot, account) => sign(runs, place, lot, ...asRoot(account), signal),
|
|
83
|
-
landRoot: (place, lot, account) => acted(place, true, landAct(runs, place, lot, ...asRoot(account), signal)),
|
|
90
|
+
signRoot: (place, lot, account) => covered(place, lot, () => sign(runs, place, lot, ...asRoot(account), signal)),
|
|
91
|
+
landRoot: (place, lot, account) => acted(place, true, covered(place, lot, () => landAct(runs, place, lot, ...asRoot(account), signal))),
|
|
84
92
|
moved: (place) => moved.has(place),
|
|
85
93
|
settle: async () => { const red = []; for (const place of touched) {
|
|
86
94
|
const why = await closing(runs, place, signal);
|
package/dist/src/flows/answer.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { canonical } from '@lapxo/topos/wire';
|
|
2
2
|
import { valueOf } from "../core/lock.js";
|
|
3
|
+
import { fieldsOf, vocabOf } from "../core/read.js";
|
|
3
4
|
/**
|
|
4
5
|
* 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
6
|
* then taken away, and only the lines the world answers for this request come back — a value of each declared field, the
|
|
6
7
|
* counts, what it held back. A place that names no view to type it gets no answer at all, never the body.
|
|
7
8
|
*/
|
|
8
9
|
export const typedAnswer = async (context, place, lines, source, operation, key, body) => {
|
|
9
|
-
const
|
|
10
|
-
if (
|
|
11
|
-
return { untyped: '' };
|
|
10
|
+
const v = vocabOf(context.lock), models = fieldsOf(lines, `${v.offers}/${source}/${operation}/${v.form}`)['answer-view'] ?? [];
|
|
11
|
+
if (models.length > 1 || models.some(model => !/^[a-z][a-z0-9-]*$/.test(model)))
|
|
12
|
+
return { untyped: 'answer/model-ambiguous' };
|
|
13
|
+
const view = models[0] ?? valueOf(context.lock, 'place/answer');
|
|
14
|
+
if (!lines.some((line) => line.scope === `view/${view}` && line.role === 'demands'))
|
|
15
|
+
return { untyped: 'answer/model-not-admitted' };
|
|
12
16
|
const [request, family] = [key.slice(0, 16), valueOf(context.lock, 'vocab/answer')];
|
|
13
17
|
const at = `${valueOf(context.lock, 'place/answers')}/${source}/${encodeURIComponent(operation).replaceAll('.', '%2E')}/${request}.json`;
|
|
14
18
|
context.files.lay(place, at, body);
|
package/dist/src/flows/call.d.ts
CHANGED
|
@@ -38,4 +38,4 @@ export interface Frame {
|
|
|
38
38
|
readonly uncovered: (role: string, scopes: readonly string[]) => readonly string[] | undefined;
|
|
39
39
|
}
|
|
40
40
|
/** Prepare one call, authorize its access and pass its frame to the read or action flow. */
|
|
41
|
-
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[]>;
|
|
41
|
+
export declare const called: (asked: Asked, context: Context, planOf: (lines: readonly Said[], lock: readonly Said[], source: string, operation: string, place: string, explicitInvocation?: boolean) => Call | Plan, then: (frame: Frame) => Promise<readonly string[]>) => Promise<readonly string[]>;
|
package/dist/src/flows/call.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { configuredAt, configuredRefusal, configuredProtocol } from "../ports/adapters/configured.js";
|
|
2
|
+
import { receiverAllows } from "../core/receiver.js";
|
|
1
3
|
import { inPlace, placeNameOf, unless } from "../core/answer.js";
|
|
2
4
|
import { fieldsOf, vocabOf } from "../core/read.js";
|
|
3
5
|
import { canonical } from '@lapxo/topos/wire';
|
|
@@ -17,7 +19,7 @@ export const pickedOrigin = async (context, place, lines, source, plan) => {
|
|
|
17
19
|
const v = vocabOf(context.lock);
|
|
18
20
|
const origins = fieldsOf(lines, `${v.region}/${source}`)[v.origins] ?? [];
|
|
19
21
|
const line = (value) => ({ at: 'policy:person/picks', by: 'target', form: 'alphabet', measure: 'id', role: 'writes', scope: `${v.pick}/${source}`, value });
|
|
20
|
-
const by = widening(context, place, lines, origins.map((o) => canonical(line(o))));
|
|
22
|
+
const by = await widening(context, place, lines, origins.map((o) => canonical(line(o))));
|
|
21
23
|
if ('said' in by)
|
|
22
24
|
return { said: by.said };
|
|
23
25
|
const chosen = (await context.person.choose(sayIn(context.lock)('pick', { source }), [{ name: v.origin, options: origins.map((o) => ({ value: o, label: o })) }]))?.[v.origin];
|
|
@@ -38,8 +40,15 @@ export const called = async (asked, context, planOf, then) => {
|
|
|
38
40
|
if ('said' in named)
|
|
39
41
|
return named.said;
|
|
40
42
|
const operation = named.operation;
|
|
43
|
+
try {
|
|
44
|
+
if (!await receiverAllows(context, place, source, operation))
|
|
45
|
+
return say('bundle-not-selected', { place, source, operation });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return say('bundle-refused', { why: 'bundle/invalid' });
|
|
49
|
+
}
|
|
41
50
|
const widen = async (message, lot) => {
|
|
42
|
-
const by = widening(context, place, held.lines, lot);
|
|
51
|
+
const by = await widening(context, place, held.lines, lot);
|
|
43
52
|
if ('said' in by)
|
|
44
53
|
return { said: by.said };
|
|
45
54
|
const refused = await unless(context, message);
|
|
@@ -52,16 +61,31 @@ export const called = async (asked, context, planOf, then) => {
|
|
|
52
61
|
if ('said' in picked)
|
|
53
62
|
return picked.said;
|
|
54
63
|
const lines = picked.lines;
|
|
55
|
-
|
|
64
|
+
let remote;
|
|
65
|
+
try {
|
|
66
|
+
remote = configuredAt(context, place, source, lines);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return configuredRefusal(context, error);
|
|
70
|
+
}
|
|
71
|
+
const plan = planOf(lines, context.lock, source, operation, placeNameOf(place), remote !== undefined);
|
|
56
72
|
if (plan.kind === 'said')
|
|
57
73
|
return say(plan.key, plan.fields);
|
|
58
|
-
|
|
74
|
+
if (remote && asked['ahead'] === 'true')
|
|
75
|
+
return [];
|
|
76
|
+
if (remote && (!remote.offers.includes(operation) || new URL(remote.endpoint).origin !== new URL(plan.origin).origin))
|
|
77
|
+
return configuredRefusal(context, Error(configuredProtocol.toolFailure));
|
|
78
|
+
const args = JSON.parse(asked['args'] || '{}');
|
|
79
|
+
let request = '';
|
|
80
|
+
const prepared = remote ? (await context.remote?.())?.prepare(remote, operation, args, async () => await context.elicit(sayIn(context.lock)(configuredProtocol.invoke, { source, operation, endpoint: remote.endpoint, request })) === 'yes')
|
|
81
|
+
: ((transport) => transport?.prepare(plan, args, context.lock))(await context.adapter(plan.origin));
|
|
59
82
|
if (prepared === undefined)
|
|
60
83
|
return say('no-transport', { source, origin: plan.origin });
|
|
61
84
|
if ('missing' in prepared)
|
|
62
85
|
return say('missing-param', { operation, param: prepared.missing });
|
|
63
86
|
if ('refused' in prepared)
|
|
64
87
|
return say(prepared.refused, { source, origin: plan.origin });
|
|
88
|
+
request = prepared.key;
|
|
65
89
|
const most = (scope) => Number((lines.find((l) => l.scope === scope)?.value ?? '0..0').split('..')[1]);
|
|
66
90
|
const typed = async (status, bytes) => ((got, at) => ['lines' in got ? say('answered', { source, operation, status, at })[0] : say('untyped', { source, operation, status, at, why: got.untyped })[0], ...('lines' in got ? got.lines : [])])(await typedAnswer(context, place, lines, source, operation, prepared.key, bytes), new Date(context.now()).toISOString());
|
|
67
91
|
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));
|
|
@@ -2,12 +2,18 @@ import type { Context, Outcome } from '../core/answer.ts';
|
|
|
2
2
|
import type { Said } from '../core/lock.ts';
|
|
3
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
4
|
export declare const deviceKey: (context: Context, role?: string) => string | undefined;
|
|
5
|
-
/**
|
|
6
|
-
export declare const
|
|
5
|
+
/** Read the receiver index through its authenticated policy, or an empty index before its birth. */
|
|
6
|
+
export declare const homeIndex: (context: Context) => Promise<{
|
|
7
|
+
readonly lines: readonly Said[];
|
|
8
|
+
} | {
|
|
9
|
+
readonly refused: string;
|
|
10
|
+
}>;
|
|
11
|
+
/** Resolve the account through the authenticated index; a legacy folder name only locates a key whose public half must match the authenticated root. */
|
|
12
|
+
export declare const rootIn: (context: Context, place: string) => Promise<{
|
|
7
13
|
readonly account: string;
|
|
8
14
|
} | {
|
|
9
15
|
readonly said: readonly string[];
|
|
10
|
-
}
|
|
16
|
+
}>;
|
|
11
17
|
/**
|
|
12
18
|
* Who signs a widening of a place: its root, when this keychain holds it and the place admits this device's person to
|
|
13
19
|
* witness it — the line is then the place's own policy, in its lock, binding every device it admits, and names as its
|
|
@@ -17,10 +23,13 @@ export declare const rootIn: (context: Context, place: string) => {
|
|
|
17
23
|
* lock as its parent has nothing signed here: what the parent would cut is bound's to judge.
|
|
18
24
|
*/
|
|
19
25
|
type By = {
|
|
26
|
+
readonly key: string;
|
|
27
|
+
readonly root: boolean;
|
|
28
|
+
readonly records: (lot: readonly string[]) => readonly string[];
|
|
20
29
|
readonly sign: (lot: readonly string[]) => Promise<Outcome>;
|
|
21
30
|
readonly land: (lot: readonly string[]) => Promise<Outcome>;
|
|
22
31
|
};
|
|
23
|
-
export declare const widening: (context: Context, place: string, lines: readonly Said[], lot: readonly string[]) => By | {
|
|
32
|
+
export declare const widening: (context: Context, place: string, lines: readonly Said[], lot: readonly string[]) => Promise<By | {
|
|
24
33
|
readonly said: readonly string[];
|
|
25
|
-
}
|
|
34
|
+
}>;
|
|
26
35
|
export {};
|
package/dist/src/flows/device.js
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import { placeNameOf } from "../core/answer.js";
|
|
2
|
-
import { canonical, parse } from '@lapxo/topos/wire';
|
|
2
|
+
import { canonical, parse, LOCK } from '@lapxo/topos/wire';
|
|
3
3
|
import { deviceOf, idIn, uncoveredIn } from "../core/device.js";
|
|
4
4
|
import { members, valueOf } from "../core/lock.js";
|
|
5
5
|
import { fill, sayIn } from "../core/surface.js";
|
|
6
6
|
import { adoptsIn } from "../adopt/adopt.js";
|
|
7
7
|
/** A key of this device by its role: read from the keychain, or made there once; only its public half is ever handed on. */
|
|
8
8
|
export const deviceKey = (context, role) => context.keys.made?.(deviceOf(context.lock, role).account);
|
|
9
|
-
/**
|
|
10
|
-
export const
|
|
11
|
-
|
|
9
|
+
/** Read the receiver index through its authenticated policy, or an empty index before its birth. */
|
|
10
|
+
export const homeIndex = (context) => context.files.has(context.home(), LOCK) ? context.policy(context.home()) : Promise.resolve({ lines: [] });
|
|
11
|
+
/** Resolve the account through the authenticated index; a legacy folder name only locates a key whose public half must match the authenticated root. */
|
|
12
|
+
export const rootIn = async (context, place) => {
|
|
13
|
+
const home = context.home(), index = await homeIndex(context);
|
|
14
|
+
const names = 'refused' in index ? [] : [...new Set(index.lines.filter((l) => l.scope?.startsWith(`${valueOf(context.lock, 'home/family')}/`) && l.value === place).map((l) => l.scope.slice(valueOf(context.lock, 'home/family').length + 1)))];
|
|
15
|
+
const name = place === home ? valueOf(context.lock, 'home/name') : names[0] ?? placeNameOf(place), account = fill(valueOf(context.lock, 'founding/root-account'), { place: name });
|
|
16
|
+
const held = await context.policy(place), key = 'refused' in held ? undefined : held.lines.find((l) => l.scope === valueOf(context.lock, 'founding/root') && l.measure === 'public-key')?.value;
|
|
17
|
+
return 'refused' in index || names.length > 1 || key === undefined || context.keys.held?.(account) !== key ? { said: [sayIn(context.lock)('no-root', { place })] } : { account };
|
|
18
|
+
};
|
|
19
|
+
export const widening = async (context, place, lines, lot) => {
|
|
12
20
|
const parent = adoptsIn(context.lock, lines);
|
|
13
21
|
if (parent !== undefined)
|
|
14
22
|
return { said: [sayIn(context.lock)('parent-lock', { place, parent })] };
|
|
15
|
-
const [root, witness, field] = [rootIn(context, place), idIn(lines, context.device('person')), valueOf(context.lock, 'widening/witness')];
|
|
23
|
+
const [root, witness, field] = [await rootIn(context, place), idIn(lines, context.device('person')), valueOf(context.lock, 'widening/witness')];
|
|
16
24
|
const wire = lines.filter((l) => l.scope === 'wire/fields'), admits = wire.some((l) => members(l.value ?? '').includes(field));
|
|
17
25
|
const witnessed = (signed) => signed.map((line) => ((got) => (got.kind === 'fact' ? canonical({ ...got.value.fields, [field]: witness }) : line))(parse(line)));
|
|
18
26
|
if ('account' in root && witness !== undefined) {
|
|
19
27
|
if (admits || lot.every((line) => / scope=wire\/fields /.test(line)))
|
|
20
|
-
return { sign: (signed) => context.signRoot(place, admits ? witnessed(signed) : signed, root.account), land: (signed) => context.landRoot(place, admits ? witnessed(signed) : signed, root.account) };
|
|
28
|
+
return { key: valueOf(context.lock, 'founding/root').split('/')[1], root: true, records: (signed) => admits ? witnessed(signed) : signed, sign: (signed) => context.signRoot(place, admits ? witnessed(signed) : signed, root.account), land: (signed) => context.landRoot(place, admits ? witnessed(signed) : signed, root.account) };
|
|
21
29
|
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)))];
|
|
22
30
|
return { said: [sayIn(context.lock)('unwitnessed', { place }), ...wire.map((l) => canonical({ ...bare(l), by: 'target', value: 'withdraw' })),
|
|
23
31
|
canonical({ at: `policy:head/wire/${new Date(context.now()).toISOString()}`, by: 'target', form: 'alphabet', measure: 'id', role: 'reads', scope: 'wire/fields', value: named })] };
|
|
@@ -26,5 +34,5 @@ export const widening = (context, place, lines, lot) => {
|
|
|
26
34
|
if (stopped)
|
|
27
35
|
return { said: stopped };
|
|
28
36
|
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];
|
|
29
|
-
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
|
+
return again !== undefined ? { said: [sayIn(context.lock)('attested-again', { place, scope: again })] } : { key: witness, root: false, records: (signed) => signed, sign: (signed) => context.signAs(place, signed, 'person', lines), land: (signed) => context.landAs(place, signed, 'person', lines) };
|
|
30
38
|
};
|
package/dist/src/flows/plan.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Asked, Context } from '../core/answer.ts';
|
|
2
|
+
/** Bound observes exact configuration records; Q4 domain checks remain in Head's existing card. */
|
|
3
|
+
export declare const authorityFor: (context: Context, place: string, key: string, kind: string, records: readonly string[], observe?: boolean, presented?: string) => Promise<readonly string[] | undefined>;
|
|
2
4
|
/**
|
|
3
5
|
* A proposal the person signs: drafted lines shown to the person with the place and the base they are made against; on
|
|
4
6
|
* 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.
|
package/dist/src/flows/plan.js
CHANGED
|
@@ -3,6 +3,13 @@ import { sayIn } from "../core/surface.js";
|
|
|
3
3
|
import { widening } from "./device.js";
|
|
4
4
|
import { parse } from '@lapxo/topos/wire';
|
|
5
5
|
const moved = async (context, place, base) => ((now) => ('said' in now ? now.said : baseOf(now.lines) !== base ? [sayIn(context.lock)('base-moved', { base })] : undefined))(await standingOr(context, place));
|
|
6
|
+
/** Bound observes exact configuration records; Q4 domain checks remain in Head's existing card. */
|
|
7
|
+
export const authorityFor = async (context, place, key, kind, records, observe = false, presented) => {
|
|
8
|
+
const result = await context.authority(place, key, kind, records, presented), say = sayIn(context.lock);
|
|
9
|
+
if ('refused' in result)
|
|
10
|
+
return [say('authority-refused', { place, key, kind, why: result.refused, code: result.code })];
|
|
11
|
+
return observe || result.decision !== 'allow' ? [say(observe ? 'authority-card' : result.decision === 'deny' ? 'authority-refused' : 'authority-needed', { place, key, kind, decision: result.decision, authority: result.authority, request: result.request, needs: result.needs.join('|'), why: result.decision })] : undefined;
|
|
12
|
+
};
|
|
6
13
|
/**
|
|
7
14
|
* A proposal the person signs: drafted lines shown to the person with the place and the base they are made against; on
|
|
8
15
|
* 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.
|
|
@@ -16,9 +23,12 @@ export const planLot = (asked, context) => inPlace(asked, context, async (place)
|
|
|
16
23
|
if ('said' in held)
|
|
17
24
|
return held.said;
|
|
18
25
|
const base = baseOf(held.lines);
|
|
19
|
-
const by = widening(context, place, held.lines, lot);
|
|
26
|
+
const by = await widening(context, place, held.lines, lot);
|
|
20
27
|
if ('said' in by)
|
|
21
28
|
return by.said;
|
|
29
|
+
const initial = by.root ? await authorityFor(context, place, by.key, 'record-coverage', by.records(lot)) : undefined;
|
|
30
|
+
if (initial)
|
|
31
|
+
return initial;
|
|
22
32
|
const consent = await context.elicit(say('elicit', { count: String(lot.length), place, base, lines: lot.join('\n') }));
|
|
23
33
|
if (consent === 'none')
|
|
24
34
|
return [say('sign-yourself', { place }), ...lot];
|
|
@@ -30,7 +40,7 @@ export const planLot = (asked, context) => inPlace(asked, context, async (place)
|
|
|
30
40
|
const stale = await moved(context, place, base);
|
|
31
41
|
if (stale)
|
|
32
42
|
return stale;
|
|
33
|
-
const kept = context.cache.keep(Buffer.from(JSON.stringify({ place, base, signed: signed.lines })));
|
|
43
|
+
const kept = context.cache.keep(Buffer.from(JSON.stringify({ place, base, signed: signed.lines, root: by.root })));
|
|
34
44
|
return [say('planned', { lot: kept, base }), ...(await context.told(place, signed.lines))];
|
|
35
45
|
});
|
|
36
46
|
/** A planned lot landed by its digest, only while the place stands on the base it was planned against. */
|
|
@@ -43,6 +53,10 @@ export const landLot = async (asked, context) => {
|
|
|
43
53
|
const stale = await moved(context, kept.place, kept.base);
|
|
44
54
|
if (stale)
|
|
45
55
|
return stale;
|
|
56
|
+
const author = parse(kept.signed[0] ?? '');
|
|
57
|
+
const checked = kept.root ? await authorityFor(context, kept.place, author.kind === 'fact' ? author.value.fields.by ?? '' : '', 'admission', kept.signed) : undefined;
|
|
58
|
+
if (checked)
|
|
59
|
+
return checked;
|
|
46
60
|
const landed = await context.land(kept.place, kept.signed);
|
|
47
61
|
return 'refused' in landed ? [landed.refused] : [say('landed', { count: String(landed.lines.length) }), ...landed.lines];
|
|
48
62
|
};
|
package/dist/src/index.js
CHANGED
|
@@ -8,14 +8,9 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
return path;
|
|
9
9
|
};
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
12
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
-
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
14
11
|
import { aboutOf, members, valueOf } from "./core/lock.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { folderOf, lockAt } from "./ports/place.js";
|
|
18
|
-
import { askedOf, headOf } from "./doors/head.js";
|
|
12
|
+
import { lockAt } from "./ports/place.js";
|
|
13
|
+
import { serve } from "./ports/adapters/protocol.js";
|
|
19
14
|
const [ext, root] = ((self) => [self.slice(self.lastIndexOf('.')), fileURLToPath(new URL(self.endsWith('.ts') ? '..' : '../..', self))])(import.meta.url);
|
|
20
15
|
const lock = lockAt(root);
|
|
21
16
|
const [door, doors] = [process.argv[3], members(valueOf(lock, 'cli/run/doors'))];
|
|
@@ -27,27 +22,4 @@ if (door !== undefined && door !== doors[0]) {
|
|
|
27
22
|
await (await import(__rewriteRelativeImportExtension(new URL(`./doors/${door}${ext}`, import.meta.url).href))).open(lock, root, ext, door);
|
|
28
23
|
process.exit();
|
|
29
24
|
}
|
|
30
|
-
|
|
31
|
-
const head = headOf(lock, root, ext, {
|
|
32
|
-
can: () => ((e) => ({ url: Boolean(e?.url), form: e !== undefined && (e.form !== undefined || Object.keys(e).length === 0) }))(server.getClientCapabilities()?.elicitation), elicit: (params) => server.elicitInput(params, { timeout: Number(valueOf(lock, 'access/wait').split('..')[1]) }),
|
|
33
|
-
roots: async () => (server.getClientCapabilities()?.roots ? (await server.listRoots()).roots.flatMap((root) => folderOf(root.uri) ?? []) : []), client: () => ((c) => (c ? `${c.name}@${c.version}` : undefined))(server.getClientVersion()), name: doors[0],
|
|
34
|
-
}), surface = head.surface;
|
|
35
|
-
const server = new Server({ name: surface.name, version: surface.version }, { capabilities: { tools: offered(surface.tools.length), resources: offered(surface.resources.length), prompts: offered(surface.prompts.length) }, instructions: aboutOf(lock, 'prose/en/instructions') });
|
|
36
|
-
const schemaOf = (fields) => ({ type: 'object',
|
|
37
|
-
properties: Object.fromEntries(fields.map((f) => [f.name, { type: f.type, description: f.about }])), required: fields.filter((f) => f.required).map((f) => f.name) });
|
|
38
|
-
if (surface.tools.length)
|
|
39
|
-
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: surface.tools.map((tool) => ({ name: tool.name, description: tool.about,
|
|
40
|
-
inputSchema: schemaOf([...tool.fields]), outputSchema: shapeSchema(lock), annotations: Object.fromEntries(tool.hints.map((hint) => [`${hint}Hint`, true])) })) }));
|
|
41
|
-
if (surface.tools.length)
|
|
42
|
-
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => ((answer) => (answer === undefined ? { isError: true, content: [] } : { content: [{ type: 'text', text: answer.join('\n') }], structuredContent: shapeOf(answer) }))(await head.call(request.params.name, request.params.arguments, extra.signal)));
|
|
43
|
-
if (surface.resources.length)
|
|
44
|
-
server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
|
|
45
|
-
if (surface.resources.length)
|
|
46
|
-
server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({ resourceTemplates: surface.resources.map((r) => ({ name: r.name, uriTemplate: r.template, description: r.about, mimeType: r.mime })) }));
|
|
47
|
-
if (surface.resources.length)
|
|
48
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => ((got) => ({ contents: got === undefined ? [] : [{ uri: request.params.uri, mimeType: got.mime, text: got.lines.join('\n') }] }))(await head.resource(request.params.uri, extra.signal)));
|
|
49
|
-
if (surface.prompts.length)
|
|
50
|
-
server.setRequestHandler(ListPromptsRequestSchema, () => ({ prompts: surface.prompts.map((p) => ({ name: p.name, description: p.about, arguments: p.fields.map((f) => ({ name: f.name, description: f.about, required: f.required })) })) }));
|
|
51
|
-
if (surface.prompts.length)
|
|
52
|
-
server.setRequestHandler(GetPromptRequestSchema, (request) => ((p) => ({ messages: p === undefined ? [] : [{ role: 'user', content: { type: 'text', text: [p.text, p.line].filter(Boolean).map((t) => fill(t, askedOf(request.params.arguments))).join('\n') } }] }))(surface.prompts.find((one) => one.name === request.params.name)));
|
|
53
|
-
await server.connect(new StdioServerTransport());
|
|
25
|
+
await serve(lock, root, ext, doors[0]);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Asked, type Context } from '../core/answer.ts';
|
|
2
|
+
import { type Said } from '../core/lock.ts';
|
|
3
|
+
/** A separate public birth artifact. The source package, authority and personal birth journal are not its payload. */
|
|
4
|
+
export declare const sharedBirth: (context: Context, to: string, standing: readonly Said[]) => readonly string[];
|
|
5
|
+
/** Inline or client-shared local birth bytes are inspected only; no transport, signer, file write or admission is acquired. */
|
|
6
|
+
export declare const receivedBirth: (asked: Asked, context: Context) => Promise<readonly string[] | undefined>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { basename, join } from 'node:path';
|
|
2
|
+
import { canonical, parse } from '@lapxo/topos/wire';
|
|
3
|
+
import { PORTABLE_BIRTH_PROFILE, decodePortableBirth, encodePortableBirth, inspectPortableBirth } from '@lapxo/topos/contract';
|
|
4
|
+
import { semanticStandingBytes } from '@lapxo/topos/standing';
|
|
5
|
+
import { inStanding } from "../core/answer.js";
|
|
6
|
+
import { members, valueOf } from "../core/lock.js";
|
|
7
|
+
import { sayIn } from "../core/surface.js";
|
|
8
|
+
import { digestIn } from "../core/worlds.js";
|
|
9
|
+
const budget = (context) => ({ bytes: Number(valueOf(context.lock, 'bound/printed').split('..')[1]), items: Number(valueOf(context.lock, 'share/birth-items').split('..')[1]) });
|
|
10
|
+
const refused = (context, error) => [sayIn(context.lock)('birth-refused', { why: error instanceof Error ? error.message : String(error) })];
|
|
11
|
+
/** A separate public birth artifact. The source package, authority and personal birth journal are not its payload. */
|
|
12
|
+
export const sharedBirth = (context, to, standing) => {
|
|
13
|
+
const say = sayIn(context.lock), scopes = members(valueOf(context.lock, 'share/birth-scopes')), file = valueOf(context.lock, 'share/birth-file');
|
|
14
|
+
const declarations = standing.filter(line => scopes.includes(line.scope ?? '')).map(({ by: _by, sig: _sig, epoch: _epoch, ...line }) => canonical(line));
|
|
15
|
+
if (!declarations.length)
|
|
16
|
+
return [say('birth-empty')];
|
|
17
|
+
try {
|
|
18
|
+
const record = encodePortableBirth({ origin: digestIn(context.lock)(semanticStandingBytes(standing.map(line => canonical(line)))), declarations }, budget(context)), bytes = new TextEncoder().encode(record);
|
|
19
|
+
context.files.lay(to, file, bytes);
|
|
20
|
+
return [say('birth-shared', { document: join(to, file), digest: digestIn(context.lock)(bytes), record })];
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return refused(context, error);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** Inline or client-shared local birth bytes are inspected only; no transport, signer, file write or admission is acquired. */
|
|
27
|
+
export const receivedBirth = async (asked, context) => {
|
|
28
|
+
const document = asked['document'] ?? '', inline = document.startsWith(PORTABLE_BIRTH_PROFILE.split('/')[0] + '/');
|
|
29
|
+
if (!inline && !(document.startsWith('/') && basename(document) === valueOf(context.lock, 'share/birth-file')))
|
|
30
|
+
return undefined;
|
|
31
|
+
return inStanding(asked, context, async (place, standing) => {
|
|
32
|
+
const say = sayIn(context.lock), bytes = inline ? new TextEncoder().encode(document) : (await context.files.read.documentOf(context, place, document))?.bytes;
|
|
33
|
+
if (!bytes)
|
|
34
|
+
return [say('birth-needs', { document })];
|
|
35
|
+
if (asked['digest'] && digestIn(context.lock)(bytes) !== asked['digest'])
|
|
36
|
+
return [say('document-digest', { document: inline ? PORTABLE_BIRTH_PROFILE : document, digest: asked['digest'], got: digestIn(context.lock)(bytes) })];
|
|
37
|
+
try {
|
|
38
|
+
const pack = decodePortableBirth(new TextDecoder().decode(bytes), budget(context)), allowed = members(valueOf(context.lock, 'share/birth-scopes'));
|
|
39
|
+
if (pack.declarations.some(line => ((p) => p.kind !== 'fact' || !allowed.includes(p.value.fields.scope ?? ''))(parse(line))))
|
|
40
|
+
return [say('birth-refused', { why: 'declaration outside the public birth selection' })];
|
|
41
|
+
const inspected = inspectPortableBirth(pack, [...new Set(standing.flatMap(line => line.scope ?? []))], budget(context));
|
|
42
|
+
return [say('birth-inspected', { inspection: inspected.status, collisions: inspected.collisions.join(', ') || 'none', origin: pack.origin, declarations: pack.declarations.join('\n') })];
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return refused(context, error);
|
|
46
|
+
}
|
|
47
|
+
}, context.policy);
|
|
48
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Asked, type Context } from '../core/answer.ts';
|
|
2
2
|
/**
|
|
3
|
-
* A
|
|
3
|
+
* A legacy or located reference connected: inspected by the instrument in the person's place, never authenticated — its bytes are the
|
|
4
4
|
* link's, and nothing shows who shared it — and what it names is shown; nothing is connected or granted, since a package
|
|
5
5
|
* is received by the digest its sharer sent apart. A place that admits no content transport is handed the lines that admit
|
|
6
6
|
* one, to sign; bytes that do not arrive are owed by the link until they do; bytes that are not what it names are refused.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { decodeReference } from '@lapxo/bound/reference';
|
|
2
|
+
import { decodeLocatedReference, locatedReferenceIdentity } from '@lapxo/topos/contract';
|
|
2
3
|
import { canonical } from '@lapxo/topos/wire';
|
|
3
4
|
import { inStanding } from "../core/answer.js";
|
|
4
5
|
import { members, valueOf } from "../core/lock.js";
|
|
@@ -6,7 +7,7 @@ import { sayIn } from "../core/surface.js";
|
|
|
6
7
|
import { linesOf } from "../core/worlds.js";
|
|
7
8
|
import { rootIn } from "../flows/device.js";
|
|
8
9
|
/**
|
|
9
|
-
* A
|
|
10
|
+
* A legacy or located reference connected: inspected by the instrument in the person's place, never authenticated — its bytes are the
|
|
10
11
|
* link's, and nothing shows who shared it — and what it names is shown; nothing is connected or granted, since a package
|
|
11
12
|
* is received by the digest its sharer sent apart. A place that admits no content transport is handed the lines that admit
|
|
12
13
|
* one, to sign; bytes that do not arrive are owed by the link until they do; bytes that are not what it names are refused.
|
|
@@ -18,25 +19,35 @@ export const linked = (asked, context) => inStanding(asked, context, async (plac
|
|
|
18
19
|
return [...standing.filter((l) => admits.includes(l.scope ?? '') || l.scope?.startsWith('transport/')).map((l) => canonical(l)), say('why-transport', { place })];
|
|
19
20
|
const lines = context.lock.filter((l) => admits.some((s) => l.scope === `birth/${s}`)).map(({ about: _about, scope, ...l }) => canonical({ ...l, scope: scope.slice('birth/'.length), at: 'policy:person/transport', by: 'target' }));
|
|
20
21
|
let reference;
|
|
22
|
+
const legacy = link.startsWith(word('link/prefix'));
|
|
21
23
|
try {
|
|
22
|
-
reference = decodeReference(link);
|
|
24
|
+
reference = legacy ? (({ source, ...held }) => ({ ...held, location: source, 'location-class': 'holder' }))(decodeReference(link)) : decodeLocatedReference(link);
|
|
23
25
|
}
|
|
24
26
|
catch (refused) {
|
|
25
27
|
return [say('link-refused', { link, why: refused instanceof Error ? refused.message : String(refused) })];
|
|
26
28
|
}
|
|
27
|
-
|
|
29
|
+
const identity = legacy ? link : locatedReferenceIdentity(reference), source = reference.location;
|
|
30
|
+
if (!source || reference['location-class'] === 'viewer')
|
|
31
|
+
return [say('reference-needs', { identity, needs: 'bytes/holder-location' })];
|
|
32
|
+
if (![admits[0], `transport/${new URL(source).protocol.slice(0, -1)}`].every((s) => standing.some((l) => l.scope === s)))
|
|
28
33
|
return [say('transport-missing', { place, lines: lines.join('\n') })];
|
|
29
|
-
const [printed, scope] = [await context.bound(place, [word('bound/read'), link]), `${word('place/needs')}/bytes/${reference.blob.slice(reference.algorithm.length + 1, reference.algorithm.length + 17)}`];
|
|
30
|
-
const [owed,
|
|
31
|
-
if ('said' in root)
|
|
32
|
-
return root.said;
|
|
34
|
+
const [printed, scope] = [await context.bound(place, [word('bound/read'), link]), `${word('place/needs')}/bytes/${reference.blob.slice(reference.algorithm.length + 1, legacy ? reference.algorithm.length + 17 : undefined)}`];
|
|
35
|
+
const [owed, why, absent] = [standing.find((l) => l.scope === scope && l.role === 'demands' && l.value === identity), printed?.err.filter((l) => !l.startsWith('{')).at(-1) ?? '', printed?.out.some((line) => line.startsWith('NEEDS '))];
|
|
33
36
|
if (printed?.code !== 0 && !why.includes(word('link/unarrived')))
|
|
34
37
|
return [say('link-refused', { link, why })];
|
|
35
|
-
const settled =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
const settled = async (lot) => {
|
|
39
|
+
if (!lot.length)
|
|
40
|
+
return [];
|
|
41
|
+
const root = await rootIn(context, place);
|
|
42
|
+
if ('said' in root)
|
|
43
|
+
return root.said;
|
|
44
|
+
const got = await context.landRoot(place, lot, root.account);
|
|
45
|
+
return 'refused' in got ? [say('not-landed', { why: got.refused })] : got.lines;
|
|
46
|
+
};
|
|
47
|
+
const due = { at: 'policy:head/link', by: 'target', form: 'alphabet', measure: 'id', role: 'demands', scope, value: identity };
|
|
48
|
+
if (printed?.code !== 0 || absent)
|
|
49
|
+
return [say('link-owed', { place, source }), ...await settled(owed ? [] : [canonical(due)])];
|
|
39
50
|
const named = linesOf(new TextEncoder().encode(printed.out.filter((l) => l.startsWith('TOPOS ')).map((l) => l.slice('TOPOS '.length)).join('\n')));
|
|
40
51
|
const sources = named.filter((l) => /^sources\/[^/]+$/.test(l.scope ?? '')).map((l) => l.scope.split('/')[1]);
|
|
41
|
-
return [say('inspected', { source
|
|
52
|
+
return [say('inspected', { source, sources: sources.join(', ') || '—', digest: reference.digest }), say('sharer-unverified', {}), ...await settled(owed ? [canonical({ ...due, value: 'withdraw' })] : [])];
|
|
42
53
|
}, context.policy);
|
package/dist/src/links/link.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Context } from '../core/answer.ts';
|
|
2
2
|
/**
|
|
3
3
|
* A package's link: its lines as one standing document, laid in its folder as it is to be published — the link names that
|
|
4
|
-
* document's bytes, never the package's files
|
|
5
|
-
*
|
|
4
|
+
* document's bytes, never the package's files. The separate located profile keeps retrieval apart from content identity.
|
|
5
|
+
* Without a location the reference still names its content; it does not invent a holder or transfer authority.
|
|
6
6
|
*/
|
|
7
7
|
export declare const linkOf: (context: Context, to: string, lines: readonly string[], urls: Readonly<Record<string, string>>) => readonly string[];
|
package/dist/src/links/link.js
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
2
|
+
import { encodeLocatedReference } from '@lapxo/topos/contract';
|
|
4
3
|
import { semanticStandingBytes } from '@lapxo/topos/standing';
|
|
5
4
|
import { members, valueOf } from "../core/lock.js";
|
|
6
5
|
import { sayIn } from "../core/surface.js";
|
|
7
|
-
|
|
6
|
+
import { digestIn } from "../core/worlds.js";
|
|
8
7
|
/**
|
|
9
8
|
* A package's link: its lines as one standing document, laid in its folder as it is to be published — the link names that
|
|
10
|
-
* document's bytes, never the package's files
|
|
11
|
-
*
|
|
9
|
+
* document's bytes, never the package's files. The separate located profile keeps retrieval apart from content identity.
|
|
10
|
+
* Without a location the reference still names its content; it does not invent a holder or transfer authority.
|
|
12
11
|
*/
|
|
13
12
|
export const linkOf = (context, to, lines, urls) => {
|
|
14
|
-
const say = sayIn(context.lock), [name, profile, [kind, algorithm]] = [valueOf(context.lock, 'link/document'), valueOf(context.lock, 'link/profile'), members(valueOf(context.lock, 'link/reference'))];
|
|
13
|
+
const say = sayIn(context.lock), digest = digestIn(context.lock), [name, profile, [kind, algorithm]] = [valueOf(context.lock, 'link/document'), valueOf(context.lock, 'link/profile'), members(valueOf(context.lock, 'link/reference'))];
|
|
15
14
|
const standing = semanticStandingBytes(lines), bytes = new TextEncoder().encode(JSON.stringify({ profile, standing })), at = urls[name], document = join(to, name);
|
|
16
15
|
context.files.lay(to, name, bytes);
|
|
17
|
-
if (at === undefined || !URL.canParse(at))
|
|
18
|
-
return [say('shared-unlinked', { document })];
|
|
19
16
|
try {
|
|
20
|
-
return [say('shared-link', { document, at, link:
|
|
17
|
+
return [say(at === undefined ? 'shared-unlinked' : 'shared-link', { document, at: at ?? '', link: encodeLocatedReference({ class: kind, algorithm, digest: digest(standing).slice(7), blob: digest(bytes), ...(at === undefined ? {} : { location: new URL(at).href, 'location-class': 'holder' }) }) })];
|
|
21
18
|
}
|
|
22
19
|
catch (refused) {
|
|
23
|
-
return [say('link-refused', { link:
|
|
20
|
+
return [say('link-refused', { link: name, why: refused instanceof Error ? refused.message : String(refused) })];
|
|
24
21
|
}
|
|
25
22
|
};
|
|
@@ -22,7 +22,7 @@ export const firstRun = async (asked, context, then) => {
|
|
|
22
22
|
const refused = await unless(context, say('elicit-first-run', { dir, source, document, until }));
|
|
23
23
|
if (refused)
|
|
24
24
|
return refused;
|
|
25
|
-
const born = await bornAt(context, dir, name), root = rootIn(context, dir);
|
|
25
|
+
const born = await bornAt(context, dir, name), root = await rootIn(context, dir);
|
|
26
26
|
if (born[0] !== say('born', { name, dir }) || 'said' in root)
|
|
27
27
|
return 'said' in root ? [...born, ...root.said] : born;
|
|
28
28
|
const connected = await then(dir, true), unread = connected.some((l) => l.startsWith('said/head/document-unread ')), held = unread ? undefined : await context.policy(dir);
|
package/dist/src/onboard/owed.js
CHANGED
|
@@ -15,7 +15,7 @@ export const paidIn = async (asked, context, place) => {
|
|
|
15
15
|
const owed = 'said' in held ? undefined : held.lines.find((l) => l.scope === scope && l.role === 'demands' && l.value !== 'withdraw');
|
|
16
16
|
if (owed === undefined)
|
|
17
17
|
return { said: [], answer: true };
|
|
18
|
-
const document = owed.value ?? '', read = await context.files.read.documentOf(context, place, document), root = rootIn(context, place);
|
|
18
|
+
const document = owed.value ?? '', read = await context.files.read.documentOf(context, place, document), root = await rootIn(context, place);
|
|
19
19
|
if ('said' in root)
|
|
20
20
|
return { said: root.said, answer: false };
|
|
21
21
|
if (read === undefined)
|