@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.
Files changed (2) hide show
  1. package/dist/weave.mjs +89 -12
  2. 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 === access.current()) return { read };
7522
+ if (!key3.did) return { read };
7505
7523
  const member = await access.membership?.() ?? null;
7506
- return { read, readKey: key3.did, ...member ? { member } : {} };
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 (!claimed.startsWith("did:key:") || !await verifyBy(provider, claimed, proof.read, label)) return false;
7512
- if (claimed === current) return true;
7513
- return access.admits ? access.admits(peerDid, claimed, proof.member) : false;
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 (network, storage).`,
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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weaveprotocol/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "The Weave protocol from a terminal: manage spaces, run an always-on node, serve MCP to agents",
5
5
  "type": "module",
6
6
  "bin": {