@lapxo/topos 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +213 -0
- package/TARGET.bound +567 -0
- package/dist/authority/admit.d.ts +2 -0
- package/dist/authority/admit.js +5 -0
- package/dist/authority/index.d.ts +4 -0
- package/dist/authority/index.js +4 -0
- package/dist/authority/motion.d.ts +11 -0
- package/dist/authority/motion.js +3 -0
- package/dist/capsule/asked.d.ts +28 -0
- package/dist/capsule/asked.js +9 -0
- package/dist/capsule/declaration.d.ts +26 -0
- package/dist/capsule/declaration.js +45 -0
- package/dist/capsule/index.d.ts +3 -0
- package/dist/capsule/index.js +3 -0
- package/dist/capsule/shell.d.ts +51 -0
- package/dist/capsule/shell.js +64 -0
- package/dist/classes/scale.d.ts +16 -0
- package/dist/classes/scale.js +1 -0
- package/dist/contract/index.d.ts +27 -0
- package/dist/contract/index.js +63 -0
- package/dist/forms/alphabet.d.ts +3 -0
- package/dist/forms/alphabet.js +19 -0
- package/dist/forms/budget.d.ts +3 -0
- package/dist/forms/budget.js +11 -0
- package/dist/forms/distribution.d.ts +3 -0
- package/dist/forms/distribution.js +36 -0
- package/dist/forms/form.d.ts +31 -0
- package/dist/forms/form.js +42 -0
- package/dist/forms/index.d.ts +12 -0
- package/dist/forms/index.js +11 -0
- package/dist/forms/interval.d.ts +3 -0
- package/dist/forms/interval.js +11 -0
- package/dist/forms/ladder.d.ts +3 -0
- package/dist/forms/ladder.js +11 -0
- package/dist/forms/lattice.d.ts +3 -0
- package/dist/forms/lattice.js +18 -0
- package/dist/forms/phase.d.ts +3 -0
- package/dist/forms/phase.js +11 -0
- package/dist/forms/rank.d.ts +3 -0
- package/dist/forms/rank.js +11 -0
- package/dist/forms/stretch.d.ts +3 -0
- package/dist/forms/stretch.js +11 -0
- package/dist/readers/bindings.d.ts +21 -0
- package/dist/readers/bindings.js +1 -0
- package/dist/readers/define.d.ts +37 -0
- package/dist/readers/define.js +59 -0
- package/dist/readers/index.d.ts +3 -0
- package/dist/readers/index.js +2 -0
- package/dist/wire/claim.d.ts +4 -0
- package/dist/wire/claim.js +43 -0
- package/dist/wire/claimline.d.ts +78 -0
- package/dist/wire/claimline.js +339 -0
- package/dist/wire/classes.d.ts +17 -0
- package/dist/wire/classes.js +39 -0
- package/dist/wire/descriptor.d.ts +45 -0
- package/dist/wire/descriptor.js +8 -0
- package/dist/wire/evidence.d.ts +2 -0
- package/dist/wire/evidence.js +2 -0
- package/dist/wire/grammar.d.ts +22 -0
- package/dist/wire/grammar.js +42 -0
- package/dist/wire/index.d.ts +14 -0
- package/dist/wire/index.js +11 -0
- package/dist/wire/line.d.ts +10 -0
- package/dist/wire/line.js +118 -0
- package/dist/wire/measures.d.ts +2 -0
- package/dist/wire/measures.js +13 -0
- package/dist/wire/names.d.ts +8 -0
- package/dist/wire/names.js +8 -0
- package/dist/wire/outcome.d.ts +18 -0
- package/dist/wire/outcome.js +3 -0
- package/dist/wire/sig.d.ts +15 -0
- package/dist/wire/sig.js +15 -0
- package/dist/wire/spec.d.ts +28 -0
- package/dist/wire/spec.js +1 -0
- package/dist/wire/types.d.ts +73 -0
- package/dist/wire/types.js +2 -0
- package/dist/wire/values.d.ts +23 -0
- package/dist/wire/values.js +80 -0
- package/package.json +68 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire reads itself: a claim is read by the lock of the wire that stood when it was signed. Its fields, their
|
|
3
|
+
* alphabets and forms, and the algorithms its digests and signatures name, are lines of that lock with an epoch —
|
|
4
|
+
* never a grammar compiled here and never a file this reads. What stays compiled is the line and the forms of the
|
|
5
|
+
* algebra; every name the wire admits arrives as a claim. The fields that say who signed and when, its envelope, never
|
|
6
|
+
* make two claims different claims.
|
|
7
|
+
*/
|
|
8
|
+
import { intervals } from '@lapxo/obligations';
|
|
9
|
+
import { parse as parseLine } from "./line.js";
|
|
10
|
+
import { abstain, fact, refuse } from "./outcome.js";
|
|
11
|
+
import { unpipe, boundOf, REF_RELATIONS, BOUND_FORMS } from "./values.js";
|
|
12
|
+
import { outranks, passes, region, resolution, within } from '@lapxo/obligations/field';
|
|
13
|
+
import { parseSignature } from "./sig.js";
|
|
14
|
+
import { under } from "../authority/admit.js";
|
|
15
|
+
export const ENVELOPE = new Set(['sig', 'expires', 'repo', 'by', 'epoch']);
|
|
16
|
+
const WITHDRAW = 'withdraw';
|
|
17
|
+
const LOCK = 'audit/wire/';
|
|
18
|
+
export function isWireClaim(fields) {
|
|
19
|
+
return (fields['scope'] ?? '').startsWith(LOCK);
|
|
20
|
+
}
|
|
21
|
+
function epochOf(fields) {
|
|
22
|
+
const value = fields['epoch'] ?? '';
|
|
23
|
+
return /^\d+$/.test(value) ? Number(value) : 0;
|
|
24
|
+
}
|
|
25
|
+
function names(withdraw, line) {
|
|
26
|
+
return Object.keys(withdraw).filter((k) => k !== 'value' && !ENVELOPE.has(k)).every((k) => line[k] === withdraw[k]);
|
|
27
|
+
}
|
|
28
|
+
export function wireAt(lines, epoch) {
|
|
29
|
+
const mine = lines.filter((f) => (f['scope'] ?? '').startsWith(LOCK) && epochOf(f) <= epoch);
|
|
30
|
+
const withdrawals = mine.filter((f) => f['value'] === WITHDRAW);
|
|
31
|
+
const standing = new Map();
|
|
32
|
+
for (const f of mine) {
|
|
33
|
+
if (f['value'] === WITHDRAW || withdrawals.some((w) => names(w, f)))
|
|
34
|
+
continue;
|
|
35
|
+
const held = standing.get(f['scope'] ?? '');
|
|
36
|
+
if (!held || epochOf(f) >= epochOf(held))
|
|
37
|
+
standing.set(f['scope'] ?? '', f);
|
|
38
|
+
}
|
|
39
|
+
if (!standing.size)
|
|
40
|
+
return null;
|
|
41
|
+
const ids = (name) => new Set((standing.get(`${LOCK}${name}`)?.['value'] ?? '').split('|').filter(Boolean));
|
|
42
|
+
const formOf = new Map([...standing]
|
|
43
|
+
.filter(([scope]) => scope.startsWith(`${LOCK}form/`))
|
|
44
|
+
.map(([scope, f]) => [scope.slice(`${LOCK}form/`.length), f['value'] ?? '']));
|
|
45
|
+
const lists = new Map([...standing.keys()]
|
|
46
|
+
.map((scope) => scope.slice(LOCK.length))
|
|
47
|
+
.filter((name) => !name.includes('/'))
|
|
48
|
+
.map((name) => [name, ids(name)]));
|
|
49
|
+
return {
|
|
50
|
+
epoch,
|
|
51
|
+
fields: ids('fields'),
|
|
52
|
+
required: ids('required'),
|
|
53
|
+
roles: ids('roles'),
|
|
54
|
+
forms: ids('forms'),
|
|
55
|
+
classes: ids('at-classes'),
|
|
56
|
+
digests: new Set([...ids('digest-algorithms')].map((one) => one.split(':')[0])),
|
|
57
|
+
signatures: ids('signature-algorithms'),
|
|
58
|
+
formOf,
|
|
59
|
+
lists,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function admittedDigest(value, wire) {
|
|
63
|
+
const colon = value.indexOf(':');
|
|
64
|
+
return colon > 0 && wire.digests.has(value.slice(0, colon)) && /^[0-9a-f]+$/i.test(value.slice(colon + 1));
|
|
65
|
+
}
|
|
66
|
+
function isRegion(need) {
|
|
67
|
+
if (need.startsWith('/') || /[\s\\]/.test(need))
|
|
68
|
+
return false;
|
|
69
|
+
const steps = need.split('/').filter((step) => step !== '');
|
|
70
|
+
return steps.length > 0 && steps.every((step, i) => step !== '.' && step !== '..' && (!step.includes('*') || i === steps.length - 1));
|
|
71
|
+
}
|
|
72
|
+
const FIELD_FORMS = {
|
|
73
|
+
region: (value) => value.split('|').filter(Boolean).every(isRegion),
|
|
74
|
+
class: (value, wire) => {
|
|
75
|
+
const colon = value.indexOf(':');
|
|
76
|
+
if (colon <= 0 || !wire.classes.has(value.slice(0, colon)))
|
|
77
|
+
return false;
|
|
78
|
+
const rest = value.slice(colon + 1);
|
|
79
|
+
return !/^[a-z0-9-]+:[0-9a-f]{16,}$/i.test(rest) || admittedDigest(rest, wire);
|
|
80
|
+
},
|
|
81
|
+
signature: (value, wire) => {
|
|
82
|
+
const got = parseSignature(value);
|
|
83
|
+
if (got === null || !wire.signatures.has(got.algorithm))
|
|
84
|
+
return false;
|
|
85
|
+
const era = got.algorithm.slice(got.algorithm.lastIndexOf(':') + 1);
|
|
86
|
+
return admittedDigest(got.algorithm, wire) || (got.algorithm.includes(':') && (wire.lists.get('era')?.has(era) ?? false));
|
|
87
|
+
},
|
|
88
|
+
count: (value) => /^\d+$/.test(value),
|
|
89
|
+
};
|
|
90
|
+
function debit(field, why) {
|
|
91
|
+
return refuse(why, field);
|
|
92
|
+
}
|
|
93
|
+
export function fromLine(text, wire, grammars) {
|
|
94
|
+
const line = parseLine(text);
|
|
95
|
+
if (line.kind !== 'fact') {
|
|
96
|
+
return line.kind === 'abstain' ? abstain(line.why) : refuse(line.why, line.named);
|
|
97
|
+
}
|
|
98
|
+
const f = line.value.fields;
|
|
99
|
+
if (wire) {
|
|
100
|
+
for (const k of Object.keys(f))
|
|
101
|
+
if (!wire.fields.has(k))
|
|
102
|
+
return debit(k, `\`${k}=\` is not a field of the wire at epoch ${wire.epoch}`);
|
|
103
|
+
for (const k of wire.required)
|
|
104
|
+
if (f[k] === undefined)
|
|
105
|
+
return debit(k, `a claim with no \`${k}\``);
|
|
106
|
+
for (const [k, form] of wire.formOf) {
|
|
107
|
+
const v = f[k];
|
|
108
|
+
if (v === undefined)
|
|
109
|
+
continue;
|
|
110
|
+
const ids = wire.lists.get(form);
|
|
111
|
+
const reads = FIELD_FORMS[form] ?? (ids ? (value) => ids.has(value) : undefined);
|
|
112
|
+
if (!reads)
|
|
113
|
+
return abstain(`\`${k}\` takes the form ${form}, which this reader does not know`);
|
|
114
|
+
if (!reads(v, wire))
|
|
115
|
+
return debit(k, `\`${k}=${v}\` is not a ${form} of the wire at epoch ${wire.epoch}`);
|
|
116
|
+
}
|
|
117
|
+
if (f.role !== undefined && !wire.roles.has(f.role))
|
|
118
|
+
return abstain(`\`role=${f.role}\` is a role this wire does not name`);
|
|
119
|
+
if (!wire.forms.has(f.form ?? ''))
|
|
120
|
+
return abstain(`\`form=${f.form}\` is a form this wire does not name`);
|
|
121
|
+
}
|
|
122
|
+
const listed = Array.isArray(grammars) ? grammars : undefined;
|
|
123
|
+
const bound = f.value === WITHDRAW ? { kind: 'enumerated', values: [WITHDRAW] } : boundOf(f.form ?? '', f.value ?? '', listed);
|
|
124
|
+
if (bound === null) {
|
|
125
|
+
const mine = BOUND_FORMS.includes(f.form ?? '') || (listed?.some((g) => g.form === f.form) ?? false);
|
|
126
|
+
return mine
|
|
127
|
+
? debit('value', `\`${f.value}\` is not a ${f.form}`)
|
|
128
|
+
: abstain(`\`form=${f.form}\` is a form this reader does not know`);
|
|
129
|
+
}
|
|
130
|
+
const role = f.role === 'writes' ? 'writes' : f.role === 'demands' ? 'demands' : 'reads';
|
|
131
|
+
const extra = {};
|
|
132
|
+
for (const [k, v] of Object.entries(f)) {
|
|
133
|
+
if (['scope', 'measure', 'form', 'value', 'by', 'at', 'role', 'unit', 'multiplicity', 'epoch', 'ref', 'to', 'ref_by'].includes(k))
|
|
134
|
+
continue;
|
|
135
|
+
extra[k] = v;
|
|
136
|
+
}
|
|
137
|
+
const rel = f.ref;
|
|
138
|
+
const to = f.to === undefined ? null : unpipe(f.to);
|
|
139
|
+
const reference = rel && to && REF_RELATIONS.includes(rel)
|
|
140
|
+
? { rel, to, by: f.ref_by === 'declared' ? 'declared' : 'inferred' }
|
|
141
|
+
: null;
|
|
142
|
+
return fact({
|
|
143
|
+
scope: f.scope ?? '',
|
|
144
|
+
measure: f.measure ?? '',
|
|
145
|
+
bound,
|
|
146
|
+
role,
|
|
147
|
+
by: f.by ?? '',
|
|
148
|
+
at: f.at ?? '',
|
|
149
|
+
...(f.unit !== undefined ? { unit: f.unit } : {}),
|
|
150
|
+
...(f.multiplicity === 'one' || f.multiplicity === 'many' || f.multiplicity === 'unknown'
|
|
151
|
+
? { multiplicity: f.multiplicity }
|
|
152
|
+
: {}),
|
|
153
|
+
...(f.epoch !== undefined ? { epoch: f.epoch } : {}),
|
|
154
|
+
...(reference ? { reference } : {}),
|
|
155
|
+
extra,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const KEYS = 'keys';
|
|
159
|
+
export function isKeyClaim(fields) {
|
|
160
|
+
const scope = fields['scope'] ?? '';
|
|
161
|
+
return scope === KEYS || scope.startsWith(`${KEYS}/`);
|
|
162
|
+
}
|
|
163
|
+
function depthWindow(value) {
|
|
164
|
+
const m = /^(\d+)\.\.(\d+)$/.exec(value);
|
|
165
|
+
return m ? { lo: Number(m[1]), hi: Number(m[2]) } : null;
|
|
166
|
+
}
|
|
167
|
+
const EPOCHS = intervals(-Infinity, Infinity);
|
|
168
|
+
const QUORUMS = intervals(1, Infinity);
|
|
169
|
+
function windowOf(signer) {
|
|
170
|
+
return signer.epoch ?? { start: 0, close: null };
|
|
171
|
+
}
|
|
172
|
+
export function windowAt(signer, epoch) {
|
|
173
|
+
const now = windowOf(signer);
|
|
174
|
+
const standing = (signer.windows ?? []).filter((w) => epoch >= w.from && (w.to === null || epoch < w.to));
|
|
175
|
+
return standing.length ? { start: now.start, close: standing.reduce((held, w) => EPOCHS.meet(held, w), EPOCHS.top).hi } : now;
|
|
176
|
+
}
|
|
177
|
+
function inside(signer, epoch) {
|
|
178
|
+
const w = windowAt(signer, epoch);
|
|
179
|
+
return epoch >= w.start && (w.close === null || epoch <= w.close);
|
|
180
|
+
}
|
|
181
|
+
function standsUntil(said) {
|
|
182
|
+
const claim = (f) => [f['role'], f['form'], f['at']].join(' ');
|
|
183
|
+
const withdrawn = new Map();
|
|
184
|
+
for (const f of said)
|
|
185
|
+
if (f['value'] === 'withdraw')
|
|
186
|
+
withdrawn.set(claim(f), EPOCHS.meet(withdrawn.get(claim(f)) ?? EPOCHS.top, { lo: -Infinity, hi: epochOf(f) }));
|
|
187
|
+
return (f) => withdrawn.get(claim(f))?.hi ?? null;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* What a key covers, and when it may sign, line by line: each coverage or epoch line stands from the epoch it was signed
|
|
191
|
+
* at until a withdraw of the same claim, named by its at, lands; what it allowed before stays allowed for what was signed
|
|
192
|
+
* then, and nothing signed after is allowed by it. Windows standing together close at the nearer, so only a withdraw
|
|
193
|
+
* widens, and never for a line signed before it. A withdrawn line is never counted again.
|
|
194
|
+
*/
|
|
195
|
+
function coverageOver(said) {
|
|
196
|
+
const until = standsUntil(said);
|
|
197
|
+
const spans = said.filter((f) => f['value'] !== 'withdraw').map((f) => ({ globs: (f['value'] ?? '').split('|').filter(Boolean), from: epochOf(f), to: until(f) }))
|
|
198
|
+
.filter((span) => span.to === null || span.from < span.to);
|
|
199
|
+
return { coverage: [...new Set(spans.filter((span) => span.to === null).flatMap((span) => span.globs))], spans };
|
|
200
|
+
}
|
|
201
|
+
function windowsOver(said) {
|
|
202
|
+
const until = standsUntil(said);
|
|
203
|
+
return said.filter((f) => f['value'] !== 'withdraw').flatMap((f) => ((w) => (w === null ? [] : [{ ...w, from: epochOf(f), to: until(f) }]))(depthWindow(f['value'] ?? '')))
|
|
204
|
+
.filter((w) => w.to === null || w.from < w.to);
|
|
205
|
+
}
|
|
206
|
+
export function signersOf(lines, root, verify) {
|
|
207
|
+
const quorumLines = lines
|
|
208
|
+
.filter((f) => f['scope'] === KEYS && f['measure'] === 'quorum' && f['by'] === root.id && verify(f, root.publicKey))
|
|
209
|
+
.sort((a, b) => epochOf(a) - epochOf(b));
|
|
210
|
+
const quorumAt = (epoch) => {
|
|
211
|
+
const standing = quorumLines.filter((f) => epochOf(f) <= epoch).pop();
|
|
212
|
+
return QUORUMS.meet(QUORUMS.top, depthWindow(standing?.['value'] ?? '') ?? QUORUMS.top).lo;
|
|
213
|
+
};
|
|
214
|
+
const ids = [...new Set(lines
|
|
215
|
+
.map((f) => f['scope'] ?? '')
|
|
216
|
+
.filter((scope) => scope.startsWith(`${KEYS}/`))
|
|
217
|
+
.map((scope) => scope.slice(KEYS.length + 1)))];
|
|
218
|
+
const closedAt = (signer, closes) => {
|
|
219
|
+
const close = closes.get(signer.id);
|
|
220
|
+
return close === undefined ? signer : { ...signer, epoch: { start: windowOf(signer).start, close } };
|
|
221
|
+
};
|
|
222
|
+
const claimsOn = (admitted, id, measure) => lines.filter((f) => {
|
|
223
|
+
const admitter = admitted.get(f['by'] ?? '');
|
|
224
|
+
return f['scope'] === `${KEYS}/${id}` && f['measure'] === measure && admitter !== undefined
|
|
225
|
+
&& admitter.keyClass === 'authorize' && inside(admitter, epochOf(f)) && verify(f, admitter.publicKey);
|
|
226
|
+
});
|
|
227
|
+
const admitUnder = (closes) => {
|
|
228
|
+
const admitted = new Map([[root.id, closedAt(root, closes)]]);
|
|
229
|
+
for (let grew = true; grew;) {
|
|
230
|
+
grew = false;
|
|
231
|
+
for (const id of ids) {
|
|
232
|
+
if (admitted.has(id))
|
|
233
|
+
continue;
|
|
234
|
+
const classLines = claimsOn(admitted, id, 'class');
|
|
235
|
+
const withdrawn = new Set(classLines.filter((f) => f['value'] === 'withdraw').map((f) => f['by'] ?? ''));
|
|
236
|
+
const counted = classLines
|
|
237
|
+
.filter((f) => f['value'] !== 'withdraw' && !withdrawn.has(f['by'] ?? ''))
|
|
238
|
+
.sort((a, b) => epochOf(a) - epochOf(b));
|
|
239
|
+
const admitters = [];
|
|
240
|
+
let start = null;
|
|
241
|
+
for (const f of counted) {
|
|
242
|
+
const by = f['by'] ?? '';
|
|
243
|
+
if (!admitters.includes(by))
|
|
244
|
+
admitters.push(by);
|
|
245
|
+
if (admitters.length >= quorumAt(epochOf(f))) {
|
|
246
|
+
start = epochOf(f);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const publicKey = claimsOn(admitted, id, 'public-key')[0]?.['value'] ?? '';
|
|
251
|
+
if (start === null || !publicKey)
|
|
252
|
+
continue;
|
|
253
|
+
admitted.set(id, closedAt({
|
|
254
|
+
id,
|
|
255
|
+
keyClass: counted.every((f) => f['value'] === 'authorize') ? 'authorize' : 'attest',
|
|
256
|
+
publicKey,
|
|
257
|
+
...coverageOver(claimsOn(admitted, id, 'coverage')),
|
|
258
|
+
windows: windowsOver(claimsOn(admitted, id, 'epoch')),
|
|
259
|
+
depth: depthWindow(claimsOn(admitted, id, 'resolution')[0]?.['value'] ?? '') ?? { lo: 1, hi: 1 },
|
|
260
|
+
admittedBy: admitters,
|
|
261
|
+
epoch: { start, close: null },
|
|
262
|
+
}, closes));
|
|
263
|
+
grew = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return admitted;
|
|
267
|
+
};
|
|
268
|
+
let closes = new Map();
|
|
269
|
+
for (let round = 0; round <= ids.length + 1; round++) {
|
|
270
|
+
const admitted = admitUnder(closes);
|
|
271
|
+
const next = new Map();
|
|
272
|
+
for (const id of admitted.keys()) {
|
|
273
|
+
const standing = windowsOver(claimsOn(admitted, id, 'epoch')).filter((w) => w.to === null);
|
|
274
|
+
if (standing.length)
|
|
275
|
+
next.set(id, standing.reduce((held, w) => EPOCHS.meet(held, w), EPOCHS.top).hi);
|
|
276
|
+
}
|
|
277
|
+
const moved = next.size !== closes.size || [...next].some(([id, close]) => closes.get(id) !== close);
|
|
278
|
+
if (!moved)
|
|
279
|
+
return { admitted: [...admitted.values()], potential: ids.filter((id) => !admitted.has(id)) };
|
|
280
|
+
closes = next;
|
|
281
|
+
}
|
|
282
|
+
const admitted = admitUnder(closes);
|
|
283
|
+
return { admitted: [...admitted.values()], potential: ids.filter((id) => !admitted.has(id)) };
|
|
284
|
+
}
|
|
285
|
+
export function authorityOf(fields, signers, verify) {
|
|
286
|
+
const by = fields['by'] ?? '';
|
|
287
|
+
if (!fields['sig'])
|
|
288
|
+
return { kind: 'grey', why: 'unsigned' };
|
|
289
|
+
const signer = signers.find((s) => s.id === by);
|
|
290
|
+
if (!signer)
|
|
291
|
+
return { kind: 'grey', why: `signer ${by || '∅'} is not admitted` };
|
|
292
|
+
if (!verify(fields, signer.publicKey))
|
|
293
|
+
return { kind: 'grey', why: `the signature does not hold for ${by}` };
|
|
294
|
+
const epoch = epochOf(fields);
|
|
295
|
+
const window = windowAt(signer, epoch);
|
|
296
|
+
if (window.close !== null && epoch > window.close) {
|
|
297
|
+
return { kind: 'refuse', why: `REFUSE·authority ${by} signed at epoch ${epoch}, after its epoch closed at ${window.close}`, named: by };
|
|
298
|
+
}
|
|
299
|
+
if (epoch < window.start)
|
|
300
|
+
return { kind: 'grey', why: `signed at epoch ${epoch}, before ${by} was admitted at ${window.start}` };
|
|
301
|
+
const scope = fields['scope'] ?? '';
|
|
302
|
+
const covering = signer.spans === undefined ? signer.coverage : signer.spans.filter((span) => epoch >= span.from && (span.to === null || epoch < span.to)).flatMap((span) => span.globs);
|
|
303
|
+
if (!covering.some((prefix) => prefix === '*' || under(prefix, scope))) {
|
|
304
|
+
return { kind: 'refuse', why: `REFUSE·authority ${by} does not cover ${scope}`, named: by };
|
|
305
|
+
}
|
|
306
|
+
const depth = resolution(region(scope));
|
|
307
|
+
if (depth < signer.depth.lo || depth > signer.depth.hi) {
|
|
308
|
+
return { kind: 'refuse', why: `REFUSE·authority ${by} may not sign at depth ${depth}`, named: by };
|
|
309
|
+
}
|
|
310
|
+
if (within(region(scope), region(KEYS)) && signer.keyClass !== 'authorize') {
|
|
311
|
+
return { kind: 'refuse', why: `REFUSE·authority ${by} is attest and cannot admit signers`, named: by };
|
|
312
|
+
}
|
|
313
|
+
return { kind: 'admitted', signer: by };
|
|
314
|
+
}
|
|
315
|
+
const COORDINATE_ROLES = ['source', 'derived', 'observed', 'foreign'];
|
|
316
|
+
const ROLE_PREFIXES = ['coordinate-role/', 'leaf-role/'];
|
|
317
|
+
const OUTRANKS = ['foreign', 'derived', 'observed'];
|
|
318
|
+
/** What a region's role makes of the paths it names: a source is written by hand, a render and a receipt by the fold. */
|
|
319
|
+
const REGIONED = new Map([['source', 'source'], ['render', 'derived'], ['receipt', 'derived']]);
|
|
320
|
+
export function roleClaimsOf(lines) {
|
|
321
|
+
return lines.flatMap((f) => {
|
|
322
|
+
const scope = f['scope'] ?? '';
|
|
323
|
+
const role = f['value'] ?? '';
|
|
324
|
+
const regioned = REGIONED.get(f['role'] ?? '');
|
|
325
|
+
if (scope.startsWith('region/') && f['measure'] === 'paths' && regioned !== undefined && role !== 'withdraw')
|
|
326
|
+
return role.split('|').filter(Boolean).map((name) => ({ name, role: regioned }));
|
|
327
|
+
const prefix = ROLE_PREFIXES.find((one) => scope.startsWith(one));
|
|
328
|
+
if (prefix === undefined || f['measure'] !== 'role' || !COORDINATE_ROLES.includes(role))
|
|
329
|
+
return [];
|
|
330
|
+
return [{ name: scope.slice(prefix.length), role: role }];
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
export function namesAt(at, name) {
|
|
334
|
+
return passes(region(at), name);
|
|
335
|
+
}
|
|
336
|
+
export function roleAt(at, claims) {
|
|
337
|
+
const place = { at: region(at).at, epoch: 0, seen: [] };
|
|
338
|
+
return outranks([place], claims.map((c) => [c.name, c.role]), OUTRANKS) ?? 'source';
|
|
339
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Bound, MeetSemilattice } from './types.ts';
|
|
2
|
+
export type Kind = 'claims' | 'forms' | 'readers' | 'views';
|
|
3
|
+
export interface FormBody<T> {
|
|
4
|
+
lattice(): MeetSemilattice<T>;
|
|
5
|
+
readonly helly: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface FileInput {
|
|
8
|
+
readonly place: string;
|
|
9
|
+
readonly text: string;
|
|
10
|
+
}
|
|
11
|
+
export type ReaderMethod = 'exact' | 'statistics';
|
|
12
|
+
export type ReaderInputKind = "bytes" | "session" | "execution" | "stream";
|
|
13
|
+
export interface ViewBody<T> {
|
|
14
|
+
to(bound: Bound): T;
|
|
15
|
+
from(shown: T): Bound | null;
|
|
16
|
+
}
|
|
17
|
+
export declare function matches(glob: string, place: string): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
function matchStar(pat, seg) {
|
|
2
|
+
if (!pat.includes('*'))
|
|
3
|
+
return pat === seg;
|
|
4
|
+
const bits = pat.split('*');
|
|
5
|
+
if (!seg.startsWith(bits[0]))
|
|
6
|
+
return false;
|
|
7
|
+
let i = bits[0].length;
|
|
8
|
+
for (let b = 1; b < bits.length; b++) {
|
|
9
|
+
const bit = bits[b];
|
|
10
|
+
if (bit === '')
|
|
11
|
+
return b === bits.length - 1 || matchStar(bits.slice(b).join('*'), seg.slice(i));
|
|
12
|
+
const hit = seg.indexOf(bit, i);
|
|
13
|
+
if (hit < 0)
|
|
14
|
+
return false;
|
|
15
|
+
i = hit + bit.length;
|
|
16
|
+
}
|
|
17
|
+
return i === seg.length || pat.endsWith('*');
|
|
18
|
+
}
|
|
19
|
+
function matchParts(g, gi, p, pi) {
|
|
20
|
+
if (gi === g.length)
|
|
21
|
+
return pi === p.length;
|
|
22
|
+
if (g[gi] === '**') {
|
|
23
|
+
if (gi + 1 === g.length)
|
|
24
|
+
return true;
|
|
25
|
+
for (let k = pi; k <= p.length; k++) {
|
|
26
|
+
if (matchParts(g, gi + 1, p, k))
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (pi === p.length)
|
|
32
|
+
return false;
|
|
33
|
+
if (!matchStar(g[gi], p[pi]))
|
|
34
|
+
return false;
|
|
35
|
+
return matchParts(g, gi + 1, p, pi + 1);
|
|
36
|
+
}
|
|
37
|
+
export function matches(glob, place) {
|
|
38
|
+
return matchParts(glob.split('/'), 0, place.split('/'), 0);
|
|
39
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inert capsule. Four classes, hash edges, no functions.
|
|
3
|
+
*/
|
|
4
|
+
import type { ScaleDescriptor } from '../classes/scale.ts';
|
|
5
|
+
import type { ReaderInputKind, ReaderMethod } from './classes.ts';
|
|
6
|
+
import type { Claim } from './types.ts';
|
|
7
|
+
interface ReaderDescriptor {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly handles: {
|
|
10
|
+
readonly format: string;
|
|
11
|
+
readonly version?: string;
|
|
12
|
+
};
|
|
13
|
+
readonly method: ReaderMethod;
|
|
14
|
+
readonly input: {
|
|
15
|
+
readonly kind: ReaderInputKind;
|
|
16
|
+
};
|
|
17
|
+
readonly needs: readonly string[];
|
|
18
|
+
readonly units: readonly string[];
|
|
19
|
+
readonly implementation: string;
|
|
20
|
+
readonly sample: {
|
|
21
|
+
readonly yes: readonly {
|
|
22
|
+
readonly place: string;
|
|
23
|
+
readonly text: string;
|
|
24
|
+
}[];
|
|
25
|
+
readonly no: readonly {
|
|
26
|
+
readonly place: string;
|
|
27
|
+
readonly text: string;
|
|
28
|
+
}[];
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
interface ViewDescriptor {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly measures: readonly string[];
|
|
34
|
+
readonly mode: 'lossless' | 'projection';
|
|
35
|
+
}
|
|
36
|
+
export interface ToposDescriptor {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly includes: readonly string[];
|
|
39
|
+
readonly claims: readonly Claim[];
|
|
40
|
+
readonly forms: readonly ScaleDescriptor[];
|
|
41
|
+
readonly readers: readonly ReaderDescriptor[];
|
|
42
|
+
readonly views: readonly ViewDescriptor[];
|
|
43
|
+
}
|
|
44
|
+
export declare const emptyDescriptor: (id: string) => ToposDescriptor;
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Alphabet {
|
|
2
|
+
readonly polarity: 'permit' | 'forbid';
|
|
3
|
+
readonly members: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export type Field = readonly [key: string, value: string];
|
|
6
|
+
export interface Resolution {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly at?: number | '*';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The grammars inside a value, one function each: handed the text a line carries it reads the value, handed the value
|
|
12
|
+
* it writes the canonical text, and what it writes reads back to what it was handed. A value is read after its line has
|
|
13
|
+
* unquoted it, and nothing outside the wire splits one.
|
|
14
|
+
*/
|
|
15
|
+
export declare function alphabet(text: string): Alphabet;
|
|
16
|
+
export declare function alphabet(value: Alphabet): string;
|
|
17
|
+
export declare function fields(text: string): readonly Field[];
|
|
18
|
+
export declare function fields(value: readonly Field[]): string;
|
|
19
|
+
export declare function steps(text: string): readonly string[];
|
|
20
|
+
export declare function steps(value: readonly string[]): string;
|
|
21
|
+
export declare function atResolution(text: string): Resolution;
|
|
22
|
+
export declare function atResolution(value: Resolution): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { unpipe } from "./values.js";
|
|
2
|
+
const [FORBID, EMPTY, MEMBER, PAIR, STEP, AT, EVERY, ESCAPE] = ['not:', 'none', '|', '=', '/', '@', '*', '\\'];
|
|
3
|
+
const refuse = (grammar, why) => {
|
|
4
|
+
throw new Error(`REFUSE·wire ${grammar}: ${why}`);
|
|
5
|
+
};
|
|
6
|
+
const digits = (text) => text !== '' && [...text].every((one) => one >= '0' && one <= '9');
|
|
7
|
+
export function alphabet(given) {
|
|
8
|
+
if (typeof given !== 'string') {
|
|
9
|
+
const written = given.members.map((one) => one.split(ESCAPE).join(ESCAPE + ESCAPE).split(MEMBER).join(ESCAPE + MEMBER)).join(MEMBER);
|
|
10
|
+
if (given.polarity === 'forbid')
|
|
11
|
+
return FORBID + written;
|
|
12
|
+
return written.startsWith(FORBID) || written === EMPTY ? refuse('alphabet', 'a permitted alphabet neither begins with not: nor is the one word none') : written || EMPTY;
|
|
13
|
+
}
|
|
14
|
+
const forbid = given.startsWith(FORBID);
|
|
15
|
+
const body = forbid ? given.slice(FORBID.length) : given;
|
|
16
|
+
const members = !forbid && body === EMPTY ? [] : unpipe(body) ?? refuse('alphabet', 'a backslash escapes only a pipe or itself');
|
|
17
|
+
return { polarity: forbid ? 'forbid' : 'permit', members: members.filter((one) => one !== '') };
|
|
18
|
+
}
|
|
19
|
+
export function fields(given) {
|
|
20
|
+
if (typeof given !== 'string')
|
|
21
|
+
return alphabet({ polarity: 'permit', members: given.map(([key, value]) => key + PAIR + value) });
|
|
22
|
+
const read = alphabet(given);
|
|
23
|
+
if (read.polarity === 'forbid')
|
|
24
|
+
refuse('fields', 'fields carry no polarity');
|
|
25
|
+
return read.members.map((one) => (one.indexOf(PAIR) > 0 ? [one.slice(0, one.indexOf(PAIR)), one.slice(one.indexOf(PAIR) + 1)] : refuse('fields', `\`${one}\` is not key=value`)));
|
|
26
|
+
}
|
|
27
|
+
export function steps(given) {
|
|
28
|
+
if (typeof given === 'string')
|
|
29
|
+
return given.split(STEP);
|
|
30
|
+
return given.some((one) => one.includes(STEP)) ? refuse('steps', 'a step holds no slash') : given.join(STEP);
|
|
31
|
+
}
|
|
32
|
+
export function atResolution(given) {
|
|
33
|
+
if (typeof given !== 'string')
|
|
34
|
+
return given.at === undefined ? given.name : given.name + AT + String(given.at);
|
|
35
|
+
const at = given.lastIndexOf(AT);
|
|
36
|
+
if (at < 0)
|
|
37
|
+
return { name: given };
|
|
38
|
+
const [name, said] = [given.slice(0, at), given.slice(at + 1)];
|
|
39
|
+
if (name === '' || (said !== EVERY && !digits(said)))
|
|
40
|
+
refuse('atResolution', `\`${given}\` is not name@digits or name@*`);
|
|
41
|
+
return { name, at: said === EVERY ? EVERY : Number(said) };
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** It answers the line, the forms' encodings, the names — how is a fact written? */
|
|
2
|
+
export { PROTOCOL } from './types.ts';
|
|
3
|
+
export type { Claim, Method } from './types.ts';
|
|
4
|
+
export { byBytes, canonical, parse, signedBytes } from './line.ts';
|
|
5
|
+
export { alphabet, atResolution, fields, steps } from './grammar.ts';
|
|
6
|
+
export type { Alphabet, Field, Resolution } from './grammar.ts';
|
|
7
|
+
export { fromLine, signersOf, authorityOf, windowAt, roleClaimsOf, roleAt, namesAt, isKeyClaim, isWireClaim, wireAt, ENVELOPE } from './claimline.ts';
|
|
8
|
+
export type { Signer, SignerVerdict, CoordinateRole, RoleClaim } from './claimline.ts';
|
|
9
|
+
export { matches } from './classes.ts';
|
|
10
|
+
export { CAPSULE, EXTENSION, LOCK, RECEIPTS } from './names.ts';
|
|
11
|
+
export { decimal } from './values.ts';
|
|
12
|
+
export { combine } from './measures.ts';
|
|
13
|
+
export { weakest } from './evidence.ts';
|
|
14
|
+
export { formatSignature, parseSignature, signedFieldsOf } from './sig.ts';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** It answers the line, the forms' encodings, the names — how is a fact written? */
|
|
2
|
+
export { PROTOCOL } from "./types.js";
|
|
3
|
+
export { byBytes, canonical, parse, signedBytes } from "./line.js";
|
|
4
|
+
export { alphabet, atResolution, fields, steps } from "./grammar.js";
|
|
5
|
+
export { fromLine, signersOf, authorityOf, windowAt, roleClaimsOf, roleAt, namesAt, isKeyClaim, isWireClaim, wireAt, ENVELOPE } from "./claimline.js";
|
|
6
|
+
export { matches } from "./classes.js";
|
|
7
|
+
export { CAPSULE, EXTENSION, LOCK, RECEIPTS } from "./names.js";
|
|
8
|
+
export { decimal } from "./values.js";
|
|
9
|
+
export { combine } from "./measures.js";
|
|
10
|
+
export { weakest } from "./evidence.js";
|
|
11
|
+
export { formatSignature, parseSignature, signedFieldsOf } from "./sig.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Outcome } from './outcome.ts';
|
|
2
|
+
export declare const PROTOCOL: "bound-lock/1";
|
|
3
|
+
export interface Line {
|
|
4
|
+
readonly version: string;
|
|
5
|
+
readonly fields: Readonly<Record<string, string>>;
|
|
6
|
+
}
|
|
7
|
+
export declare function byBytes(a: string, b: string): number;
|
|
8
|
+
export declare function canonical(fields: Readonly<Record<string, string>>, version?: string): string;
|
|
9
|
+
export declare function signedBytes(fields: Readonly<Record<string, string>>, version?: string): string;
|
|
10
|
+
export declare function parse(text: string): Outcome<Line>;
|