@weaveprotocol/cli 0.2.2 → 0.2.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.
- package/dist/weave.mjs +89 -12
- package/package.json +1 -1
package/dist/weave.mjs
CHANGED
|
@@ -6894,6 +6894,21 @@ function toJsonSchema(schema) {
|
|
|
6894
6894
|
return json;
|
|
6895
6895
|
}
|
|
6896
6896
|
var MAX_SCREEN_BYTES = 48 * 1024;
|
|
6897
|
+
var MAX_SCREEN_ORIGINS = 8;
|
|
6898
|
+
var SCREEN_ORIGIN = /^(https|wss):\/\/[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+(:[0-9]{1,5})?$/;
|
|
6899
|
+
function checkScreenNetwork(network, hasScreen) {
|
|
6900
|
+
if (network === void 0) return null;
|
|
6901
|
+
if (!Array.isArray(network)) return 'network must be a list of origins, like ["https://api.example.com"]';
|
|
6902
|
+
if (!hasScreen && network.length) return "network is where a screen may connect; this definition has no screen";
|
|
6903
|
+
if (network.length > MAX_SCREEN_ORIGINS) return `network may name at most ${MAX_SCREEN_ORIGINS} origins`;
|
|
6904
|
+
for (const origin of network) {
|
|
6905
|
+
if (typeof origin !== "string" || !SCREEN_ORIGIN.test(origin)) {
|
|
6906
|
+
return `"${String(origin)}" is not an origin a screen may reach: give https:// or wss:// and a host, lower case, no path \u2014 like "https://api.example.com"`;
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
if (new Set(network).size !== network.length) return "network names an origin twice";
|
|
6910
|
+
return null;
|
|
6911
|
+
}
|
|
6897
6912
|
var CATALOG_COLLECTION = "sys.collection";
|
|
6898
6913
|
var KEYWORDS = /* @__PURE__ */ new Set([
|
|
6899
6914
|
"type",
|
|
@@ -7030,6 +7045,8 @@ function checkStoredCollection(definition) {
|
|
|
7030
7045
|
const bytes = new TextEncoder().encode(d.screen).length;
|
|
7031
7046
|
if (bytes > MAX_SCREEN_BYTES) return `screen is ${Math.ceil(bytes / 1024)} KB; at most ${MAX_SCREEN_BYTES / 1024} KB`;
|
|
7032
7047
|
}
|
|
7048
|
+
const network = checkScreenNetwork(d.network, typeof d.screen === "string");
|
|
7049
|
+
if (network) return network;
|
|
7033
7050
|
return checkPublishableSchema(d.schema);
|
|
7034
7051
|
}
|
|
7035
7052
|
function validateJsonSchema(schema, value) {
|
|
@@ -7491,6 +7508,7 @@ function createRTCTransport(config) {
|
|
|
7491
7508
|
}
|
|
7492
7509
|
|
|
7493
7510
|
// ../core/src/network/peer-auth.ts
|
|
7511
|
+
var MAX_EARLIER_READ_KEYS = 4;
|
|
7494
7512
|
function readAccessOf(read) {
|
|
7495
7513
|
if (!read) return null;
|
|
7496
7514
|
if ("current" in read) return read;
|
|
@@ -7501,16 +7519,22 @@ async function proveRead(access, label, provider) {
|
|
|
7501
7519
|
const key3 = await access.key();
|
|
7502
7520
|
if (!key3) return null;
|
|
7503
7521
|
const read = base64UrlEncode(await provider.sign(key3.privateKey, label));
|
|
7504
|
-
if (key3.did
|
|
7522
|
+
if (!key3.did) return { read };
|
|
7505
7523
|
const member = await access.membership?.() ?? null;
|
|
7506
|
-
|
|
7524
|
+
const older = (await access.earlier?.() ?? []).filter((held) => held.did !== key3.did).slice(0, MAX_EARLIER_READ_KEYS);
|
|
7525
|
+
const earlier = await Promise.all(older.map(async (held) => ({ readKey: held.did, read: base64UrlEncode(await provider.sign(held.privateKey, label)) })));
|
|
7526
|
+
return { read, readKey: key3.did, ...member ? { member } : {}, ...earlier.length ? { earlier } : {} };
|
|
7507
7527
|
}
|
|
7508
7528
|
async function checkRead(access, peerDid, label, proof, provider) {
|
|
7509
7529
|
const current = access.current();
|
|
7510
7530
|
const claimed = typeof proof.readKey === "string" ? proof.readKey : current;
|
|
7511
|
-
if (
|
|
7512
|
-
|
|
7513
|
-
|
|
7531
|
+
if (claimed.startsWith("did:key:") && await verifyBy(provider, claimed, proof.read, label)) {
|
|
7532
|
+
if (claimed === current) return true;
|
|
7533
|
+
if (access.admits && await access.admits(peerDid, claimed, proof.member)) return true;
|
|
7534
|
+
}
|
|
7535
|
+
const earlier = Array.isArray(proof.earlier) ? proof.earlier.slice(0, MAX_EARLIER_READ_KEYS) : [];
|
|
7536
|
+
const ours = earlier.find((item) => item?.readKey === current);
|
|
7537
|
+
return ours !== void 0 && current.startsWith("did:key:") && verifyBy(provider, current, ours.read, label);
|
|
7514
7538
|
}
|
|
7515
7539
|
function peerNonce() {
|
|
7516
7540
|
return base64UrlEncode(globalThis.crypto.getRandomValues(new Uint8Array(16)));
|
|
@@ -9463,6 +9487,7 @@ async function openSpaceRuntime(deps) {
|
|
|
9463
9487
|
rules: definition?.rules ?? {},
|
|
9464
9488
|
topics: definition?.topics ?? [],
|
|
9465
9489
|
...definition?.screen !== void 0 ? { screen: definition.screen } : {},
|
|
9490
|
+
...definition?.network?.length ? { network: definition.network } : {},
|
|
9466
9491
|
records
|
|
9467
9492
|
});
|
|
9468
9493
|
}
|
|
@@ -9702,6 +9727,12 @@ async function openSpaceRuntime(deps) {
|
|
|
9702
9727
|
return held ? deriveReadKey(held, provider) : record.read ?? null;
|
|
9703
9728
|
},
|
|
9704
9729
|
current: () => currentReadKey,
|
|
9730
|
+
async earlier() {
|
|
9731
|
+
const { current } = (await access()).history;
|
|
9732
|
+
const newest = newestKey(current);
|
|
9733
|
+
const older = [...current.keys].reverse().map((epoch) => keyring.get(epoch.keyId)).filter((held) => held !== void 0 && held !== newest);
|
|
9734
|
+
return Promise.all(older.slice(0, MAX_EARLIER_READ_KEYS).map((held) => deriveReadKey(held, provider)));
|
|
9735
|
+
},
|
|
9705
9736
|
async membership() {
|
|
9706
9737
|
const held = newestKey((await access()).history.current);
|
|
9707
9738
|
return held ? sealWith(held, session.proof(), membershipContext(space2.id)) : null;
|
|
@@ -10267,7 +10298,8 @@ async function openSpaceRuntime(deps) {
|
|
|
10267
10298
|
...input.permissions !== void 0 ? { permissions: input.permissions } : {},
|
|
10268
10299
|
...input.rules !== void 0 ? { rules: input.rules } : {},
|
|
10269
10300
|
...input.topics !== void 0 ? { topics: input.topics } : {},
|
|
10270
|
-
...input.screen !== void 0 ? { screen: input.screen } : {}
|
|
10301
|
+
...input.screen !== void 0 ? { screen: input.screen } : {},
|
|
10302
|
+
...input.network?.length ? { network: input.network } : {}
|
|
10271
10303
|
};
|
|
10272
10304
|
const problem = checkStoredCollection(definition);
|
|
10273
10305
|
if (problem) throw new Error(problem);
|
|
@@ -10745,8 +10777,21 @@ function checkNotify(when) {
|
|
|
10745
10777
|
}
|
|
10746
10778
|
}
|
|
10747
10779
|
if (typeof w.since !== "string" || !Number.isFinite(Date.parse(w.since))) return "since must be a date";
|
|
10780
|
+
if (w.app !== void 0) {
|
|
10781
|
+
const a = w.app;
|
|
10782
|
+
if (typeof a !== "object" || a === null || typeof a.origin !== "string" || !isOrigin(a.origin)) return "app.origin must be a web origin";
|
|
10783
|
+
if (a.name !== void 0 && (typeof a.name !== "string" || a.name.length > 80)) return "app.name must be text, at most 80 characters";
|
|
10784
|
+
}
|
|
10748
10785
|
return null;
|
|
10749
10786
|
}
|
|
10787
|
+
function isOrigin(value) {
|
|
10788
|
+
if (/^(chrome|moz|safari-web)-extension:\/\/[a-z0-9-]{1,64}$/i.test(value)) return true;
|
|
10789
|
+
try {
|
|
10790
|
+
return new URL(value).origin === value;
|
|
10791
|
+
} catch {
|
|
10792
|
+
return false;
|
|
10793
|
+
}
|
|
10794
|
+
}
|
|
10750
10795
|
async function carriedFor(when, spaces) {
|
|
10751
10796
|
let tags;
|
|
10752
10797
|
if (when.topic) {
|
|
@@ -12605,6 +12650,16 @@ async function createCarryCore(config) {
|
|
|
12605
12650
|
await refreshing;
|
|
12606
12651
|
return [...carries.values()].flatMap((entry) => [...entry.subscriptions].map(([id, sub]) => view(id, sub)));
|
|
12607
12652
|
},
|
|
12653
|
+
async collections(spaceId) {
|
|
12654
|
+
const entry = carried.get(spaceId);
|
|
12655
|
+
if (!entry || carries.has(spaceId)) return [];
|
|
12656
|
+
return (await entry.runtime.collections()).map((found) => ({
|
|
12657
|
+
name: found.name,
|
|
12658
|
+
...found.title ? { title: found.title } : {},
|
|
12659
|
+
topics: found.topics,
|
|
12660
|
+
records: found.records
|
|
12661
|
+
}));
|
|
12662
|
+
},
|
|
12608
12663
|
async spaces() {
|
|
12609
12664
|
const found = [];
|
|
12610
12665
|
for (const [spaceId, entry] of carried) {
|
|
@@ -12868,6 +12923,10 @@ function describeCollection(definition) {
|
|
|
12868
12923
|
sentences.push(`Roles in the space can be given permission to ${list}.`);
|
|
12869
12924
|
}
|
|
12870
12925
|
if (definition.history === "all") sentences.push(`Every earlier version of ${a} is kept.`);
|
|
12926
|
+
if (definition.network?.length) {
|
|
12927
|
+
const hosts = joinAnd(definition.network.map((origin) => origin.replace(/^[a-z]+:\/\//, "")));
|
|
12928
|
+
sentences.push(`Its screen can connect to ${hosts}, and send there anything the person looking can see in it. Each person is asked first.`);
|
|
12929
|
+
}
|
|
12871
12930
|
return sentences;
|
|
12872
12931
|
}
|
|
12873
12932
|
|
|
@@ -12927,7 +12986,8 @@ function essence(definition) {
|
|
|
12927
12986
|
links: canonicalize(definition.links ?? {}),
|
|
12928
12987
|
permissions: canonicalize([...definition.permissions ?? []].sort()),
|
|
12929
12988
|
rules: canonicalize(definition.rules ?? {}),
|
|
12930
|
-
screen: definition.screen ?? ""
|
|
12989
|
+
screen: definition.screen ?? "",
|
|
12990
|
+
network: canonicalize([...definition.network ?? []].sort())
|
|
12931
12991
|
};
|
|
12932
12992
|
}
|
|
12933
12993
|
var fieldNames = (schema) => Object.keys(schema?.properties ?? {});
|
|
@@ -12946,12 +13006,17 @@ function differences(held, wanted) {
|
|
|
12946
13006
|
if (a.links !== b.links) out.push("changes what it points at");
|
|
12947
13007
|
if (a.history !== b.history) out.push(b.history === "all" ? "starts keeping every version" : "stops keeping old versions");
|
|
12948
13008
|
if (a.screen !== b.screen) out.push(!b.screen ? "takes its screen away" : !a.screen ? "gives it a screen" : "changes its screen");
|
|
13009
|
+
if (a.network !== b.network) {
|
|
13010
|
+
const before2 = new Set(held.network ?? []);
|
|
13011
|
+
const reached = (wanted.network ?? []).filter((origin) => !before2.has(origin));
|
|
13012
|
+
out.push(reached.length ? `lets its screen reach ${reached.join(", ")}` : "lets its screen reach fewer places");
|
|
13013
|
+
}
|
|
12949
13014
|
if (a.title !== b.title || a.description !== b.description) out.push("renames or redescribes it");
|
|
12950
13015
|
return out;
|
|
12951
13016
|
}
|
|
12952
13017
|
function appScreen(body) {
|
|
12953
13018
|
const found = body.needs.find((need) => typeof need.screen === "string" && need.screen.trim());
|
|
12954
|
-
return found ? { collection: found.name, screen: found.screen } : null;
|
|
13019
|
+
return found ? { collection: found.name, screen: found.screen, network: found.network ?? [] } : null;
|
|
12955
13020
|
}
|
|
12956
13021
|
function reviewApp(body, collections) {
|
|
12957
13022
|
const problem = checkApp(body);
|
|
@@ -12977,6 +13042,12 @@ var SCREEN_GUIDE = `A screen is one HTML document (scripts and styles inline, at
|
|
|
12977
13042
|
When the app is added, apps in the space can show it instead of plain lists. It runs sealed: no network (no fetch, no
|
|
12978
13043
|
images from URLs, no fonts from URLs), no storage, no popups, no forms that submit. Keep state in records, never in the page.
|
|
12979
13044
|
|
|
13045
|
+
If it truly needs the network \u2014 weather, a map, a timetable \u2014 give its collection "network": the exact origins it may
|
|
13046
|
+
fetch from and load images from, like ["https://api.open-meteo.com"] (https:// or wss://, a host, no path; at most 8).
|
|
13047
|
+
Nothing else is reachable, and fonts never are. Each origin is named when people review the app, and each person is asked
|
|
13048
|
+
before a screen they open gets it: a person who says no gets the screen sealed, so it must still work without them.
|
|
13049
|
+
Ask for no more than it needs.
|
|
13050
|
+
|
|
12980
13051
|
Inside it, window.weave is:
|
|
12981
13052
|
weave.me { did, name } \u2014 who is looking
|
|
12982
13053
|
weave.collections the collection names this screen may use (its app's)
|
|
@@ -13179,7 +13250,12 @@ var NODE_ACTIONS = Object.freeze([
|
|
|
13179
13250
|
type: "object",
|
|
13180
13251
|
description: 'What its records allow, enforced by every peer: { "create": "member", "edit": "creator", "delete": ["creator", "can:moderate"], "onePer": ["@author", "link:about"], "fixed": ["options"] }. Who is "member" (anyone holding a role here, the default), "creator" (whoever created that record) or "can:<permission>" (anyone whose role holds one of the permissions declared above). onePer makes at most one record per author + linked record (+ body field): writing again changes it. fixed fields keep their first value.'
|
|
13181
13252
|
},
|
|
13182
|
-
screen: { type: "string", description: "Optional: its own screen, one HTML document \u2014 read apps_screen_guide first" }
|
|
13253
|
+
screen: { type: "string", description: "Optional: its own screen, one HTML document \u2014 read apps_screen_guide first" },
|
|
13254
|
+
network: {
|
|
13255
|
+
type: "array",
|
|
13256
|
+
items: { type: "string" },
|
|
13257
|
+
description: 'Optional, with a screen: the exact origins it may connect to and load images from, like ["https://api.open-meteo.com"]. None keeps it sealed. Each is named when people review the app, and each person is asked before their screen gets them.'
|
|
13258
|
+
}
|
|
13183
13259
|
},
|
|
13184
13260
|
required: ["space", "name", "schema"]
|
|
13185
13261
|
},
|
|
@@ -13196,7 +13272,8 @@ var NODE_ACTIONS = Object.freeze([
|
|
|
13196
13272
|
...typeof input.links === "object" && input.links !== null ? { links: input.links } : {},
|
|
13197
13273
|
...Array.isArray(input.permissions) ? { permissions: input.permissions.filter((p) => typeof p === "string") } : {},
|
|
13198
13274
|
...typeof input.rules === "object" && input.rules !== null ? { rules: input.rules } : {},
|
|
13199
|
-
...typeof input.screen === "string" ? { screen: input.screen } : {}
|
|
13275
|
+
...typeof input.screen === "string" ? { screen: input.screen } : {},
|
|
13276
|
+
...Array.isArray(input.network) ? { network: input.network.filter((o) => typeof o === "string") } : {}
|
|
13200
13277
|
});
|
|
13201
13278
|
return { ...defined, summary: describeCollection({ ...defined, schema: defined.schema ?? void 0 }) };
|
|
13202
13279
|
}
|
|
@@ -13229,14 +13306,14 @@ var NODE_ACTIONS = Object.freeze([
|
|
|
13229
13306
|
},
|
|
13230
13307
|
{
|
|
13231
13308
|
name: "apps_screen_guide",
|
|
13232
|
-
description: `How to write a screen \u2014 an app's own HTML UI, kept on its main collection's definition as "screen" and run sealed in the app: what it can use (window.weave: list, put, update, remove, onChange, me) and what it can't (
|
|
13309
|
+
description: `How to write a screen \u2014 an app's own HTML UI, kept on its main collection's definition as "screen" and run sealed in the app: what it can use (window.weave: list, put, update, remove, onChange, me) and what it can't (storage, and the network beyond the origins it names).`,
|
|
13233
13310
|
input: { type: "object", properties: {} },
|
|
13234
13311
|
readOnly: true,
|
|
13235
13312
|
run: async () => SCREEN_GUIDE
|
|
13236
13313
|
},
|
|
13237
13314
|
{
|
|
13238
13315
|
name: "apps_propose",
|
|
13239
|
-
description: `Propose an app in a space: a new way for its people to work together \u2014 a carpool, a sign-up sheet, a decision log. Give it a title, a line on what it is for, and the collections it needs, each exactly as collections_define takes it (name, title, description, schema, links, permissions, rules \u2014 no version). Nothing is defined yet: everyone in the space sees the proposal, with what it allows worked out from its rules, and a person who may define collections adds it. Read collections_list first and reuse what the space already has (std.poll, std.task\u2026) rather than inventing a twin. For anything plain lists and forms can't show \u2014 a game board, a calendar, a whiteboard \u2014 give the main collection a "screen": its own HTML UI (one document, inline scripts and styles, no network). Inside it, use exactly: weave.me ({ did, name }), await weave.list("<collection>", { where: { "link:<rel>": key } }), await weave.put("<collection>", body, { links: [{ rel, to: key }] }), await weave.update(key, body), await weave.remove(key), weave.onChange(redraw). Records are { key, body, links, createdBy, mine }. Read apps_screen_guide for the rest. Returns what each collection will allow; tell the person that, not your own description.`,
|
|
13316
|
+
description: `Propose an app in a space: a new way for its people to work together \u2014 a carpool, a sign-up sheet, a decision log. Give it a title, a line on what it is for, and the collections it needs, each exactly as collections_define takes it (name, title, description, schema, links, permissions, rules \u2014 no version). Nothing is defined yet: everyone in the space sees the proposal, with what it allows worked out from its rules, and a person who may define collections adds it. Read collections_list first and reuse what the space already has (std.poll, std.task\u2026) rather than inventing a twin. For anything plain lists and forms can't show \u2014 a game board, a calendar, a whiteboard \u2014 give the main collection a "screen": its own HTML UI (one document, inline scripts and styles, no network unless the collection names exact origins in "network"). Inside it, use exactly: weave.me ({ did, name }), await weave.list("<collection>", { where: { "link:<rel>": key } }), await weave.put("<collection>", body, { links: [{ rel, to: key }] }), await weave.update(key, body), await weave.remove(key), weave.onChange(redraw). Records are { key, body, links, createdBy, mine }. Read apps_screen_guide for the rest. Returns what each collection will allow; tell the person that, not your own description.`,
|
|
13240
13317
|
input: {
|
|
13241
13318
|
type: "object",
|
|
13242
13319
|
properties: {
|