@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
|
@@ -2,11 +2,11 @@ import type { Context, Outcome } from '../core/answer.ts';
|
|
|
2
2
|
import type { Said } from '../core/lock.ts';
|
|
3
3
|
import type { Line } from '../compile/pin.ts';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Only the requested worlds, or the default for an unspecified protocol, adopted once per place: its standing and blob pinned where they are published, for bound to
|
|
6
6
|
* fetch and verify, and each of its readers told where head lays what it reads — read off the standing head reads from
|
|
7
7
|
* the same address once per device, kept by its digest and answered only when it hashes to the pin. A world whose
|
|
8
8
|
* standing cannot be read so is no adoption.
|
|
9
9
|
*/
|
|
10
|
-
export declare const adopted: (context: Context, place: string, lands: (lot: readonly string[]) => Promise<Outcome>, line: Line) => Promise<Outcome>;
|
|
10
|
+
export declare const adopted: (context: Context, place: string, lands: (lot: readonly string[]) => Promise<Outcome>, line: Line, additional?: readonly string[]) => Promise<Outcome>;
|
|
11
11
|
/** The name of a lock the place pins as its parent, when it pins one: composing a parent's policy with a child's is bound's, so head reads nothing of such a place and signs nothing for it. */
|
|
12
12
|
export declare const adoptsIn: (lock: readonly Said[], lines: readonly Said[]) => string | undefined;
|
package/dist/src/adopt/adopt.js
CHANGED
|
@@ -9,27 +9,41 @@ import { linesOf } from "../core/worlds.js";
|
|
|
9
9
|
const readersOf = (lock, standing) => {
|
|
10
10
|
const region = valueOf(lock, 'vocab/region');
|
|
11
11
|
const reads = (view) => members(standing.find((l) => l.scope === `${region}/${valueOf(lock, view)}` && l.measure === 'reads')?.value ?? '');
|
|
12
|
-
const
|
|
12
|
+
const answer = valueOf(standing, 'capsule/answer-region');
|
|
13
|
+
const [connected, answered] = [reads('place/view'), [...reads('place/answer'), ...members(standing.find(l => l.scope === `${region}/${answer}` && l.measure === 'reads')?.value ?? '')]];
|
|
13
14
|
return standing.filter((l) => l.scope?.startsWith(`${region}/`) && l.measure === 'writes' && l.role === 'writes').flatMap((l) => ((coordinates) => (coordinates
|
|
14
|
-
? [{ scope: l.scope, measure: 'coordinates', value: coordinates }, { scope: l.scope, measure: 'receipts', value: l.value }] : []))(connected.includes(l.scope) ? valueOf(lock, 'place/connected/coordinates') : answered.includes(l.scope) ? valueOf(lock, 'place/answers/coordinates') : ''));
|
|
15
|
+
? [{ scope: l.scope, measure: 'coordinates', value: coordinates }, { scope: l.scope, measure: 'receipts', value: l.value }] : []))(connected.includes(l.scope) || standing.some(input => input.scope === l.scope && input.measure === 'reads' && input.role === 'receipt') && members(l.value ?? '').some(output => connected.includes(output)) ? valueOf(lock, 'place/connected/coordinates') : answered.includes(l.scope) || standing.some(input => input.scope === l.scope && input.measure === 'reads' && input.role === 'receipt') && members(l.value ?? '').some(output => answered.includes(output)) ? valueOf(lock, 'place/answers/coordinates') : ''));
|
|
15
16
|
};
|
|
16
17
|
/**
|
|
17
|
-
*
|
|
18
|
+
* Only the requested worlds, or the default for an unspecified protocol, adopted once per place: its standing and blob pinned where they are published, for bound to
|
|
18
19
|
* fetch and verify, and each of its readers told where head lays what it reads — read off the standing head reads from
|
|
19
20
|
* the same address once per device, kept by its digest and answered only when it hashes to the pin. A world whose
|
|
20
21
|
* standing cannot be read so is no adoption.
|
|
21
22
|
*/
|
|
22
|
-
export const adopted = async (context, place, lands, line) => {
|
|
23
|
+
export const adopted = async (context, place, lands, line, additional = []) => {
|
|
23
24
|
const [word, held] = [(k) => valueOf(context.lock, k), await context.policy(place)];
|
|
24
25
|
if ('refused' in held)
|
|
25
26
|
return held;
|
|
26
27
|
const own = held.lines, [wait, most] = ['adapter/request/wait', 'world/standing-bytes'].map((k) => Number(word(k).split('..')[1]));
|
|
27
|
-
const lot = [];
|
|
28
|
-
for (const w of
|
|
28
|
+
const lot = [], worlds = [...new Set(additional.length ? additional : members(word('world/adopts')))];
|
|
29
|
+
for (const w of worlds) {
|
|
30
|
+
const existing = own.filter(l => l.scope === `uses/${w}`);
|
|
31
|
+
if (existing.length > 1 || existing.some(l => l.value !== word(`world/${w}/standing`)))
|
|
32
|
+
return { refused: `REFUSE·world/pin-mismatch ${w}` };
|
|
33
|
+
}
|
|
34
|
+
for (const w of worlds.filter((w) => !own.some((l) => l.scope === `uses/${w}`))) {
|
|
29
35
|
const [standing, blob, at, blobAt] = [word(`world/${w}/standing`), word(`world/${w}/blob`), word(`sources/${w}`), word(`sources/${w}-blob`)];
|
|
30
36
|
const bytes = context.cache.held(standing) ?? ((got) => (got === undefined ? undefined : (context.cache.keep(got), got)))(await (await context.adapter(at))?.published(at, standing, most, wait));
|
|
31
37
|
if (bytes === undefined)
|
|
32
38
|
return { refused: `REFUSE·world ${w}: ${at} answered no bytes that hash to its pin ${standing}` };
|
|
39
|
+
const provider = linesOf(bytes), answers = provider.filter(l => l.scope === 'capsule/answer-region').map(l => l.value);
|
|
40
|
+
if (answers.length > 1 || answers.some(name => !/^[a-z][a-z0-9-]*$/.test(name)))
|
|
41
|
+
return { refused: 'REFUSE·world/answer-view' };
|
|
42
|
+
const answer = answers[0], selected = answer && word('birth/view/answer').replace(/^[^@]+/, answer);
|
|
43
|
+
if (answer && own.some(l => l.scope === `view/${answer}` && (l.value !== selected || l.role !== 'demands')))
|
|
44
|
+
return { refused: 'REFUSE·world/answer-view' };
|
|
45
|
+
if (answer && !own.some(l => l.scope === `view/${answer}`))
|
|
46
|
+
lot.push(line(`view/${answer}`, 'id', selected, { role: 'demands' }));
|
|
33
47
|
const laid = `${word('place/worlds')}/${w}${EXTENSION}`;
|
|
34
48
|
context.files.lay(place, laid, bytes);
|
|
35
49
|
lot.push(line(`uses/${w}`, 'digest', standing, { needs: laid }), line(`sources/${w}`, 'id', at), line(`dep/${w}-blob`, 'digest', blob, { shape: word('world/shape') }), line(`sources/${w}-blob`, 'id', blobAt), ...readersOf(context.lock, linesOf(bytes)).map((r) => line(r.scope, r.measure, r.value, { role: 'reads' })));
|
package/dist/src/ahead/read.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Cell } from '../ports/surface.ts';
|
|
|
4
4
|
* What an answer leaves to do before the next call, done after it is answered; another call coming gives up what is not
|
|
5
5
|
* begun, never what is, so no land is cut halfway:
|
|
6
6
|
* the reads a known next asks for, read in a context that asks the person nothing — a read only, never an act, nothing it
|
|
7
|
-
* would need a yes or a secret for
|
|
8
|
-
*
|
|
7
|
+
* would need a yes or a secret for. Each question obtains a fresh native snapshot; speculative recapture cannot validate it.
|
|
8
|
+
* Reading ahead spends only the source-call budget the place declares.
|
|
9
9
|
*/
|
|
10
10
|
export declare const readAhead: (cells: readonly Cell[], context: Context, signal: AbortSignal) => Promise<void>;
|
package/dist/src/ahead/read.js
CHANGED
|
@@ -4,8 +4,8 @@ const none = async () => 'none', nothing = async () => undefined;
|
|
|
4
4
|
* What an answer leaves to do before the next call, done after it is answered; another call coming gives up what is not
|
|
5
5
|
* begun, never what is, so no land is cut halfway:
|
|
6
6
|
* the reads a known next asks for, read in a context that asks the person nothing — a read only, never an act, nothing it
|
|
7
|
-
* would need a yes or a secret for
|
|
8
|
-
*
|
|
7
|
+
* would need a yes or a secret for. Each question obtains a fresh native snapshot; speculative recapture cannot validate it.
|
|
8
|
+
* Reading ahead spends only the source-call budget the place declares.
|
|
9
9
|
*/
|
|
10
10
|
export const readAhead = async (cells, context, signal) => {
|
|
11
11
|
const quiet = { ...context, elicit: none, person: { consent: none, enter: nothing, choose: nothing, visit: nothing } };
|
|
@@ -13,6 +13,4 @@ export const readAhead = async (cells, context, signal) => {
|
|
|
13
13
|
if (!signal.aborted && 'tool' in next && next.tool === 'ask' && next.args['source'] && next.args['operation'] && !next.args['why'] && !next.args['history'])
|
|
14
14
|
await readSource({ ...next.args, ahead: 'true' }, quiet);
|
|
15
15
|
await quiet.settle();
|
|
16
|
-
for (const place of new Set(cells.flatMap((c) => [c.fields['place'] ?? '', 'tool' in c.next ? c.next.args['place'] ?? '' : '']).filter((p) => p && !signal.aborted)))
|
|
17
|
-
await Promise.all([context.standing(place), context.policy(place)]).catch(() => undefined);
|
|
18
16
|
};
|
|
@@ -6,7 +6,7 @@ const nothing = (bytes) => ((text) => text === '' || /^\[\s*\]$/.test(text))(new
|
|
|
6
6
|
/** Distinguish an empty response from missing transport bytes; only the latter leaves a named need. */
|
|
7
7
|
export const answering = (context, place, asked, frame) => {
|
|
8
8
|
const need = `${valueOf(context.lock, 'place/needs')}/bytes/${frame.source}/${frame.operation}`, held = frame.lines.find((l) => l.scope === need && l.role === 'demands' && l.value !== 'withdraw');
|
|
9
|
-
const owe = (line) => ((root) => ('said' in root ? undefined : context.landRoot(place, [line], root.account)))(rootIn(context, place));
|
|
9
|
+
const owe = async (line) => ((root) => ('said' in root ? undefined : context.landRoot(place, [line], root.account)))(await rootIn(context, place));
|
|
10
10
|
const withdrawn = ({ about: _about, sig: _sig, epoch: _epoch, ...l }) => canonical({ ...l, by: 'target', value: 'withdraw' });
|
|
11
11
|
return { ...frame,
|
|
12
12
|
typed: (status, bytes) => (nothing(bytes) ? Promise.resolve(frame.say('answered-nothing', { source: frame.source, operation: frame.operation, status, at: new Date(context.now()).toISOString() })) : frame.typed(status, bytes)),
|
|
@@ -21,7 +21,7 @@ export declare const pinnedAs: (context: Context, place: string, file: string, l
|
|
|
21
21
|
* — so the same input finds its compiled standing by name, and pinned from the place's store: a new pin lands with the old
|
|
22
22
|
* one's withdraw, said at its own time, since a withdrawn saying stays withdrawn.
|
|
23
23
|
*/
|
|
24
|
-
export declare const pinIn: (context: Context, place: string, lands: (lot: readonly string[]) => Promise<Outcome>, line: Line) => (source: string, read: readonly string[]) => Promise<Pinned>;
|
|
24
|
+
export declare const pinIn: (context: Context, place: string, lands: (lot: readonly string[]) => Promise<Outcome>, line: Line, validate?: (lines: readonly Said[]) => void) => (source: string, read: readonly string[]) => Promise<Pinned>;
|
|
25
25
|
/**
|
|
26
26
|
* The demands a document laid in a place pays: every other pin whose compile named a reference to it unresolved, as it
|
|
27
27
|
* was not there yet, recompiled now that it is — the input its old pin read and the place's pins as they now stand — and
|
package/dist/src/compile/pin.js
CHANGED
|
@@ -4,14 +4,14 @@ import { members, under, valueOf } from "../core/lock.js";
|
|
|
4
4
|
import { fieldsOf, vocabOf } from "../core/read.js";
|
|
5
5
|
import { sayIn } from "../core/surface.js";
|
|
6
6
|
import { digestIn, linesOf, pinnedOf } from "../core/worlds.js";
|
|
7
|
-
/** Pin
|
|
7
|
+
/** Pin source declarations and read/write bounds; host output-presence facts are not source policy. */
|
|
8
8
|
const compiledOf = async (context, place, source, file) => {
|
|
9
9
|
const say = sayIn(context.lock), v = vocabOf(context.lock);
|
|
10
10
|
const compiled = await context.world(place, valueOf(context.lock, 'place/view'));
|
|
11
11
|
if ('refused' in compiled)
|
|
12
12
|
return { said: [say('unsettled', { source, why: compiled.refused })] };
|
|
13
13
|
const issued = (fieldsOf(compiled.lines, `${v.region}/${source}`)[v.issuers] ?? []).map((issuer) => `${v.keys}/${issuer}`);
|
|
14
|
-
const mine = compiled.lines.filter((l) => l.scope === `${v.region}/${source}` || l.scope?.startsWith(`${v.keys}/${source}/`) || l.scope?.startsWith(`${v.offers}/${source}/`) || /^(read|write)\//.test(l.scope ?? '')
|
|
14
|
+
const mine = compiled.lines.filter((l) => l.scope === `${v.region}/${source}` || l.scope?.startsWith(`${v.keys}/${source}/`) || l.scope?.startsWith(`${v.offers}/${source}/`) || (l.form === 'interval' && /^(read|write)\//.test(l.scope ?? ''))
|
|
15
15
|
|| issued.some((at) => l.scope === at || l.scope?.startsWith(`${at}/`)));
|
|
16
16
|
if (!mine.some((l) => !/^(read|write)\//.test(l.scope ?? '')))
|
|
17
17
|
return { said: [], open: true };
|
|
@@ -36,7 +36,7 @@ export const pinnedAs = async (context, place, file, lines) => {
|
|
|
36
36
|
* — so the same input finds its compiled standing by name, and pinned from the place's store: a new pin lands with the old
|
|
37
37
|
* one's withdraw, said at its own time, since a withdrawn saying stays withdrawn.
|
|
38
38
|
*/
|
|
39
|
-
export const pinIn = (context, place, lands, line) => async (source, read) => {
|
|
39
|
+
export const pinIn = (context, place, lands, line, validate) => async (source, read) => {
|
|
40
40
|
const [say, digest, held] = [sayIn(context.lock), digestIn(context.lock), await context.policy(place)];
|
|
41
41
|
if ('refused' in held)
|
|
42
42
|
return { said: [say('unsettled', { source, why: held.refused })] };
|
|
@@ -45,6 +45,8 @@ export const pinIn = (context, place, lands, line) => async (source, read) => {
|
|
|
45
45
|
const file = `${members(valueOf(context.lock, 'place/compiled'))[0]}/${source}/${input.split(':')[1].slice(0, 16)}${EXTENSION}`;
|
|
46
46
|
const old = own.find((l) => l.scope === `uses/${source}`);
|
|
47
47
|
const pinned = ((kept) => (kept !== undefined && kept.pin === old?.value ? kept : undefined))(pinnedOf(context.lock, old?.needs === file ? context.files.take(join(place, file)) : undefined)) ?? await compiledOf(context, place, source, file);
|
|
48
|
+
if (!('said' in pinned))
|
|
49
|
+
validate?.(pinned.lines);
|
|
48
50
|
if ('said' in pinned || (old?.needs === file && old.value === pinned.pin))
|
|
49
51
|
return pinned;
|
|
50
52
|
const withdraw = old === undefined ? [] : [canonical({ ...Object.fromEntries(Object.entries(old).filter(([k]) => !['sig', 'epoch'].includes(k))), by: 'target', value: 'withdraw' })];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Said } from './lock.ts';
|
|
2
|
+
import type { authorityAt } from '../ports/host.ts';
|
|
2
3
|
import type { Printed } from '../ports/bound.ts';
|
|
3
4
|
export type Outcome = {
|
|
4
5
|
readonly lines: readonly string[];
|
|
@@ -12,6 +13,16 @@ import type { Call } from './read.ts';
|
|
|
12
13
|
import type { AccessAdapter, Answered, Attachment, Choice, Field, Held } from '../access/contract.ts';
|
|
13
14
|
export type Asked = Readonly<Record<string, string | undefined>>;
|
|
14
15
|
export interface Context {
|
|
16
|
+
readonly remote?: () => Promise<import('../ports/adapters/remote.ts').RemotePort>;
|
|
17
|
+
readonly packaged?: () => Promise<{
|
|
18
|
+
readonly root: string;
|
|
19
|
+
readonly read: (path: string) => Uint8Array | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
readonly bundle?: (payload: import('../ports/adapters/bundle.ts').Payload | undefined) => Promise<{
|
|
22
|
+
readonly bytes: Uint8Array;
|
|
23
|
+
readonly digest: string;
|
|
24
|
+
readonly sources: readonly string[];
|
|
25
|
+
}>;
|
|
15
26
|
readonly lock: readonly Said[];
|
|
16
27
|
readonly bound: (cwd: string, args: readonly string[]) => Promise<Printed | undefined>;
|
|
17
28
|
readonly roots: () => Promise<readonly string[]>;
|
|
@@ -23,6 +34,7 @@ export interface Context {
|
|
|
23
34
|
} | {
|
|
24
35
|
readonly refused: string;
|
|
25
36
|
}>;
|
|
37
|
+
readonly authority: (place: string, key: string, kind: string, records: readonly string[], presented?: string) => ReturnType<typeof authorityAt>;
|
|
26
38
|
readonly policy: (place: string) => Promise<{
|
|
27
39
|
readonly lines: readonly Said[];
|
|
28
40
|
} | {
|
package/dist/src/core/answer.js
CHANGED
|
@@ -13,7 +13,7 @@ import { basename, isAbsolute, resolve, sep } from 'node:path';
|
|
|
13
13
|
import { sayIn } from "./surface.js";
|
|
14
14
|
/** What answers a line of the surface: the module named as the line, in the folder of its family. */
|
|
15
15
|
export const answerOf = async (family, name) => (await import(__rewriteRelativeImportExtension(new URL(`../${family}/${name}${import.meta.url.slice(import.meta.url.lastIndexOf('.'))}`, import.meta.url).href))).answer;
|
|
16
|
-
export const placeOf = async (asked, context) => asked['place'] || (await context.roots())
|
|
16
|
+
export const placeOf = async (asked, context) => asked['place'] || (await context.roots()).find((root) => context.files.has(root, LOCK) || context.files.has(root, '.bound')) || ((held) => ('refused' in held ? undefined
|
|
17
17
|
: ((named) => (named.length === 1 ? named[0] : undefined))(held.lines.filter((l) => l.scope?.startsWith(`${valueOf(context.lock, 'home/family')}/`) && l.value !== 'withdraw').map((l) => l.value))))(context.files.has(context.home(), LOCK) ? await context.policy(context.home()) : { refused: '' });
|
|
18
18
|
export const inPlace = async (asked, context, then) => ((place) => (place === undefined ? [sayIn(context.lock)('no-place')] : then(place)))(await placeOf(asked, context));
|
|
19
19
|
/** The person's yes to exactly what a message says; anything else is what head says of it, and nothing is done. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Said } from './lock.ts';
|
|
2
|
+
export interface PackageIdentity {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly revision: string;
|
|
5
|
+
readonly digest: string;
|
|
6
|
+
}
|
|
7
|
+
/** Receiver-local records only: no grants, source pins or sender keys. Use the existing wire fields and forms. */
|
|
8
|
+
export declare function associationFor(prefix: string, incoming: PackageIdentity, authenticated: readonly Said[]): {
|
|
9
|
+
readonly state: "collision" | "new" | "same" | "revision";
|
|
10
|
+
readonly previous: Readonly<Record<string, string>>[];
|
|
11
|
+
readonly incoming: PackageIdentity;
|
|
12
|
+
readonly scope: string;
|
|
13
|
+
readonly place: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
/** Ordinary recipient import must succeed before approval. Bound admits all exact withdrawals and records in one lot. */
|
|
16
|
+
export declare function associationLines(plan: ReturnType<typeof associationFor>, place: string, approved: boolean, act: string): readonly string[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { canonical } from '@lapxo/topos/wire';
|
|
2
|
+
/** Receiver-local records only: no grants, source pins or sender keys. Use the existing wire fields and forms. */
|
|
3
|
+
export function associationFor(prefix, incoming, authenticated) {
|
|
4
|
+
if (!/^[a-z][a-z0-9/-]*$/.test(prefix) || !/^[a-z][a-z0-9-]*$/.test(incoming.id) || !/^\d+\.\d+\.\d+$/.test(incoming.revision) || !/^sha256:[a-f0-9]{64}$/.test(incoming.digest))
|
|
5
|
+
throw Error('package/identity');
|
|
6
|
+
const scope = `${prefix}/${incoming.id}`, names = [scope, `${scope}/revision`, `${scope}/place`];
|
|
7
|
+
const previous = authenticated.filter(line => names.includes(line.scope ?? '') && line.value !== 'withdraw');
|
|
8
|
+
if (names.some(name => previous.filter(line => line.scope === name).length > 1))
|
|
9
|
+
throw Error('package/association-ambiguous');
|
|
10
|
+
if (previous.length && previous.length !== 3)
|
|
11
|
+
throw Error('package/association-incomplete');
|
|
12
|
+
const held = (at) => previous.find(line => line.scope === at)?.value;
|
|
13
|
+
const state = !previous.length ? 'new' : held(scope) === incoming.digest && held(`${scope}/revision`) === incoming.revision ? 'same'
|
|
14
|
+
: held(`${scope}/revision`) === incoming.revision ? 'collision' : 'revision';
|
|
15
|
+
return { state, previous, incoming, scope, place: held(`${scope}/place`) };
|
|
16
|
+
}
|
|
17
|
+
/** Ordinary recipient import must succeed before approval. Bound admits all exact withdrawals and records in one lot. */
|
|
18
|
+
export function associationLines(plan, place, approved, act) {
|
|
19
|
+
if (!approved)
|
|
20
|
+
return [];
|
|
21
|
+
if (plan.state === 'collision' || !place || !/^policy:[A-Za-z0-9/._:-]+$/.test(act))
|
|
22
|
+
throw Error('package/association-collision');
|
|
23
|
+
if (plan.state === 'same' && plan.place === place)
|
|
24
|
+
return [];
|
|
25
|
+
const base = { at: act, by: 'target', role: 'writes', form: 'alphabet' };
|
|
26
|
+
return [[plan.scope, 'digest', plan.incoming.digest], [`${plan.scope}/revision`, 'id', plan.incoming.revision], [`${plan.scope}/place`, 'id', place]].flatMap(([scope, measure, value]) => {
|
|
27
|
+
const old = plan.previous.find(line => line.scope === scope);
|
|
28
|
+
if (old?.measure === measure && old.value === value)
|
|
29
|
+
return [];
|
|
30
|
+
if (old?.at === act)
|
|
31
|
+
throw Error('package/association-stale-act');
|
|
32
|
+
return [...(old ? [canonical({ ...Object.fromEntries(Object.entries(old).filter(([key]) => !['sig', 'epoch'].includes(key))), by: 'target', value: 'withdraw' })] : []), canonical({ ...base, scope, measure, value })];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type Source = {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly kind: string;
|
|
4
|
+
readonly document: string;
|
|
5
|
+
readonly pin: string;
|
|
6
|
+
readonly offers: readonly string[];
|
|
7
|
+
};
|
|
8
|
+
export type Selection = {
|
|
9
|
+
readonly source: string;
|
|
10
|
+
readonly namespace: string;
|
|
11
|
+
readonly offers: readonly string[];
|
|
12
|
+
};
|
|
13
|
+
export type Recipe = {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly revision: string;
|
|
16
|
+
readonly title: string;
|
|
17
|
+
readonly description: string;
|
|
18
|
+
readonly sources: readonly Selection[];
|
|
19
|
+
};
|
|
20
|
+
/** A selection of already interpreted, authenticated sources. It neither interprets a document nor grants an action. */
|
|
21
|
+
export declare function compose(recipe: Recipe, sources: readonly Source[], most: number): {
|
|
22
|
+
bytes: Buffer<ArrayBuffer>;
|
|
23
|
+
digest: string;
|
|
24
|
+
entries: {
|
|
25
|
+
source: string;
|
|
26
|
+
namespace: string;
|
|
27
|
+
kind: string;
|
|
28
|
+
document: string;
|
|
29
|
+
pin: string;
|
|
30
|
+
offers: string[];
|
|
31
|
+
}[];
|
|
32
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const name = /^[a-z][a-z0-9-]*$/;
|
|
3
|
+
const digest = /^sha256:[a-f0-9]{64}$/;
|
|
4
|
+
/** A selection of already interpreted, authenticated sources. It neither interprets a document nor grants an action. */
|
|
5
|
+
export function compose(recipe, sources, most) {
|
|
6
|
+
if (!Number.isSafeInteger(most) || most < 1 || !name.test(recipe.id) || !/^\d+\.\d+\.\d+$/.test(recipe.revision) || !recipe.title.trim() || !recipe.description.trim()
|
|
7
|
+
|| recipe.sources.length < 1 || recipe.sources.length > most)
|
|
8
|
+
throw Error('composition/recipe');
|
|
9
|
+
const namespaces = new Set(), selected = new Set();
|
|
10
|
+
const entries = recipe.sources.map(choice => {
|
|
11
|
+
if (!name.test(choice.namespace) || namespaces.has(choice.namespace) || selected.has(choice.source))
|
|
12
|
+
throw Error('composition/collision');
|
|
13
|
+
namespaces.add(choice.namespace);
|
|
14
|
+
selected.add(choice.source);
|
|
15
|
+
const found = sources.filter(source => source.name === choice.source);
|
|
16
|
+
if (found.length !== 1)
|
|
17
|
+
throw Error('composition/source');
|
|
18
|
+
const source = found[0];
|
|
19
|
+
if (!digest.test(source.pin) || !digest.test(source.document) || !source.kind.trim())
|
|
20
|
+
throw Error('composition/pin');
|
|
21
|
+
if (!choice.offers.length || choice.offers.length > most || new Set(choice.offers).size !== choice.offers.length
|
|
22
|
+
|| choice.offers.some(offer => !source.offers.includes(offer)))
|
|
23
|
+
throw Error('composition/offer');
|
|
24
|
+
return { source: source.name, namespace: choice.namespace, kind: source.kind, document: source.document, pin: source.pin, offers: [...choice.offers].sort() };
|
|
25
|
+
}).sort((a, b) => a.namespace < b.namespace ? -1 : a.namespace > b.namespace ? 1 : 0);
|
|
26
|
+
const bytes = Buffer.from(JSON.stringify({ protocol: 'head-composition@1', id: recipe.id, revision: recipe.revision, title: recipe.title, description: recipe.description, sources: entries }) + '\n');
|
|
27
|
+
return { bytes, digest: `sha256:${createHash('sha256').update(bytes).digest('hex')}`, entries };
|
|
28
|
+
}
|
|
@@ -2,6 +2,7 @@ import { type Asked, type Context } from './answer.ts';
|
|
|
2
2
|
import { type Said } from './lock.ts';
|
|
3
3
|
/** A line no package carries: one that is not a world, or a source's document, overlay or pin, said once by digest — with, beside a digest that names bytes, the encrypted address they are read at. */
|
|
4
4
|
export declare const foreignIn: (lines: readonly Said[]) => Said | undefined;
|
|
5
|
+
export declare const sharePath: (context: Context, place: string, to: string) => Promise<boolean>;
|
|
5
6
|
/** A place packed as its published lock would name it, into a folder inside it or one the client shares, apart from what head and bound keep: each world its sources were compiled under, and each source — document, overlay, and compiled pin resting on the sources whose issuers it names — by digest, beside the bytes those digests name, and where the person says they are also published, at that encrypted address. No line of access, key or grant: a package grants nothing, and nothing is asked, signed or landed. */
|
|
6
7
|
export declare const shared: (asked: Asked, context: Context) => Promise<readonly string[]>;
|
|
7
8
|
/** What a package names, checked whole before anything is asked: every line a world or a source's document, overlay or pin, by digest; each world the one this head pins; an order its pins rest in. A source whose bytes did not arrive, or did not hash to their line, is open, and so is every source resting on it: absence is not a refusal, and the rest arrives. */
|
package/dist/src/core/package.js
CHANGED
|
@@ -5,10 +5,13 @@ import { members, under, valueOf } from "./lock.js";
|
|
|
5
5
|
import { fieldsOf, vocabOf } from "./read.js";
|
|
6
6
|
import { sayIn } from "./surface.js";
|
|
7
7
|
import { digestIn, linesOf } from "./worlds.js";
|
|
8
|
+
import { sharedBirth } from "../links/birth.js";
|
|
9
|
+
import { shareBundle } from "./share-bundle.js";
|
|
8
10
|
import { linkOf } from "../links/link.js";
|
|
9
11
|
/** A line no package carries: one that is not a world, or a source's document, overlay or pin, said once by digest — with, beside a digest that names bytes, the encrypted address they are read at. */
|
|
10
12
|
export const foreignIn = (lines) => lines.find((l) => !/^(sources\/[a-z0-9-]+(\/overlay)?|uses\/[a-z0-9-]+)$/.test(l.scope ?? '') || lines.some((o) => o !== l && o.scope === l.scope && o.measure === l.measure)
|
|
11
13
|
|| (l.measure !== 'digest' && !(l.measure === 'id' && URL.canParse(l.value ?? '') && lines.some((o) => o.scope === l.scope && o.measure === 'digest' && o.needs))));
|
|
14
|
+
export const sharePath = async (context, place, to) => await within(context, place, to) && ((rel) => rel.startsWith('..') || (rel !== '' && !rel.startsWith('.') && ![valueOf(context.lock, 'place/connected'), ...members(valueOf(context.lock, 'place/compiled')), valueOf(context.lock, 'walk/context/file'), valueOf(context.lock, 'place/worlds')].some(at => rel.split(sep)[0] === at.split('/')[0])))(relative(place, to));
|
|
12
15
|
const orderOf = (all, out = []) => ((next) => (next === undefined ? (out.length === all.length ? out : undefined) : orderOf(all, [...out, next])))(all.find((s) => !out.includes(s) && s.rests.every((r) => out.some((o) => o.source === r))));
|
|
13
16
|
const packed = (context, place, own, urls) => {
|
|
14
17
|
const [word, v, digest, held] = [(k) => valueOf(context.lock, k), vocabOf(context.lock), digestIn(context.lock), (rel) => context.files.take(join(place, rel)) ?? new Uint8Array()];
|
|
@@ -17,14 +20,23 @@ const packed = (context, place, own, urls) => {
|
|
|
17
20
|
const pins = own.filter((l) => l.scope?.startsWith('uses/') && under(context.lock, 'place/compiled', l.needs)).map((l) => ((source) => ({ source, pin: l.value, needs: l.needs, lines: linesOf(held(l.needs)), laid: context.files.read.laidOf(context, place, source) }))(l.scope.slice('uses/'.length)));
|
|
18
21
|
const declares = new Map(pins.flatMap((p) => (fieldsOf(p.lines, `${v.region}/${p.source}`)[v.issuers] ?? []).map((issuer) => [issuer, p.source])));
|
|
19
22
|
const rested = pins.map((p) => ({ ...p, rests: [...new Set(p.lines.filter((l) => l.measure === 'issuer').flatMap((l) => declares.get(l.value ?? '') ?? []))].filter((s) => s !== p.source).sort() })), sources = orderOf(rested) ?? rested;
|
|
20
|
-
return { names: sources.map((s) => s.source), files: sources.flatMap((s) => [...s.laid, [s.needs, held(s.needs)]]), lines: [...own.filter((l) =>
|
|
23
|
+
return { names: sources.map((s) => s.source), files: sources.flatMap((s) => [...s.laid, [s.needs, held(s.needs)]]), lines: [...own.filter((l) => l.scope?.startsWith('uses/') && l.value === word(`world/${l.scope.slice(5)}/standing`)).flatMap((l) => row(l.scope, l.value)),
|
|
21
24
|
...sources.flatMap((s) => [...s.laid.flatMap(([rel, bytes]) => row(/\.overlay\.[^.]+$/.test(rel) ? `sources/${s.source}/overlay` : `sources/${s.source}`, digest(bytes), { needs: rel })),
|
|
22
25
|
...row(`uses/${s.source}`, s.pin, { needs: s.needs, ...(s.rests.length ? { restsOn: s.rests.map((r) => `uses/${r}`).join('|') } : {}) })])] };
|
|
23
26
|
};
|
|
24
27
|
/** A place packed as its published lock would name it, into a folder inside it or one the client shares, apart from what head and bound keep: each world its sources were compiled under, and each source — document, overlay, and compiled pin resting on the sources whose issuers it names — by digest, beside the bytes those digests name, and where the person says they are also published, at that encrypted address. No line of access, key or grant: a package grants nothing, and nothing is asked, signed or landed. */
|
|
25
28
|
export const shared = (asked, context) => inPlace(asked, context, async (place) => {
|
|
26
|
-
const
|
|
27
|
-
|
|
29
|
+
const bundled = await shareBundle({ ...asked, place }, context, async () => {
|
|
30
|
+
const own = await standingOr(context, place, context.policy);
|
|
31
|
+
if ('said' in own)
|
|
32
|
+
throw Error('bundle/standing');
|
|
33
|
+
const pack = packed(context, place, own.lines, {}), v = vocabOf(context.lock);
|
|
34
|
+
return { files: new Map([...pack.files, [LOCK, Buffer.from(pack.lines.join('\n') + '\n')]]), sources: pack.names.map(name => { const pin = own.lines.find(line => line.scope === `uses/${name}`); const doc = linesOf(Buffer.from(pack.lines.join('\n'))).find(line => line.scope === `sources/${name}` && line.measure === 'digest'); return { name, kind: 'compiled', document: doc.value, pin: pin.value, offers: [...new Set(linesOf(context.files.take(join(place, pin.needs))).filter(line => line.scope?.startsWith(`${v.offers}/${name}/`)).map(line => line.scope.split('/')[2]))] }; }) };
|
|
35
|
+
});
|
|
36
|
+
if (bundled !== undefined)
|
|
37
|
+
return bundled;
|
|
38
|
+
const [say, to, held] = [sayIn(context.lock), asked['to'] ?? '', await standingOr(context, place, context.policy)], there = linesOf(context.files.take(join(to, LOCK)) ?? new Uint8Array());
|
|
39
|
+
if (!await sharePath(context, place, to) || foreignIn(there))
|
|
28
40
|
return [say('share-to', { place, to })];
|
|
29
41
|
if ('said' in held)
|
|
30
42
|
return held.said;
|
|
@@ -34,7 +46,8 @@ export const shared = (asked, context) => inPlace(asked, context, async (place)
|
|
|
34
46
|
catch {
|
|
35
47
|
return {};
|
|
36
48
|
} })()), pack = packed(context, place, held.lines, urls), bytes = new TextEncoder().encode(`${pack.lines.join('\n')}\n`);
|
|
37
|
-
|
|
49
|
+
const birth = sharedBirth(context, to, held.lines);
|
|
50
|
+
return !pack.names.length ? [say('share-none', { place }), ...birth] : ([...pack.files, [LOCK, bytes]].forEach(([at, laid]) => context.files.lay(to, at, laid)), [say('shared', { place, to, sources: pack.names.join(', '), digest: digestIn(context.lock)(bytes) }), ...linkOf(context, to, pack.lines, urls), ...birth, ...pack.lines]);
|
|
38
51
|
});
|
|
39
52
|
/** What a package names, checked whole before anything is asked: every line a world or a source's document, overlay or pin, by digest; each world the one this head pins; an order its pins rest in. A source whose bytes did not arrive, or did not hash to their line, is open, and so is every source resting on it: absence is not a refusal, and the rest arrives. */
|
|
40
53
|
export const unpacked = (context, document, bytes, read) => {
|
package/dist/src/core/read.d.ts
CHANGED
|
@@ -51,4 +51,5 @@ export type Vocab = ReturnType<typeof vocabOf>;
|
|
|
51
51
|
export declare const observedAt: (lock: readonly Said[], source: string, operation: string) => string;
|
|
52
52
|
export declare const stepOf: (lines: readonly Said[], lock: readonly Said[], source: string, operation: string) => Call | Plan;
|
|
53
53
|
export declare const planRead: (lines: readonly Said[], lock: readonly Said[], source: string, operation: string, place: string) => Call | Plan;
|
|
54
|
-
|
|
54
|
+
/** An explicit configured invocation may attempt anonymous access, still under the action flow and separate per-call consent. This is not an authentication claim. */
|
|
55
|
+
export declare const planAct: (lines: readonly Said[], lock: readonly Said[], source: string, operation: string, place: string, explicitInvocation?: boolean) => Call | Plan;
|
package/dist/src/core/read.js
CHANGED
|
@@ -60,4 +60,5 @@ const callOf = (lines, lock, source, operation, place) => {
|
|
|
60
60
|
};
|
|
61
61
|
const isCallable = (lock, classes) => classes.length === 1 && classes[0] === vocabOf(lock).callable;
|
|
62
62
|
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));
|
|
63
|
-
|
|
63
|
+
/** An explicit configured invocation may attempt anonymous access, still under the action flow and separate per-call consent. This is not an authentication claim. */
|
|
64
|
+
export const planAct = (lines, lock, source, operation, place, explicitInvocation = false) => ((call) => (call.kind === 'call' && isCallable(lock, call.classes) ? said('is-read', { source, operation }) : call.kind === 'call' && call.access.probe && !explicitInvocation ? said('access-unstated', { source, operation, method: call.carry['method']?.[0], path: call.carry['path']?.[0] }) : call))(callOf(lines, lock, source, operation, place));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Asked, Context } from './answer.ts';
|
|
2
|
+
/** Questions only describe the required connect, or route an already associated package; they never import or grant. */
|
|
3
|
+
export declare function receiverQuestion(asked: Asked, context: Context): Promise<{
|
|
4
|
+
place: string;
|
|
5
|
+
} | {
|
|
6
|
+
said: readonly string[];
|
|
7
|
+
} | undefined>;
|
|
8
|
+
/** Existing connect does the import; only its successful exact pins allow the receiver-owned association. */
|
|
9
|
+
export declare function receiverConnect(asked: Asked, context: Context): Promise<readonly string[] | undefined>;
|
|
10
|
+
/** Selection narrows this packaged surface; it never grants a call or replaces Bound's authority checks. */
|
|
11
|
+
export declare function receiverAllows(context: Context, place: string, source: string, operation: string): Promise<boolean>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { configuredPackage } from "../ports/adapters/configured.js";
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { LOCK } from '@lapxo/topos/wire';
|
|
4
|
+
import { baseOf, unless } from "./answer.js";
|
|
5
|
+
import { valueOf } from "./lock.js";
|
|
6
|
+
import { shapeOf } from "../ports/surface.js";
|
|
7
|
+
import { sayIn } from "./surface.js";
|
|
8
|
+
import { compose } from "./composition.js";
|
|
9
|
+
import { digestIn, linesOf } from "./worlds.js";
|
|
10
|
+
import { unpacked } from "./package.js";
|
|
11
|
+
import { associationFor, associationLines } from "./association.js";
|
|
12
|
+
import { received, laidIn } from "../sources/connect.js";
|
|
13
|
+
import { rootIn, homeIndex } from "../flows/device.js";
|
|
14
|
+
import { bundleRefusal } from "./share-bundle.js";
|
|
15
|
+
import { bornAt } from "../places/found.js";
|
|
16
|
+
/** Inspect only public installed data. Identity is integrity, never sender authentication or permission. */
|
|
17
|
+
async function material(context) {
|
|
18
|
+
const port = await context.packaged?.(), selection = port?.read('composition/selection.json');
|
|
19
|
+
if (!port || !selection)
|
|
20
|
+
return undefined;
|
|
21
|
+
const selected = JSON.parse(Buffer.from(selection).toString()), manifest = JSON.parse(Buffer.from(port.read('manifest.json') ?? []).toString());
|
|
22
|
+
if (selected.protocol !== 'head-composition@1' || !Array.isArray(selected.sources) || manifest.name !== selected.id || manifest.version !== selected.revision)
|
|
23
|
+
throw Error('bundle/identity');
|
|
24
|
+
const recipe = { id: selected.id, revision: selected.revision, title: selected.title, description: selected.description, sources: selected.sources.map((s) => ({ source: s.source, namespace: s.namespace, offers: s.offers })) };
|
|
25
|
+
const sources = selected.sources.map((s) => ({ name: s.source, kind: s.kind, document: s.document, pin: s.pin, offers: s.offers }));
|
|
26
|
+
const made = compose(recipe, sources, Number(valueOf(context.lock, 'share/bundle-selections').split('..')[1]));
|
|
27
|
+
if (!Buffer.from(made.bytes).equals(Buffer.from(selection)))
|
|
28
|
+
throw Error('bundle/selection');
|
|
29
|
+
const bytes = port.read(`composition/${LOCK}`);
|
|
30
|
+
if (!bytes)
|
|
31
|
+
throw Error('bundle/public-lock');
|
|
32
|
+
const files = new Map();
|
|
33
|
+
for (const line of linesOf(bytes))
|
|
34
|
+
if (line.needs)
|
|
35
|
+
files.set(line.needs, port.read(`composition/${line.needs}`));
|
|
36
|
+
const documents = configuredPackage(context, files);
|
|
37
|
+
const document = join(port.root, 'composition', LOCK), got = unpacked(context, document, bytes, files), digest = digestIn(context.lock);
|
|
38
|
+
if (!got.sources || got.open.length || got.sources.length !== sources.length || sources.some(s => s.kind !== 'compiled' || !got.sources.some(g => g.source === s.name && g.pin === s.pin && digest(g.document.bytes) === s.document)))
|
|
39
|
+
throw Error('bundle/public-package');
|
|
40
|
+
const identity = { id: recipe.id, revision: recipe.revision, digest: digest(Buffer.from(JSON.stringify([digest(selection), digest(bytes)]))) };
|
|
41
|
+
return { identity, document, lockDigest: digest(bytes), bytes, files, documents, sources, selection: made.entries };
|
|
42
|
+
}
|
|
43
|
+
async function associated(context, payload) {
|
|
44
|
+
const index = await homeIndex(context);
|
|
45
|
+
if ('refused' in index)
|
|
46
|
+
throw Error('bundle/index-unread');
|
|
47
|
+
return { index: index.lines, plan: associationFor(valueOf(context.lock, 'bundle/association-family'), payload.identity, index.lines) };
|
|
48
|
+
}
|
|
49
|
+
const validPins = async (context, place, sources) => {
|
|
50
|
+
if (!context.files.has(place, LOCK))
|
|
51
|
+
return false;
|
|
52
|
+
const held = await context.policy(place);
|
|
53
|
+
return !('refused' in held) && sources.every(s => held.lines.some(l => l.scope === `uses/${s.name}` && l.value === s.pin));
|
|
54
|
+
};
|
|
55
|
+
/** Questions only describe the required connect, or route an already associated package; they never import or grant. */
|
|
56
|
+
export async function receiverQuestion(asked, context) {
|
|
57
|
+
try {
|
|
58
|
+
const payload = await material(context);
|
|
59
|
+
if (!payload || asked['place'])
|
|
60
|
+
return undefined;
|
|
61
|
+
const { plan } = await associated(context, payload);
|
|
62
|
+
if (plan.state === 'collision')
|
|
63
|
+
throw Error('bundle/revision-collision');
|
|
64
|
+
if (plan.state === 'same' && plan.place && await validPins(context, plan.place, payload.sources))
|
|
65
|
+
return { place: plan.place };
|
|
66
|
+
return { said: [sayIn(context.lock)('bundle-receiver-ready', { document: payload.document, digest: payload.lockDigest, name: payload.identity.id, revision: payload.identity.revision, place: plan.place })] };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return { said: bundleRefusal(context, error) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Existing connect does the import; only its successful exact pins allow the receiver-owned association. */
|
|
73
|
+
export async function receiverConnect(asked, context) {
|
|
74
|
+
const say = sayIn(context.lock);
|
|
75
|
+
try {
|
|
76
|
+
const payload = await material(context);
|
|
77
|
+
if (!payload || asked['document'] !== payload.document)
|
|
78
|
+
return undefined;
|
|
79
|
+
if (asked['digest'] !== payload.lockDigest)
|
|
80
|
+
throw Error('bundle/document-digest');
|
|
81
|
+
const { plan } = await associated(context, payload);
|
|
82
|
+
if (plan.state === 'collision')
|
|
83
|
+
throw Error('bundle/revision-collision');
|
|
84
|
+
const place = asked['place'] || plan.place || join(context.personal(), valueOf(context.lock, 'bundle/receiver-folder'), payload.identity.id);
|
|
85
|
+
if (plan.state === 'same' && plan.place === place && await validPins(context, place, payload.sources))
|
|
86
|
+
return [say('bundle-associated', { place, name: payload.identity.id, revision: payload.identity.revision })];
|
|
87
|
+
const refused = await unless(context, say('elicit-bundle-receive', { name: payload.identity.id, revision: payload.identity.revision, place, sources: payload.sources.map(s => { const doc = payload.documents.find(doc => doc.configuration.name === s.name); return doc ? `${s.name} (${doc.configuration.endpoint})` : s.name; }).join(', ') }));
|
|
88
|
+
if (refused)
|
|
89
|
+
return refused;
|
|
90
|
+
if (!context.files.has(place, LOCK)) {
|
|
91
|
+
const born = await bornAt(context, place, `bundle-${payload.identity.id}`);
|
|
92
|
+
if (!context.files.has(place, LOCK))
|
|
93
|
+
return born;
|
|
94
|
+
}
|
|
95
|
+
const root = await rootIn(context, place);
|
|
96
|
+
if ('said' in root)
|
|
97
|
+
return root.said;
|
|
98
|
+
const imported = await received(context, place, payload.document, payload.bytes, payload.lockDigest, laidIn(context, place, root.account), root.account, payload.files);
|
|
99
|
+
if (shapeOf(imported).cells.some(cell => ['refused', 'open'].includes(cell.state)) || !await validPins(context, place, payload.sources))
|
|
100
|
+
return imported;
|
|
101
|
+
const home = await rootIn(context, context.home());
|
|
102
|
+
if ('said' in home)
|
|
103
|
+
return [...imported, ...home.said];
|
|
104
|
+
const current = await context.policy(context.home());
|
|
105
|
+
if ('refused' in current)
|
|
106
|
+
throw Error('bundle/index-unread');
|
|
107
|
+
const fresh = associationFor(valueOf(context.lock, 'bundle/association-family'), payload.identity, current.lines);
|
|
108
|
+
const lot = associationLines(fresh, place, true, `policy:head/bundle/${payload.identity.id}/${baseOf(current.lines)}`), landed = lot.length ? await context.landRoot(context.home(), lot, home.account) : undefined;
|
|
109
|
+
return landed && 'refused' in landed ? [...imported, say('not-landed', { why: landed.refused })] : [...imported, say('bundle-associated', { place, name: payload.identity.id, revision: payload.identity.revision })];
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return bundleRefusal(context, error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Selection narrows this packaged surface; it never grants a call or replaces Bound's authority checks. */
|
|
116
|
+
export async function receiverAllows(context, place, source, operation) {
|
|
117
|
+
const payload = await material(context);
|
|
118
|
+
if (!payload)
|
|
119
|
+
return true;
|
|
120
|
+
const { plan } = await associated(context, payload);
|
|
121
|
+
return plan.state === 'same' && plan.place === place && await validPins(context, place, payload.sources) && payload.selection.some(s => s.source === source && s.offers.includes(operation));
|
|
122
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Asked, Context } from './answer.ts';
|
|
2
|
+
import { type Source } from './composition.ts';
|
|
3
|
+
/** Refusals expose only declared public bundle codes, never host paths or parsed payloads. */
|
|
4
|
+
export declare const bundleRefusal: (context: Context, error: unknown) => string[];
|
|
5
|
+
/** Export only the existing public package. Connecting a document remains the consented connect action; it is never hidden in export. */
|
|
6
|
+
export declare function shareBundle(asked: Asked, context: Context, read: () => Promise<{
|
|
7
|
+
readonly files: ReadonlyMap<string, Uint8Array>;
|
|
8
|
+
readonly sources: readonly Source[];
|
|
9
|
+
}>): Promise<string[] | undefined>;
|