@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.
- package/dist/weave.mjs +116 -14
- 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
|
|
|
@@ -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 (
|
|
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);
|