@weaveprotocol/cli 0.2.2 → 0.2.4

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 +116 -14
  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
 
@@ -12883,7 +12942,8 @@ var app = {
12883
12942
  title: { type: "string", minLength: 1, maxLength: 100 },
12884
12943
  description: { type: "string", maxLength: 1e3 },
12885
12944
  needs: { type: "array", minItems: 1, maxItems: MAX_APP_COLLECTIONS, items: { type: "object" } },
12886
- from: { type: "string", maxLength: 300 }
12945
+ from: { type: "string", maxLength: 300 },
12946
+ updates: { type: "string", minLength: 1, maxLength: 100 }
12887
12947
  },
12888
12948
  required: ["title", "needs"]
12889
12949
  },
@@ -12897,6 +12957,9 @@ function checkApp(value) {
12897
12957
  if (body.description !== void 0 && (typeof body.description !== "string" || body.description.length > 1e3)) {
12898
12958
  return "An app's description is at most 1000 characters";
12899
12959
  }
12960
+ if (body.updates !== void 0 && (typeof body.updates !== "string" || !body.updates || body.updates.length > 100)) {
12961
+ return `An app's "updates" is the key of the app it replaces`;
12962
+ }
12900
12963
  if (!Array.isArray(body.needs) || body.needs.length === 0 || body.needs.length > MAX_APP_COLLECTIONS) {
12901
12964
  return `An app needs 1\u2013${MAX_APP_COLLECTIONS} collections`;
12902
12965
  }
@@ -12927,7 +12990,8 @@ function essence(definition) {
12927
12990
  links: canonicalize(definition.links ?? {}),
12928
12991
  permissions: canonicalize([...definition.permissions ?? []].sort()),
12929
12992
  rules: canonicalize(definition.rules ?? {}),
12930
- screen: definition.screen ?? ""
12993
+ screen: definition.screen ?? "",
12994
+ network: canonicalize([...definition.network ?? []].sort())
12931
12995
  };
12932
12996
  }
12933
12997
  var fieldNames = (schema) => Object.keys(schema?.properties ?? {});
@@ -12946,12 +13010,17 @@ function differences(held, wanted) {
12946
13010
  if (a.links !== b.links) out.push("changes what it points at");
12947
13011
  if (a.history !== b.history) out.push(b.history === "all" ? "starts keeping every version" : "stops keeping old versions");
12948
13012
  if (a.screen !== b.screen) out.push(!b.screen ? "takes its screen away" : !a.screen ? "gives it a screen" : "changes its screen");
13013
+ if (a.network !== b.network) {
13014
+ const before2 = new Set(held.network ?? []);
13015
+ const reached = (wanted.network ?? []).filter((origin) => !before2.has(origin));
13016
+ out.push(reached.length ? `lets its screen reach ${reached.join(", ")}` : "lets its screen reach fewer places");
13017
+ }
12949
13018
  if (a.title !== b.title || a.description !== b.description) out.push("renames or redescribes it");
12950
13019
  return out;
12951
13020
  }
12952
13021
  function appScreen(body) {
12953
13022
  const found = body.needs.find((need) => typeof need.screen === "string" && need.screen.trim());
12954
- return found ? { collection: found.name, screen: found.screen } : null;
13023
+ return found ? { collection: found.name, screen: found.screen, network: found.network ?? [] } : null;
12955
13024
  }
12956
13025
  function reviewApp(body, collections) {
12957
13026
  const problem = checkApp(body);
@@ -12966,9 +13035,25 @@ function reviewApp(body, collections) {
12966
13035
  });
12967
13036
  return { needs, added: needs.every((need) => need.status === "same"), problem: null };
12968
13037
  }
13038
+ function supersededApps(records, collections) {
13039
+ const replaces = /* @__PURE__ */ new Map();
13040
+ for (const record of records) {
13041
+ if (record.body?.updates && record.body.updates !== record.key) replaces.set(record.key, record.body.updates);
13042
+ }
13043
+ const superseded = /* @__PURE__ */ new Set();
13044
+ for (const record of records) {
13045
+ if (!record.body || !reviewApp(record.body, collections).added) continue;
13046
+ for (let key3 = replaces.get(record.key); key3 && !superseded.has(key3) && key3 !== record.key; key3 = replaces.get(key3)) superseded.add(key3);
13047
+ }
13048
+ return superseded;
13049
+ }
12969
13050
  async function proposeApp(node, spaceId, body) {
12970
13051
  const problem = checkApp(body);
12971
13052
  if (problem) throw new Error(problem);
13053
+ if (body.updates) {
13054
+ const replaced = await node.records.get(spaceId, body.updates);
13055
+ if (!replaced || replaced.collection !== app.name) throw new Error(`"updates" names no app in this space: ${body.updates}`);
13056
+ }
12972
13057
  return node.records.put(spaceId, app.name, body);
12973
13058
  }
12974
13059
 
@@ -12977,6 +13062,12 @@ var SCREEN_GUIDE = `A screen is one HTML document (scripts and styles inline, at
12977
13062
  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
13063
  images from URLs, no fonts from URLs), no storage, no popups, no forms that submit. Keep state in records, never in the page.
12979
13064
 
13065
+ If it truly needs the network \u2014 weather, a map, a timetable \u2014 give its collection "network": the exact origins it may
13066
+ fetch from and load images from, like ["https://api.open-meteo.com"] (https:// or wss://, a host, no path; at most 8).
13067
+ Nothing else is reachable, and fonts never are. Each origin is named when people review the app, and each person is asked
13068
+ before a screen they open gets it: a person who says no gets the screen sealed, so it must still work without them.
13069
+ Ask for no more than it needs.
13070
+
12980
13071
  Inside it, window.weave is:
12981
13072
  weave.me { did, name } \u2014 who is looking
12982
13073
  weave.collections the collection names this screen may use (its app's)
@@ -13179,7 +13270,12 @@ var NODE_ACTIONS = Object.freeze([
13179
13270
  type: "object",
13180
13271
  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
13272
  },
13182
- screen: { type: "string", description: "Optional: its own screen, one HTML document \u2014 read apps_screen_guide first" }
13273
+ screen: { type: "string", description: "Optional: its own screen, one HTML document \u2014 read apps_screen_guide first" },
13274
+ network: {
13275
+ type: "array",
13276
+ items: { type: "string" },
13277
+ 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.'
13278
+ }
13183
13279
  },
13184
13280
  required: ["space", "name", "schema"]
13185
13281
  },
@@ -13196,14 +13292,15 @@ var NODE_ACTIONS = Object.freeze([
13196
13292
  ...typeof input.links === "object" && input.links !== null ? { links: input.links } : {},
13197
13293
  ...Array.isArray(input.permissions) ? { permissions: input.permissions.filter((p) => typeof p === "string") } : {},
13198
13294
  ...typeof input.rules === "object" && input.rules !== null ? { rules: input.rules } : {},
13199
- ...typeof input.screen === "string" ? { screen: input.screen } : {}
13295
+ ...typeof input.screen === "string" ? { screen: input.screen } : {},
13296
+ ...Array.isArray(input.network) ? { network: input.network.filter((o) => typeof o === "string") } : {}
13200
13297
  });
13201
13298
  return { ...defined, summary: describeCollection({ ...defined, schema: defined.schema ?? void 0 }) };
13202
13299
  }
13203
13300
  },
13204
13301
  {
13205
13302
  name: "apps_list",
13206
- description: 'The apps proposed in a space: each with its title, who proposed it (viaAgent when an agent did), and for every collection it needs whether it is new, already there, or would change one \u2014 with what it allows, worked out from its rules. "added" is true once a person has added it.',
13303
+ description: 'The apps proposed in a space: each with its title, who proposed it (viaAgent when an agent did), and for every collection it needs whether it is new, already there, or would change one \u2014 with what it allows, worked out from its rules. "added" is true once a person has added it. "updates" names the app it is a new version of; "superseded" is true once a newer version of it has been added, so it is only history.',
13207
13304
  input: { type: "object", properties: { space }, required: ["space"] },
13208
13305
  readOnly: true,
13209
13306
  peerContent: true,
@@ -13211,6 +13308,7 @@ var NODE_ACTIONS = Object.freeze([
13211
13308
  const spaceId = str(input, "space");
13212
13309
  const collections = await node.collections.list(spaceId);
13213
13310
  const found = await node.records.list(spaceId, { collection: app.name });
13311
+ const superseded = supersededApps(found, collections);
13214
13312
  return found.map((record) => {
13215
13313
  const review = record.body ? reviewApp(record.body, collections) : null;
13216
13314
  return {
@@ -13220,7 +13318,9 @@ var NODE_ACTIONS = Object.freeze([
13220
13318
  proposedBy: record.createdBy,
13221
13319
  ...record.viaAgent ? { viaAgent: true } : {},
13222
13320
  ...record.body && appScreen(record.body) ? { screen: appScreen(record.body).collection } : {},
13321
+ ...record.body?.updates ? { updates: record.body.updates } : {},
13223
13322
  added: review?.added ?? false,
13323
+ superseded: superseded.has(record.key),
13224
13324
  problem: review?.problem ?? (record.body ? null : "It could not be read"),
13225
13325
  needs: review?.needs.map(({ definition, status, summary, changes }) => ({ name: definition.name, status, summary, changes })) ?? []
13226
13326
  };
@@ -13229,20 +13329,21 @@ var NODE_ACTIONS = Object.freeze([
13229
13329
  },
13230
13330
  {
13231
13331
  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).`,
13332
+ 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
13333
  input: { type: "object", properties: {} },
13234
13334
  readOnly: true,
13235
13335
  run: async () => SCREEN_GUIDE
13236
13336
  },
13237
13337
  {
13238
13338
  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.`,
13339
+ 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. To change an app that is already here, read apps_list and pass the key of its newest version, the one not superseded, as "updates": the change then shows as an update to it, and once it is added the old version is not offered again. 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
13340
  input: {
13241
13341
  type: "object",
13242
13342
  properties: {
13243
13343
  space,
13244
13344
  title: { type: "string", description: 'What people will call it, e.g. "Carpool"' },
13245
13345
  description: { type: "string", description: "What it is for, in a sentence" },
13346
+ updates: { type: "string", description: "The key of the app this is a new version of, from apps_list" },
13246
13347
  needs: {
13247
13348
  type: "array",
13248
13349
  description: "Collection definitions, as collections_define takes them \u2014 without version",
@@ -13257,6 +13358,7 @@ var NODE_ACTIONS = Object.freeze([
13257
13358
  const body = {
13258
13359
  title: str(input, "title"),
13259
13360
  ...typeof input.description === "string" ? { description: input.description } : {},
13361
+ ...typeof input.updates === "string" ? { updates: input.updates } : {},
13260
13362
  needs: input.needs
13261
13363
  };
13262
13364
  const problem = checkApp(body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weaveprotocol/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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": {