@lapxo/head 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +21 -0
  2. package/TARGET.bound +786 -0
  3. package/dist/src/access/contract.d.ts +71 -0
  4. package/dist/src/access/contract.js +6 -0
  5. package/dist/src/access/obtain.d.ts +19 -0
  6. package/dist/src/access/obtain.js +70 -0
  7. package/dist/src/adopt/adopt.d.ts +12 -0
  8. package/dist/src/adopt/adopt.js +40 -0
  9. package/dist/src/compile/pin.d.ts +26 -0
  10. package/dist/src/compile/pin.js +74 -0
  11. package/dist/src/core/act.d.ts +18 -0
  12. package/dist/src/core/act.js +18 -0
  13. package/dist/src/core/answer.d.ts +102 -0
  14. package/dist/src/core/answer.js +26 -0
  15. package/dist/src/core/device.d.ts +26 -0
  16. package/dist/src/core/device.js +27 -0
  17. package/dist/src/core/lock.d.ts +11 -0
  18. package/dist/src/core/lock.js +13 -0
  19. package/dist/src/core/package.d.ts +54 -0
  20. package/dist/src/core/package.js +65 -0
  21. package/dist/src/core/read.d.ts +54 -0
  22. package/dist/src/core/read.js +64 -0
  23. package/dist/src/core/receipt.d.ts +13 -0
  24. package/dist/src/core/receipt.js +12 -0
  25. package/dist/src/core/surface.d.ts +6 -0
  26. package/dist/src/core/surface.js +7 -0
  27. package/dist/src/core/trust.d.ts +7 -0
  28. package/dist/src/core/trust.js +19 -0
  29. package/dist/src/core/worlds.d.ts +10 -0
  30. package/dist/src/core/worlds.js +9 -0
  31. package/dist/src/flows/act.d.ts +7 -0
  32. package/dist/src/flows/act.js +43 -0
  33. package/dist/src/flows/answer.d.ts +12 -0
  34. package/dist/src/flows/answer.js +25 -0
  35. package/dist/src/flows/call.d.ts +50 -0
  36. package/dist/src/flows/call.js +76 -0
  37. package/dist/src/flows/device.d.ts +26 -0
  38. package/dist/src/flows/device.js +32 -0
  39. package/dist/src/flows/plan.d.ts +8 -0
  40. package/dist/src/flows/plan.js +51 -0
  41. package/dist/src/flows/read.d.ts +9 -0
  42. package/dist/src/flows/read.js +95 -0
  43. package/dist/src/flows/world.d.ts +12 -0
  44. package/dist/src/flows/world.js +29 -0
  45. package/dist/src/index.d.ts +2 -0
  46. package/dist/src/index.js +138 -0
  47. package/dist/src/onboard/first.d.ts +7 -0
  48. package/dist/src/onboard/first.js +21 -0
  49. package/dist/src/places/admit.d.ts +7 -0
  50. package/dist/src/places/admit.js +32 -0
  51. package/dist/src/places/found.d.ts +7 -0
  52. package/dist/src/places/found.js +60 -0
  53. package/dist/src/ports/access/authorizer.d.ts +10 -0
  54. package/dist/src/ports/access/authorizer.js +34 -0
  55. package/dist/src/ports/access/flow.d.ts +13 -0
  56. package/dist/src/ports/access/flow.js +130 -0
  57. package/dist/src/ports/access/page.d.ts +12 -0
  58. package/dist/src/ports/access/page.js +50 -0
  59. package/dist/src/ports/access/person.d.ts +26 -0
  60. package/dist/src/ports/access/person.js +41 -0
  61. package/dist/src/ports/adapters/request.d.ts +41 -0
  62. package/dist/src/ports/adapters/request.js +69 -0
  63. package/dist/src/ports/bound.d.ts +61 -0
  64. package/dist/src/ports/bound.js +85 -0
  65. package/dist/src/ports/cache.d.ts +4 -0
  66. package/dist/src/ports/cache.js +14 -0
  67. package/dist/src/ports/documents.d.ts +34 -0
  68. package/dist/src/ports/documents.js +64 -0
  69. package/dist/src/ports/keychain.d.ts +8 -0
  70. package/dist/src/ports/keychain.js +12 -0
  71. package/dist/src/ports/keys.d.ts +14 -0
  72. package/dist/src/ports/keys.js +20 -0
  73. package/dist/src/ports/place.d.ts +11 -0
  74. package/dist/src/ports/place.js +32 -0
  75. package/dist/src/ports/process.d.ts +29 -0
  76. package/dist/src/ports/process.js +51 -0
  77. package/dist/src/ports/signers.d.ts +6 -0
  78. package/dist/src/ports/signers.js +14 -0
  79. package/dist/src/ports/spare.d.ts +1 -0
  80. package/dist/src/ports/spare.js +33 -0
  81. package/dist/src/ports/stores/credential-manager.d.ts +3 -0
  82. package/dist/src/ports/stores/credential-manager.js +22 -0
  83. package/dist/src/ports/stores/keychain.d.ts +3 -0
  84. package/dist/src/ports/stores/keychain.js +16 -0
  85. package/dist/src/ports/stores/secret-tool.d.ts +3 -0
  86. package/dist/src/ports/stores/secret-tool.js +16 -0
  87. package/dist/src/ports/surface.d.ts +126 -0
  88. package/dist/src/ports/surface.js +48 -0
  89. package/dist/src/ports/warm.d.ts +19 -0
  90. package/dist/src/ports/warm.js +46 -0
  91. package/dist/src/resources/view.d.ts +1 -0
  92. package/dist/src/resources/view.js +1 -0
  93. package/dist/src/signer.d.ts +2 -0
  94. package/dist/src/signer.js +14 -0
  95. package/dist/src/sources/access.d.ts +17 -0
  96. package/dist/src/sources/access.js +39 -0
  97. package/dist/src/sources/connect.d.ts +13 -0
  98. package/dist/src/sources/connect.js +130 -0
  99. package/dist/src/tools/access.d.ts +1 -0
  100. package/dist/src/tools/access.js +1 -0
  101. package/dist/src/tools/act.d.ts +1 -0
  102. package/dist/src/tools/act.js +1 -0
  103. package/dist/src/tools/admit.d.ts +1 -0
  104. package/dist/src/tools/admit.js +1 -0
  105. package/dist/src/tools/card.d.ts +3 -0
  106. package/dist/src/tools/card.js +13 -0
  107. package/dist/src/tools/connect.d.ts +1 -0
  108. package/dist/src/tools/connect.js +1 -0
  109. package/dist/src/tools/key.d.ts +3 -0
  110. package/dist/src/tools/key.js +11 -0
  111. package/dist/src/tools/land.d.ts +1 -0
  112. package/dist/src/tools/land.js +1 -0
  113. package/dist/src/tools/look.d.ts +8 -0
  114. package/dist/src/tools/look.js +41 -0
  115. package/dist/src/tools/place.d.ts +1 -0
  116. package/dist/src/tools/place.js +1 -0
  117. package/dist/src/tools/places.d.ts +7 -0
  118. package/dist/src/tools/places.js +32 -0
  119. package/dist/src/tools/plan.d.ts +1 -0
  120. package/dist/src/tools/plan.js +1 -0
  121. package/dist/src/tools/share.d.ts +1 -0
  122. package/dist/src/tools/share.js +1 -0
  123. package/dist/src/walk/context.d.ts +7 -0
  124. package/dist/src/walk/context.js +23 -0
  125. package/package.json +36 -4
  126. package/README.md +0 -3
@@ -0,0 +1,32 @@
1
+ import { placeNameOf } from "../core/answer.js";
2
+ import { canonical, parse } from '@lapxo/topos/wire';
3
+ import { deviceOf, idIn, uncoveredIn } from "../core/device.js";
4
+ import { members, valueOf } from "../core/lock.js";
5
+ import { fill, sayIn } from "../core/surface.js";
6
+ import { adoptsIn } from "../adopt/adopt.js";
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
+ export const deviceKey = (context, role) => context.keys.made?.(deviceOf(context.lock, role).account);
9
+ /** The keychain account of a place's root, when this keychain holds it — who configures the place is here; else what head says of that. */
10
+ export const rootIn = (context, place) => ((account) => (context.keys.read(account) === undefined ? { said: [sayIn(context.lock)('no-root', { place })] } : { account }))(fill(valueOf(context.lock, 'founding/root-account'), { place: placeNameOf(place) }));
11
+ export const widening = (context, place, lines, lot) => {
12
+ const parent = adoptsIn(context.lock, lines);
13
+ if (parent !== undefined)
14
+ 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')];
16
+ const wire = lines.filter((l) => l.scope === 'wire/fields');
17
+ const witnessed = (signed) => signed.map((line) => ((got) => (got.kind === 'fact' ? canonical({ ...got.value.fields, [field]: witness }) : line))(parse(line)));
18
+ if ('account' in root && witness !== undefined) {
19
+ if (wire.some((l) => members(l.value ?? '').includes(field)))
20
+ return { sign: (signed) => context.signRoot(place, witnessed(signed), root.account), land: (signed) => context.landRoot(place, witnessed(signed), root.account) };
21
+ if (lot.every((line) => / scope=wire\/fields /.test(line)))
22
+ return { sign: (signed) => context.signRoot(place, signed, root.account), land: (signed) => context.landRoot(place, signed, root.account) };
23
+ 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)))];
24
+ return { said: [sayIn(context.lock)('unwitnessed', { place }), ...wire.map((l) => canonical({ ...bare(l), by: 'target', value: 'withdraw' })),
25
+ canonical({ at: `policy:head/wire/${new Date(context.now()).toISOString()}`, by: 'target', form: 'alphabet', measure: 'id', role: 'reads', scope: 'wire/fields', value: named })] };
26
+ }
27
+ const stopped = uncoveredIn(context.lock, lines, context.device('person'), place, lot);
28
+ if (stopped)
29
+ return { said: stopped };
30
+ 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];
31
+ 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) };
32
+ };
@@ -0,0 +1,8 @@
1
+ import type { Asked, Context } from '../core/answer.ts';
2
+ /**
3
+ * A proposal the person signs: drafted lines shown to the person with the place and the base they are made against; on
4
+ * their yes to exactly those, signed by their key; what bound prints folding it as if landed comes back as it printed it. Nothing lands; the lot is kept by its digest.
5
+ */
6
+ export declare const planLot: (asked: Asked, context: Context) => Promise<readonly string[]>;
7
+ /** A planned lot landed by its digest, only while the place stands on the base it was planned against. */
8
+ export declare const landLot: (asked: Asked, context: Context) => Promise<readonly string[]>;
@@ -0,0 +1,51 @@
1
+ import { baseOf, inPlace, standingOr } from "../core/answer.js";
2
+ import { sayIn } from "../core/surface.js";
3
+ import { widening } from "./device.js";
4
+ import { parse } from '@lapxo/topos/wire';
5
+ /**
6
+ * A proposal the person signs: drafted lines shown to the person with the place and the base they are made against; on
7
+ * their yes to exactly those, signed by their key; what bound prints folding it as if landed comes back as it printed it. Nothing lands; the lot is kept by its digest.
8
+ */
9
+ export const planLot = (asked, context) => inPlace(asked, context, async (place) => {
10
+ const say = sayIn(context.lock);
11
+ const lot = (asked['lines'] ?? '').split('\n').filter(Boolean);
12
+ if (!lot.length || lot.some((line) => ((got) => got.kind !== 'fact' || got.value.fields['sig'] !== undefined || got.value.fields['by'] !== 'target')(parse(line))))
13
+ return [say('bad-lot')];
14
+ const held = await standingOr(context, place);
15
+ if ('said' in held)
16
+ return held.said;
17
+ const base = baseOf(held.lines);
18
+ const by = widening(context, place, held.lines, lot);
19
+ if ('said' in by)
20
+ return by.said;
21
+ const consent = await context.elicit(say('elicit', { count: String(lot.length), place, base, lines: lot.join('\n') }));
22
+ if (consent === 'none')
23
+ return [say('sign-yourself', { place }), ...lot];
24
+ if (consent === 'no')
25
+ return [say('no-yes')];
26
+ const signed = await by.sign(lot);
27
+ if ('refused' in signed)
28
+ return [signed.refused];
29
+ const now = await standingOr(context, place);
30
+ if ('said' in now)
31
+ return now.said;
32
+ if (baseOf(now.lines) !== base)
33
+ return [say('base-moved', { base })];
34
+ const kept = context.cache.keep(Buffer.from(JSON.stringify({ place, base, signed: signed.lines })));
35
+ return [say('planned', { lot: kept, base }), ...(await context.told(place, signed.lines))];
36
+ });
37
+ /** A planned lot landed by its digest, only while the place stands on the base it was planned against. */
38
+ export const landLot = async (asked, context) => {
39
+ const say = sayIn(context.lock);
40
+ const bytes = context.cache.held(asked['lot'] ?? '');
41
+ if (bytes === undefined)
42
+ return [say('no-lot', { lot: asked['lot'] ?? '' })];
43
+ const kept = JSON.parse(Buffer.from(bytes).toString('utf8'));
44
+ const now = await standingOr(context, kept.place);
45
+ if ('said' in now)
46
+ return now.said;
47
+ if (baseOf(now.lines) !== kept.base)
48
+ return [say('base-moved', { base: kept.base })];
49
+ const landed = await context.land(kept.place, kept.signed);
50
+ return 'refused' in landed ? [landed.refused] : [say('landed', { count: String(landed.lines.length) }), ...landed.lines];
51
+ };
@@ -0,0 +1,9 @@
1
+ import type { Asked, Context } from '../core/answer.ts';
2
+ /**
3
+ * A read of one offer the place declares of the class head reads: the response comes back once, typed by the world,
4
+ * and both are kept — the body's digest and the typed answer's, receipts the device key signs. Asked again as the same
5
+ * principal, while the place stands on what typed it, the person's grant holds now and the newest read is younger than
6
+ * the reuse the place allows, it answers from what was kept: nothing called. Asked for history, it shows what was kept
7
+ * of that request as of when it was read — evidence of that read, never a current answer — where the place allows it.
8
+ */
9
+ export declare const readSource: (asked: Asked, context: Context) => Promise<readonly string[]>;
@@ -0,0 +1,95 @@
1
+ import { under } from "../core/lock.js";
2
+ import { observedAt, planRead } from "../core/read.js";
3
+ import { basisOf, draftOf, receiptsOf, scopeOf, spentOf } from "../core/receipt.js";
4
+ import { redIn, sayIn } from "../core/surface.js";
5
+ import { grantedOf, isOpen, untilOf } from "../core/act.js";
6
+ import { canonical } from '@lapxo/topos/wire';
7
+ import { called } from "./call.js";
8
+ import { principalOf } from "../access/obtain.js";
9
+ import { jsonOf } from "../access/contract.js";
10
+ /** An intent an earlier act left open, closed by the read that looked at what it would have changed: a line naming that read-s digest, signed by head. */
11
+ const closeWith = async (intent, source, digest, { word, lines, lands }, context) => {
12
+ const say = sayIn(context.lock);
13
+ if (!intent)
14
+ return [];
15
+ if (!intent.startsWith(`${word('act/family')}/${source}/`) || !isOpen(lines, intent, [word('act/done'), word('act/closed')]))
16
+ return [say('nothing-open', { intent })];
17
+ const landed = await lands(draftOf('', word('receipt/at'), `${intent}/${word('act/closed')}`, digest, new Date(context.now()).toISOString()));
18
+ return 'refused' in landed ? [say('not-landed', { why: landed.refused })] : [...landed.lines, say('closed', { intent, digest })];
19
+ };
20
+ /**
21
+ * A read of one offer the place declares of the class head reads: the response comes back once, typed by the world,
22
+ * and both are kept — the body's digest and the typed answer's, receipts the device key signs. Asked again as the same
23
+ * principal, while the place stands on what typed it, the person's grant holds now and the newest read is younger than
24
+ * the reuse the place allows, it answers from what was kept: nothing called. Asked for history, it shows what was kept
25
+ * of that request as of when it was read — evidence of that read, never a current answer — where the place allows it.
26
+ */
27
+ export const readSource = (asked, context) => ((started) => called(asked, context, planRead, async ({ word, say, place, lines, source, operation, plan, prepared, most, typed, attach, send, lands, widen, uncovered }) => {
28
+ const [family, at, when] = [word('receipt/family'), word('receipt/at'), new Date(context.now()).toISOString()];
29
+ const closing = (digest) => closeWith(asked['closes'], source, digest, { word, lines, lands }, context);
30
+ const scopes = (principal) => [scopeOf(family, source, operation, prepared.key, principal), `${scopeOf(word('receipt/kept'), source, operation, prepared.key, principal)}/${basisOf(lines, word, source)}`];
31
+ const held = (digest) => ((bytes) => (bytes === undefined || digest === undefined ? undefined : { digest, bytes }))(digest === undefined ? undefined : context.cache.held(digest));
32
+ const keeping = (kept, answer) => (answer.length > 1 ? [draftOf(word('receipt/kept'), at, kept, context.cache.keep(Buffer.from(answer.join('\n'))), when)] : []);
33
+ const allowed = (key, measure) => (lines.find((l) => l.scope === `${word(key)}/${source}` && l.measure === measure) ?? lines.find((l) => l.scope === word(key) && l.measure === measure))?.value;
34
+ if (asked['why'] === 'true') {
35
+ const asOf = principalOf(context, plan, false), [newest] = asOf === undefined ? [] : receiptsOf(lines, at, scopes(asOf)[0]), pin = lines.find((l) => l.scope === `uses/${source}`);
36
+ const signed = newest && lines.find((l) => l.scope === newest.scope && l.value === newest.digest && l.at === `${at}:${newest.at}`), worlds = lines.filter((l) => l.scope?.startsWith('uses/') && (!l.needs || under(context.lock, 'place/worlds', l.needs))).map((l) => `${l.scope.slice('uses/'.length)} ${l.value}`);
37
+ return newest === undefined ? say('why-none', { source, operation }) : say('why', { source, operation, origin: plan.origin, at: newest.at, by: signed?.by ?? '', epoch: signed?.epoch ?? '', digest: newest.digest, pin: pin?.value ?? '', pinned: pin?.epoch ?? '', worlds: worlds.join(', ') });
38
+ }
39
+ if (asked['history'] === 'true') {
40
+ const asOf = principalOf(context, plan, false);
41
+ if (allowed('history/family', 'status') !== 'present')
42
+ return [...say('history-undeclared', { place, source }), canonical({ at: 'policy:person/history', by: 'target', form: 'alphabet', measure: 'status', role: 'writes', scope: `${word('history/family')}/${source}`, value: 'present' })];
43
+ const [newest, ...earlier] = asOf === undefined ? [] : receiptsOf(lines, at, scopes(asOf)[0]);
44
+ if (newest === undefined)
45
+ return say('history-none', { source, operation });
46
+ const keptThen = receiptsOf(lines, at, scopeOf(word('receipt/kept'), source, operation, prepared.key, asOf), true).find((k) => k.at === newest.at);
47
+ const typedThen = held(keptThen?.digest);
48
+ return [...say('history', { source, operation, at: newest.at, digest: newest.digest }), ...(typedThen ? new TextDecoder().decode(typedThen.bytes).split('\n') : say(keptThen ? 'history-gone' : 'history-untyped')),
49
+ ...earlier.flatMap((r) => say('history-earlier', { source, at: r.at, digest: r.digest }))];
50
+ }
51
+ const principal = principalOf(context, plan);
52
+ const stopped = uncovered('head', scopes(principal ?? ''));
53
+ if (stopped)
54
+ return stopped;
55
+ if (!grantedOf(lines, word('grant/family'), source, context.now())) {
56
+ const sources = [...new Set(lines.filter((l) => l.scope?.startsWith(`${word('vocab/region')}/`) && l.measure === word('vocab/origins')).map((l) => l.scope.split('/')[1]))].sort();
57
+ const until = untilOf(word('access/period'), context.now());
58
+ const line = (measure, value) => canonical({ at: 'policy:person/grants', by: 'target', form: 'alphabet', measure, role: 'writes', scope: word('grant/family'), value });
59
+ const granted = await widen(say('grant', { sources: sources.join(', '), place, until })[0], [line('sources', sources.join('|')), line('until', until)]);
60
+ if ('said' in granted)
61
+ return granted.said;
62
+ }
63
+ const reuse = Number(/^\d+\.\.(\d+)$/.exec(allowed('reuse/family', 'seconds') ?? '')?.[1] ?? Number.NaN);
64
+ const [newest] = principal === undefined ? [] : receiptsOf(lines, at, scopes(principal)[0]), stood = ((b) => b !== undefined && Number(Buffer.from(b.bytes.subarray(0, b.bytes.indexOf(10))).toString('utf8')) < 300)(held(newest?.digest));
65
+ if (principal !== undefined && newest !== undefined && stood && context.now() - Date.parse(newest.at) <= reuse * 1000) {
66
+ const [scope, kept] = scopes(principal);
67
+ const answered = held(receiptsOf(lines, at, kept).find((k) => k.at >= newest.at)?.digest);
68
+ if (answered !== undefined)
69
+ return [...new TextDecoder().decode(answered.bytes).split('\n'), ...say('reused', { scope: kept, digest: answered.digest, at: newest.at }), ...(await closing(newest.digest)), ...redIn(context.lock)('answer/kept', `answer/${source}/${operation}@${word('look/call')}`, started, context.machine())];
70
+ const body = held(newest.digest);
71
+ if (body !== undefined) {
72
+ const cut = body.bytes.indexOf(10);
73
+ const answer = await typed(Buffer.from(body.bytes.subarray(0, cut)).toString('utf8'), body.bytes.subarray(cut + 1));
74
+ const keptLines = keeping(kept, answer);
75
+ const landed = keptLines.length ? await lands(...keptLines) : { lines: [] };
76
+ return [...answer, ...say('reread', { scope, digest: body.digest, at: newest.at }), ...('lines' in landed ? landed.lines : []), ...(await closing(body.digest))];
77
+ }
78
+ }
79
+ if (spentOf(lines, family, at, source, context.now()) >= most('read/rate'))
80
+ return say('rate', { source });
81
+ const attached = await attach();
82
+ if ('said' in attached)
83
+ return attached.said;
84
+ const [scope, kept] = scopes(attached.principal ?? '');
85
+ const got = await send(attached.attachment);
86
+ if (got.kind !== 'got')
87
+ return say(got.kind === 'over' ? 'too-large' : 'unreachable-source', { source });
88
+ const digest = context.cache.keep(Buffer.concat([Buffer.from(`${got.status}\n`), got.bytes]));
89
+ const answer = await typed(String(got.status), got.bytes);
90
+ const observed = plan.access.probe && got.status < 300 ? [canonical({ at: `${at}:${when}`, by: 'target', form: 'alphabet', measure: word('vocab/access'), role: 'writes', scope: observedAt(context.lock, source, operation), value: word('vocab/anonymous') })] : [];
91
+ const found = plan.access.probe && got.status === 401 && got.authenticate ? await (await context.access(word('access/adapter')))?.discover?.(plan.origin, got.authenticate, async (url) => jsonOf((await context.files.read.documentOf(context, place, url))?.bytes ?? new Uint8Array())) : undefined;
92
+ const by = `${word('access/family')}/${source}/${word('access/authorizer')}`, authorizer = Object.entries(found ?? {}).flatMap(([k, f]) => Object.entries(f).map(([measure, all]) => canonical({ at: `${at}:${when}`, by: 'target', form: 'alphabet', measure, role: 'writes', scope: k ? `${by}/${k}` : by, value: all.join('|') })));
93
+ const landed = await lands(draftOf(family, at, scope, digest, when), ...(attached.principal === undefined ? [] : keeping(kept, answer)), ...observed, ...authorizer);
94
+ return [...answer, ...('lines' in landed ? [...landed.lines, ...(await closing(digest))] : say('not-landed', { why: landed.refused })), ...(found?.['']?.['issuer'] && 'lines' in landed ? say('authorizer-observed', { source, issuer: found['']['issuer'][0] }) : [])];
95
+ }))(context.machine());
@@ -0,0 +1,12 @@
1
+ import type { Context } from '../core/answer.ts';
2
+ import type { Said } from '../core/lock.ts';
3
+ /**
4
+ * The lines a place stands on with what its sources compile to. A source the place pins is read from the standing it
5
+ * pins, checked by hashing the bytes it holds — compiled once, never folded again; a file that no longer hashes to its pin
6
+ * is no standing. A place that pins a lock as its parent stands on nothing head may read: composing the two is bound's, never head's. A place that pins none and names a view has bound compile it, asked once; a fold bound refused is no standing either.
7
+ */
8
+ export declare const standingFor: (context: Context, place: string) => Promise<{
9
+ readonly lines: readonly Said[];
10
+ } | {
11
+ readonly unsettled: string;
12
+ }>;
@@ -0,0 +1,29 @@
1
+ import { join } from 'node:path';
2
+ import { under, valueOf } from "../core/lock.js";
3
+ import { sayIn } from "../core/surface.js";
4
+ import { pinnedOf } from "../core/worlds.js";
5
+ import { adoptsIn } from "../adopt/adopt.js";
6
+ /**
7
+ * The lines a place stands on with what its sources compile to. A source the place pins is read from the standing it
8
+ * pins, checked by hashing the bytes it holds — compiled once, never folded again; a file that no longer hashes to its pin
9
+ * is no standing. A place that pins a lock as its parent stands on nothing head may read: composing the two is bound's, never head's. A place that pins none and names a view has bound compile it, asked once; a fold bound refused is no standing either.
10
+ */
11
+ export const standingFor = async (context, place) => {
12
+ const [held, view] = [await context.standing(place), valueOf(context.lock, 'place/view')];
13
+ if ('refused' in held)
14
+ return { unsettled: held.refused };
15
+ const own = held.lines;
16
+ const parent = adoptsIn(context.lock, own);
17
+ if (parent !== undefined)
18
+ return { unsettled: sayIn(context.lock)('parent-lock', { place, parent }) };
19
+ const pins = own.filter((l) => l.scope?.startsWith('uses/') && under(context.lock, 'place/compiled', l.needs)).map((l) => [l, pinnedOf(context.lock, context.files.take(join(place, l.needs)))]);
20
+ const moved = pins.find(([l, p]) => p?.pin !== l.value);
21
+ if (moved)
22
+ return { unsettled: sayIn(context.lock)('compiled-moved', { file: moved[0].needs }) };
23
+ if (pins.length)
24
+ return { lines: [...own, ...pins.flatMap(([, p]) => p.lines)] };
25
+ if (!own.some((line) => line.scope === `view/${view}`))
26
+ return { lines: own };
27
+ const world = await context.world(place, view);
28
+ return 'refused' in world ? { unsettled: world.refused } : { lines: [...own, ...world.lines] };
29
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
3
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
4
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
5
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
6
+ });
7
+ }
8
+ return path;
9
+ };
10
+ import { basename, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
15
+ import { answerOf } from "./core/answer.js";
16
+ import { valueOf } from "./core/lock.js";
17
+ import { fill, sayIn } from "./core/surface.js";
18
+ import { matchTemplate, refusedOn, shapeOf, shapeSchema, surfaceOf } from "./ports/surface.js";
19
+ import { closing, cooled, founded, land, landAct, limits, pinnedAt, policyAt, run, settledView, sign, standingAt, told, warm, words } from "./ports/bound.js";
20
+ import { warmth } from "./ports/warm.js";
21
+ import { held, keep } from "./ports/cache.js";
22
+ import { registryFor } from "./ports/signers.js";
23
+ import { dropAt, folderOf, hasAt, layAt, listAt, lockAt, takeAt } from "./ports/place.js";
24
+ import * as documents from "./ports/documents.js";
25
+ import { keychain } from "./ports/keychain.js";
26
+ import { algorithmOf, deviceOf, idIn } from "./core/device.js";
27
+ import { publicOf } from "./ports/keys.js";
28
+ import { personOf } from "./ports/access/person.js";
29
+ import { inTurn } from "./flows/call.js";
30
+ const [ext, root] = ((self) => [self.slice(self.lastIndexOf('.')), fileURLToPath(new URL(self.endsWith('.ts') ? '..' : '../..', self))])(import.meta.url);
31
+ const lock = lockAt(root);
32
+ const surface = surfaceOf(lock);
33
+ const runs = valueOf(lock, 'runs');
34
+ Object.assign(words, Object.fromEntries(['read', 'view', 'sign', 'land', 'key', 'signer', 'pin', 'topos', 'standing', 'evidence', 'act', 'profile', 'receipts', 'refusal'].map((w) => [w, valueOf(lock, `bound/${w}`)])));
35
+ Object.assign(limits, { wait: Number(valueOf(lock, 'bound/wait').split('..')[1]), most: Number(valueOf(lock, 'bound/printed').split('..')[1]) });
36
+ Object.assign(warmth, { spares: Number(valueOf(lock, 'bound/spares').split('..')[1]), boot: valueOf(lock, 'bound/spare-env').split('|') });
37
+ /** One call at a time, with bound started ahead for it and nothing of it kept once it is answered. */
38
+ const turn = (act) => inTurn('', () => { void warm(runs); return act().finally(cooled); });
39
+ const offered = (n) => (n ? { listChanged: false } : undefined);
40
+ 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) } });
41
+ /** The person, asked as the client lets head ask; every wait on them is counted, so a time ceiling measures head's work and never the person's. */
42
+ let waited = 0;
43
+ const timed = (ask) => async (...args) => { const t = Date.now(); try {
44
+ return await ask(...args);
45
+ }
46
+ finally {
47
+ waited += Date.now() - t;
48
+ } };
49
+ const asked = personOf(lock, { 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]) }) });
50
+ const person = { consent: timed(asked.consent), enter: timed(asked.enter), choose: timed(asked.choose), visit: timed(asked.visit) };
51
+ /** head is its home: the folder of its index of places, and the keychain service its keys live under, one per home, so two heads on one keychain never share a key. A place's root is told to bound by the key id its lock names, its secret kept there under the place's account. */
52
+ const home = process.env[valueOf(lock, 'home/env')] || join(process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '', valueOf(lock, 'home/folder'));
53
+ const service = `${deviceOf(lock).service}/${basename(home).replace(/[^\w.-]+/g, '-')}`;
54
+ const store = valueOf(lock, `platform/${process.platform}/store`);
55
+ const keys = keychain(service, store);
56
+ const devices = new Map();
57
+ const device = (role) => devices.get(role) ?? ((secret) => (secret === undefined ? undefined : ((key) => { devices.set(role, key); return key; })(publicOf(secret))))(keys.read(deviceOf(lock, role).account));
58
+ const asRoot = (account) => ((device, key) => [key, device.signer, { BOUND_SIGNERS: registryFor(device.signer, service, account, key, valueOf(lock, 'founding/algorithm'), store) }])(deviceOf(lock), valueOf(lock, 'founding/root').split('/')[1]);
59
+ /** What a call is answered with, one call at a time as when bound blocked head: every bound process it starts is stopped when the client cancels it, and a call cancelled while it waits its turn is never begun. */
60
+ const contextOf = (signal) => {
61
+ /** One call's folds of a place, kept until something lands in it or is laid there; and the places its root acted in, whose receipts close once, when the call ends. */
62
+ const [folds, touched] = [new Map(), new Set()], forget = (place) => { for (const k of [...folds.keys()])
63
+ if (k.endsWith(`\0${place}`))
64
+ folds.delete(k); };
65
+ const once = (key, place, fold) => (folds.get(`${key}\0${place}`) ?? ((p) => (folds.set(`${key}\0${place}`, p), p))(fold()));
66
+ const acted = (place, root, done) => done.finally(() => { forget(place); if (root)
67
+ touched.add(place); });
68
+ const signRoot = (place, lot, account) => sign(runs, place, lot, ...asRoot(account), signal);
69
+ const signAs = (place, lot, role, lines) => ((one, id) => sign(runs, place, lot, id, one.signer, { BOUND_SIGNERS: registryFor(one.signer, service, one.account, id, algorithmOf(lines), store) }, signal))(deviceOf(lock, role), idIn(lines, device(role)) ?? deviceOf(lock, role).account);
70
+ return {
71
+ lock,
72
+ bound: (cwd, args) => run(runs, cwd, args, {}, signal),
73
+ standing: (place) => once('standing', place, () => standingAt(runs, place, (lines) => ['head', 'person'].flatMap((role) => ((id) => (id === undefined ? [] : [id]))(idIn(lines, device(role)))), (line) => line.at?.startsWith(`${valueOf(lock, 'receipt/at')}:`) === true, signal)),
74
+ policy: (place) => once('policy', place, () => policyAt(runs, place, signal)),
75
+ world: (place, view) => once(`world/${view}`, place, () => settledView(runs, place, view, signal)),
76
+ 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)}`) : ''),
77
+ files: { lay: (place, rel, bytes) => { forget(place); layAt(place, rel, bytes); }, drop: (place, rel) => { forget(place); dropAt(place, rel); }, has: hasAt, list: listAt, read: documents, take: takeAt, own: (rel) => takeAt(join(root, rel)) },
78
+ home: () => home,
79
+ personal: () => process.env[valueOf(lock, `platform/${process.platform}/home`)] ?? '',
80
+ found: (place, account) => acted(place, true, founded(runs, place, ...asRoot(account), signal)),
81
+ signRoot,
82
+ landRoot: (place, lot, account) => acted(place, true, landAct(runs, place, lot, ...asRoot(account), signal)),
83
+ settle: async () => { const red = []; for (const place of touched) {
84
+ const why = await closing(runs, place, signal);
85
+ if (why !== undefined)
86
+ red.push(sayIn(lock)('receipts-open', { place, why }));
87
+ } touched.clear(); return red; },
88
+ pinned: (place, file) => pinnedAt(runs, place, file, signal),
89
+ access: async (kind) => (kind !== valueOf(lock, 'access/adapter') || !/^[a-z0-9-]+$/.test(kind) ? undefined : import(__rewriteRelativeImportExtension(new URL(`./ports/access/${kind}${ext}`, import.meta.url).href)).then((m) => m.adapter)),
90
+ person,
91
+ cache: { keep, held },
92
+ signAs,
93
+ landAs: (place, lot, role, lines) => acted(place, false, ((one, id) => landAct(runs, place, lot, id, one.signer, { BOUND_SIGNERS: registryFor(one.signer, service, one.account, id, algorithmOf(lines), store) }, signal))(deviceOf(lock, role), idIn(lines, device(role)) ?? deviceOf(lock, role).account)),
94
+ land: (place, signed) => acted(place, true, land(runs, place, signed, signal)),
95
+ told: (place, signed) => told(runs, place, signed, words.standing, signal),
96
+ elicit: (message) => person.consent(message, valueOf(lock, 'said/elicit/yes')),
97
+ now: () => Date.now(),
98
+ machine: () => Date.now() - waited,
99
+ keys,
100
+ device,
101
+ roots: async () => (server.getClientCapabilities()?.roots ? (await server.listRoots()).roots.flatMap((root) => folderOf(root.uri) ?? []) : []),
102
+ };
103
+ };
104
+ const askedOf = (raw) => Object.fromEntries(Object.entries(raw ?? {}).map(([k, v]) => [k, v === undefined ? undefined : typeof v === 'object' ? JSON.stringify(v) : String(v)]));
105
+ const schemaOf = (fields) => ({ type: 'object',
106
+ properties: Object.fromEntries(fields.map((f) => [f.name, { type: f.type, description: f.about }])), required: fields.filter((f) => f.required).map((f) => f.name) });
107
+ if (surface.tools.length)
108
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: surface.tools.map((tool) => ({ name: tool.name, description: tool.about,
109
+ inputSchema: schemaOf([...tool.fields]), outputSchema: shapeSchema(lock), annotations: Object.fromEntries(tool.hints.map((hint) => [`${hint}Hint`, true])) })) }));
110
+ if (surface.tools.length)
111
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => turn(async () => {
112
+ const tool = surface.tools.find((one) => one.name === request.params.name);
113
+ if (tool === undefined)
114
+ return { isError: true, content: [] };
115
+ const refused = refusedOn(lock, process.platform);
116
+ const context = contextOf(extra.signal), answer = refused !== undefined ? [refused] : extra.signal.aborted ? [] : [...await (await answerOf('tools', tool.name))(askedOf(request.params.arguments), context), ...await context.settle()];
117
+ return { content: [{ type: 'text', text: answer.join('\n') }], structuredContent: shapeOf(answer) };
118
+ }));
119
+ if (surface.resources.length)
120
+ server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [] }));
121
+ if (surface.resources.length)
122
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, () => ({ resourceTemplates: surface.resources.map((r) => ({ name: r.name, uriTemplate: r.template, description: r.about, mimeType: r.mime })) }));
123
+ if (surface.resources.length)
124
+ server.setRequestHandler(ReadResourceRequestSchema, async (request, extra) => turn(async () => {
125
+ const found = surface.resources.flatMap((r) => ((vars) => (vars === undefined ? [] : [{ r, vars }]))(matchTemplate(r.template, request.params.uri)))[0];
126
+ if (found === undefined)
127
+ return { contents: [] };
128
+ const refused = refusedOn(lock, process.platform);
129
+ if (refused !== undefined)
130
+ return { contents: [{ uri: request.params.uri, mimeType: found.r.mime, text: refused }] };
131
+ const lines = await (await answerOf('resources', found.r.name))(askedOf(found.vars), contextOf(extra.signal));
132
+ return { contents: [{ uri: request.params.uri, mimeType: found.r.mime, text: lines.join('\n') }] };
133
+ }));
134
+ if (surface.prompts.length)
135
+ 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 })) })) }));
136
+ if (surface.prompts.length)
137
+ 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)));
138
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,7 @@
1
+ import { type Asked, type Context } from '../core/answer.ts';
2
+ /**
3
+ * The first source a person connects with no place named and none to read from: their place is founded in the folder
4
+ * the lock names under their own home, and the source connected to it, on one yes for both. A folder that already holds
5
+ * a place is used as it stands, never founded again, and its connect asks its own yes.
6
+ */
7
+ export declare const firstRun: (asked: Asked, context: Context, then: (place: string, covered: boolean) => Promise<readonly string[]>) => Promise<readonly string[]>;
@@ -0,0 +1,21 @@
1
+ import { basename, join } from 'node:path';
2
+ import { LOCK } from '@lapxo/topos/wire';
3
+ import { unless } from "../core/answer.js";
4
+ import { valueOf } from "../core/lock.js";
5
+ import { sayIn } from "../core/surface.js";
6
+ import { bornAt } from "../places/found.js";
7
+ /**
8
+ * The first source a person connects with no place named and none to read from: their place is founded in the folder
9
+ * the lock names under their own home, and the source connected to it, on one yes for both. A folder that already holds
10
+ * a place is used as it stands, never founded again, and its connect asks its own yes.
11
+ */
12
+ export const firstRun = async (asked, context, then) => {
13
+ const say = sayIn(context.lock), dir = join(context.personal(), valueOf(context.lock, 'first-run/folder')), name = basename(dir);
14
+ if (context.files.has(dir, LOCK))
15
+ return then(dir, false);
16
+ const refused = await unless(context, say('elicit-first-run', { dir, source: asked['source'] ?? '', document: asked['document'] ?? '' }));
17
+ if (refused)
18
+ return refused;
19
+ const born = await bornAt(context, dir, name);
20
+ return born[0] === say('born', { name, dir }) ? [...born, ...(await then(dir, true))] : born;
21
+ };
@@ -0,0 +1,7 @@
1
+ import type { Asked, Context } from '../core/answer.ts';
2
+ /**
3
+ * Another device admitted into a place by whoever holds its root, once they said yes: the public key its own lines hand
4
+ * over — what those lines claim it may sign is not read — admitted under the name given here, covering what is given here
5
+ * or, without it, what a member is covered for: it asks, keeps what it read and proves access, and widens nothing.
6
+ */
7
+ export declare const admitted: (asked: Asked, context: Context) => Promise<readonly string[]>;
@@ -0,0 +1,32 @@
1
+ import { parse } from '@lapxo/topos/wire';
2
+ import { inPlace, standingOr, unless } from "../core/answer.js";
3
+ import { admissionOf, idIn } from "../core/device.js";
4
+ import { valueOf } from "../core/lock.js";
5
+ import { sayIn } from "../core/surface.js";
6
+ import { rootIn } from "../flows/device.js";
7
+ /**
8
+ * Another device admitted into a place by whoever holds its root, once they said yes: the public key its own lines hand
9
+ * over — what those lines claim it may sign is not read — admitted under the name given here, covering what is given here
10
+ * or, without it, what a member is covered for: it asks, keeps what it read and proves access, and widens nothing.
11
+ */
12
+ export const admitted = (asked, context) => inPlace(asked, context, async (place) => {
13
+ const say = sayIn(context.lock), word = (k) => valueOf(context.lock, k);
14
+ const name = (asked['name'] ?? '').toLowerCase().replace(/[^a-z0-9-]+/g, '-');
15
+ const key = (asked['key'] ?? '').split('\n').map((line) => parse(line.trim())).flatMap((got) => (got.kind === 'fact' && got.value.fields['measure'] === 'public-key' ? [got.value.fields['value'] ?? ''] : []))[0];
16
+ if (!name || !key)
17
+ return [say('no-key')];
18
+ const [root, held] = [rootIn(context, place), await standingOr(context, place, context.policy)];
19
+ if ('said' in root)
20
+ return root.said;
21
+ if ('said' in held)
22
+ return held.said;
23
+ if (held.lines.some((l) => l.scope === `keys/${name}`) || idIn(held.lines, key) !== undefined)
24
+ return [say('key-taken', { name, place })];
25
+ const coverage = asked['coverage'] || word('member/coverage');
26
+ const refused = await unless(context, say('elicit-admit', { name, place, coverage }));
27
+ if (refused)
28
+ return refused;
29
+ const lot = admissionOf(context.lock, key, 'head', { id: name, coverage, at: `policy:head/admit/${new Date(context.now()).toISOString()}` });
30
+ const landed = await context.landRoot(place, lot, root.account);
31
+ return 'refused' in landed ? [say('not-landed', { why: landed.refused })] : [say('admitted', { name, place, coverage }), ...landed.lines];
32
+ });
@@ -0,0 +1,7 @@
1
+ import type { Asked, Context } from '../core/answer.ts';
2
+ /**
3
+ * A place born where the person names, once they said yes: founded with its own root and the prelude head declares,
4
+ * and named in head's own index of places — itself a place, founded the first time — by a line its root signs.
5
+ */
6
+ export declare const bornPlace: (asked: Asked, context: Context) => Promise<readonly string[]>;
7
+ export declare const bornAt: (context: Context, dir: string, name: string) => Promise<readonly string[]>;
@@ -0,0 +1,60 @@
1
+ import { isAbsolute, basename } from 'node:path';
2
+ import { canonical, LOCK } from '@lapxo/topos/wire';
3
+ import { unless } from "../core/answer.js";
4
+ import { admissionOf } from "../core/device.js";
5
+ import { valueOf } from "../core/lock.js";
6
+ import { deviceKey } from "../flows/device.js";
7
+ import { walkable } from "../walk/context.js";
8
+ import { fill, sayIn } from "../core/surface.js";
9
+ /**
10
+ * The lines a place is born with, every one drafted by target: its root, by the public half of a key kept only in the
11
+ * keychain and the signer that reaches it; this device's keys admitted; and the prelude head's lock declares under
12
+ * birth/ — the wire, the regions and views a world is read through, and the lifetime every reader is bounded by.
13
+ */
14
+ const birthOf = (lock, root, head, person, place) => [
15
+ ...[['class', 'authorize'], ['coverage', '*'], ['signer', valueOf(lock, 'device/head/signer')], ['public-key', root]].map(([measure, value]) => canonical({ at: 'policy:head/birth', by: 'target', form: 'alphabet', measure: measure, role: 'writes', scope: valueOf(lock, 'founding/root'), value: value })),
16
+ ...admissionOf(lock, head, 'head'), ...admissionOf(lock, person, 'person'),
17
+ ...lock.filter((l) => l.scope?.startsWith('birth/')).map(({ about: _about, scope, ...line }) => canonical({ ...line, value: fill(line.value ?? '', { place }), scope: scope.slice('birth/'.length), at: 'policy:head/birth', by: 'target' })),
18
+ ];
19
+ /** A place founded in a folder: a root key made in the keychain under the place's name — never a file — the birth lines written, and the first land signed by that root. */
20
+ const founded = async (context, dir, name) => {
21
+ const account = fill(valueOf(context.lock, 'founding/root-account'), { place: name });
22
+ const [head, person] = [deviceKey(context, 'head'), deviceKey(context, 'person')];
23
+ if (head === undefined || person === undefined)
24
+ return { refused: 'no-keychain' };
25
+ if (context.keys.read(account) !== undefined)
26
+ return { refused: 'place-taken' };
27
+ const root = context.keys.made?.(account);
28
+ if (root === undefined)
29
+ return { refused: 'no-keychain' };
30
+ context.files.lay(dir, LOCK, new TextEncoder().encode(`${birthOf(context.lock, root, head, person, name).join('\n')}\n`));
31
+ const born = await context.found(dir, account);
32
+ return ((got) => ('refused' in got ? { refused: 'not-landed', why: got.refused } : got))('refused' in born ? born : await walkable(context, dir, account));
33
+ };
34
+ /**
35
+ * A place born where the person names, once they said yes: founded with its own root and the prelude head declares,
36
+ * and named in head's own index of places — itself a place, founded the first time — by a line its root signs.
37
+ */
38
+ export const bornPlace = async (asked, context) => {
39
+ const say = sayIn(context.lock);
40
+ const dir = asked['dir'] ?? '';
41
+ if (!isAbsolute(dir))
42
+ return [say('place-absolute')];
43
+ const name = (asked['name'] || basename(dir)).toLowerCase().replace(/[^a-z0-9-]+/g, '-');
44
+ if (context.files.has(dir, LOCK))
45
+ return [say('place-exists', { dir })];
46
+ const refused = await unless(context, say('elicit-place', { name, dir }));
47
+ return refused ?? bornAt(context, dir, name);
48
+ };
49
+ export const bornAt = async (context, dir, name) => {
50
+ const say = sayIn(context.lock), born = await founded(context, dir, name);
51
+ if ('refused' in born)
52
+ return [say(born.refused, { name, dir, why: born.why ?? '' })];
53
+ const home = context.home();
54
+ const homed = context.files.has(home, LOCK) ? { lines: [] } : await founded(context, home, valueOf(context.lock, 'home/name'));
55
+ if ('refused' in homed)
56
+ return [say(homed.refused, { name: valueOf(context.lock, 'home/name'), dir: home, why: homed.why ?? '' })];
57
+ const line = canonical({ at: 'policy:head/places', by: 'target', form: 'alphabet', measure: 'id', role: 'writes', scope: `${valueOf(context.lock, 'home/family')}/${name}`, value: dir });
58
+ const indexed = await context.landRoot(home, [line], fill(valueOf(context.lock, 'founding/root-account'), { place: valueOf(context.lock, 'home/name') }));
59
+ return 'refused' in indexed ? [say('not-landed', { why: indexed.refused })] : [say('born', { name, dir }), ...indexed.lines];
60
+ };
@@ -0,0 +1,10 @@
1
+ import type { Fields } from '../../access/contract.ts';
2
+ /**
3
+ * The authorizer a resource names when it refuses a request: its protected resource metadata (RFC 9728) at the address
4
+ * its challenge gives, which must name the very origin that refused; the authorization server's metadata (RFC 8414),
5
+ * which must name the issuer the resource named and take S256; and from them the oauth2 authorization-code flow a
6
+ * document would have declared — the same steps, PKCE through head's page, a client registered (RFC 7591) only where
7
+ * the server offers registration and the person's own where it does not, the token and its refresh, renewed at the
8
+ * token endpoint. Every one of these reads goes with nothing attached; what does not hold is no authorizer.
9
+ */
10
+ export declare const discovered: (resource: string, challenge: string, get: (url: string) => Promise<unknown>) => Promise<Readonly<Record<string, Fields>> | undefined>;
@@ -0,0 +1,34 @@
1
+ const text = (json, key) => ((v) => (typeof v === 'string' && v ? v : undefined))(json?.[key]);
2
+ const texts = (json, key) => ((v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : []))(json?.[key]);
3
+ const fields = (pairs) => Object.fromEntries(Object.entries(pairs).map(([k, v]) => [k, [v].flat()]));
4
+ /**
5
+ * The authorizer a resource names when it refuses a request: its protected resource metadata (RFC 9728) at the address
6
+ * its challenge gives, which must name the very origin that refused; the authorization server's metadata (RFC 8414),
7
+ * which must name the issuer the resource named and take S256; and from them the oauth2 authorization-code flow a
8
+ * document would have declared — the same steps, PKCE through head's page, a client registered (RFC 7591) only where
9
+ * the server offers registration and the person's own where it does not, the token and its refresh, renewed at the
10
+ * token endpoint. Every one of these reads goes with nothing attached; what does not hold is no authorizer.
11
+ */
12
+ export const discovered = async (resource, challenge, get) => {
13
+ const at = /resource_metadata="([^"]+)"/.exec(challenge)?.[1];
14
+ if (at === undefined || !URL.canParse(at, resource))
15
+ return undefined;
16
+ const named = await get(new URL(at, resource).href), [issuer] = texts(named, 'authorization_servers'), own = text(named, 'resource');
17
+ if (own === undefined || !URL.canParse(own) || new URL(own).origin !== new URL(resource).origin || issuer === undefined || !URL.canParse(issuer))
18
+ return undefined;
19
+ const server = await get(`${new URL(issuer).origin}/.well-known/oauth-authorization-server${new URL(issuer).pathname.replace(/\/$/, '')}`);
20
+ const [authorize, token, register, methods] = [text(server, 'authorization_endpoint'), text(server, 'token_endpoint'), text(server, 'registration_endpoint'), texts(server, 'code_challenge_methods_supported')];
21
+ if (text(server, 'issuer') !== issuer || authorize === undefined || token === undefined || (methods.length > 0 && !methods.includes('S256')))
22
+ return undefined;
23
+ const [client, scopes] = [register ? '$steps.register.outputs.client' : '$client', texts(named, 'scopes_supported').join(' ')];
24
+ const sent = (step, pairs) => Object.fromEntries(Object.entries(pairs).filter(([, v]) => v).map(([k, v]) => [`${step}/payload/${k}`, fields({ value: v })]));
25
+ return {
26
+ '': fields({ type: 'oauth2/authorizationCode', issuer, 'attach-in': 'header', 'attach-name': 'Authorization', 'attach-template': 'Bearer {token}', token: '$steps.exchange.outputs.token', expires: '$steps.exchange.outputs.expires', refresh: '$steps.exchange.outputs.refresh', renew: token }),
27
+ ...(register ? { 'steps/0': fields({ id: 'register', url: register, encoding: 'json' }), 'steps/0/payload/redirect_uris': fields({ items: '$callback' }), 'steps/0/payload/grant_types': fields({ items: ['authorization_code', 'refresh_token'] }),
28
+ 'steps/0/payload/response_types': fields({ items: 'code' }), ...sent('steps/0', { client_name: 'head', token_endpoint_auth_method: "'none'" }), 'steps/0/outputs/client': fields({ value: '$response.body#/client_id' }) } : {}),
29
+ 'steps/1': fields({ id: 'authorize', page: authorize }), 'steps/1/outputs/code': fields({ value: '$response.body#/code' }),
30
+ ...sent('steps/1', { client_id: client, redirect_uri: '$callback', response_type: 'code', scope: scopes, state: '$state', code_challenge: 's256($verifier)', code_challenge_method: 'S256', resource: new URL(resource).origin }),
31
+ 'steps/2': fields({ id: 'exchange', url: token, encoding: 'form' }), ...sent('steps/2', { grant_type: 'authorization_code', code: '$steps.authorize.outputs.code', redirect_uri: '$callback', client_id: client, code_verifier: '$verifier', resource: new URL(resource).origin }),
32
+ 'steps/2/outputs/token': fields({ value: '$response.body#/access_token' }), 'steps/2/outputs/expires': fields({ value: '$response.body#/expires_in' }), 'steps/2/outputs/refresh': fields({ value: '$response.body#/refresh_token' }),
33
+ };
34
+ };
@@ -0,0 +1,13 @@
1
+ import type { AccessAdapter } from '../../access/contract.ts';
2
+ /**
3
+ * Access as its document declares it, one way for every kind: the steps its flow names, in order — an operation of a
4
+ * source, an address its authorizer names, or a page the person is sent to and comes back from, opened first so every
5
+ * step before it knows where the person comes back — each sent what its payload says, with the token so far when it
6
+ * says so, keeping what its outputs point at — whatever its status, since an answer that calls for a next step may come
7
+ * as an error; a step whose when holds nothing is passed over, one whose answer holds nothing its outputs name stops the
8
+ * flow by its name, and so does one that answers an error and names nothing to keep. What the person is asked is asked the first time a step needs it, as the
9
+ * flow says: a form, a one-time code, a choice among what an answer listed, or a secret on head's own page as far as the
10
+ * place's root allows. What the person gives travels only to the source's origin or one its own document names. Only
11
+ * the token is kept, and it rides a request as the flow's template says.
12
+ */
13
+ export declare const adapter: AccessAdapter;