toolaby 1.8.1 → 1.8.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/bin.mjs CHANGED
@@ -44,7 +44,7 @@ import { runMcp } from './lib/mcp.js';
44
44
  import { runCheck } from './lib/check.js';
45
45
  import { inside, signInPage, devicePolling, opener, privateKind, privateContent, privateCopies, googleKey, projectRoot, gitDirectory, gitObject, gitObjects, gitFile, notAnImage, unpacked, COMPRESSED, publicLinks, privateLinks, oneLine, fetchBounded, fetchHere, readBounded, readText, SCAFFOLD_FILES } from './lib/safe.js';
46
46
 
47
- const VERSION = '1.8.1';
47
+ const VERSION = '1.8.3';
48
48
  const CLIENT_ID = 'toolaby-cli';
49
49
  const argv = process.argv.slice(2);
50
50
  // `--version` and `--help` are commands of their own; every other command is the first bare word.
@@ -312,7 +312,7 @@ async function wire() {
312
312
  // The background the manifest names, read from inside the folder only: whether it starts the Wall, or is the Wall's.
313
313
  const namedWorker = workerIn(manifest) ?? '';
314
314
  const workerSource = namedWorker && has(namedWorker) && !inside(dir, namedWorker).why ? readOr(join(dir, namedWorker), '') : null;
315
- const wiring = project.manifestFile && manifest ? wireManifest(manifest, { site, has, remembered, rememberedSidePanel, manifestKey, ownInFront, workerSource }) : null;
315
+ const wiring = project.manifestFile && manifest ? wireManifest(manifest, { site, apex: new URL(wall).host, has, remembered, rememberedSidePanel, manifestKey, ownInFront, workerSource }) : null;
316
316
  const appPopup = wiring ? wiring.appPopup : project.wrapper ? (project.appPopup ?? remembered) : null;
317
317
  const appSidePanel = wiring ? wiring.appSidePanel : project.wrapper ? (project.appSidePanel ?? rememberedSidePanel) : null;
318
318
  const targets = filesToWrite(project, appPopup, appSidePanel, { popup: !!wiring?.ownPopup, sidePanel: !!wiring?.ownSidePanel });
@@ -743,6 +743,15 @@ function pack() {
743
743
  const hosts = manifest.host_permissions ?? []; const ec = manifest.externally_connectable?.matches ?? [];
744
744
  if (!hosts.includes(sitePattern(site)) || !ec.includes(sitePattern(site))) problems.push(`The manifest does not name ${oneLine(sitePattern(site))} in both host_permissions and externally_connectable: sign-in and the purchase would not reach the extension. \`npx -y toolaby@latest wire <tool-id> --live --force\` adds them.`);
745
745
  }
746
+ // A host under the Wall's address that no key in the build names: another workspace's, or the other side's Test
747
+ // address, left by an earlier wiring or pasted by hand. The store build asks its buyers for it at install, and gains
748
+ // nothing: the extension talks to its own key's host alone (30 Sep 2026).
749
+ const walls = [...new Set(['toolaby.app', new URL(wall).hostname])];
750
+ const hostOf = (m) => /^(?:\*|https?|wss?):\/\/([^/]+)\//.exec(m)?.[1]?.replace(/^\*\./, '').toLowerCase() ?? null;
751
+ const keys = new Set([...sites].map(sitePattern));
752
+ const strangers = [...new Set([...(manifest.host_permissions ?? []), ...(manifest.externally_connectable?.matches ?? [])])]
753
+ .filter((m) => typeof m === 'string' && !keys.has(m) && walls.some((w) => { const h = hostOf(m); return !!h && (h === w || h.endsWith(`.${w}`)); }));
754
+ if (strangers.length && !flag('--allow-wall-hosts')) problems.push(`The manifest names ${strangers.map(oneLine).join(', ')}: the Wall's, and no key's in this build — another workspace, or the other side's Test address. A store build asks its buyers for ${strangers.length > 1 ? 'these' : 'it'} at install for nothing: take ${strangers.length > 1 ? 'them' : 'it'} out of host_permissions and externally_connectable. (--allow-wall-hosts packs it anyway.)`);
746
755
  if (problems.length) fail(`Not packed:\n\n${problems.map((p) => ` · ${p}`).join('\n\n')}`);
747
756
 
748
757
  const hadKey = typeof manifest.key === 'string';
@@ -785,7 +794,7 @@ function mcp() {
785
794
  }
786
795
 
787
796
  async function check() {
788
- const passed = await runCheck({ mark: (tone, text) => say(` ${tone === 'ok' ? green('✓') : tone === 'warn' ? yellow('!') : red('✗')} ${plain(text)}`), dir: opt('--into') ?? positional[0] ?? '.', out, api: (path, o = {}) => api(path, o), token: storedToken, detectProject, wireManifest, listTree, readJson, browser: flag('--browser') });
797
+ const passed = await runCheck({ mark: (tone, text) => say(` ${tone === 'ok' ? green('✓') : tone === 'warn' ? yellow('!') : red('✗')} ${plain(text)}`), dir: opt('--into') ?? positional[0] ?? '.', out, api: (path, o = {}) => api(path, o), token: storedToken, detectProject, wireManifest: (m, o) => wireManifest(m, { apex: new URL(wall).host, ...o }), listTree, readJson, browser: flag('--browser') });
789
798
  if (!passed) process.exit(1);
790
799
  }
791
800
 
@@ -804,7 +813,8 @@ function help() {
804
813
  --surfaces both|popup|side-panel|none what it opens (default: both)
805
814
  tools your tools, with their ids
806
815
  pack [folder] the zip for the Chrome Web Store: the built extension, its manifest's "key" left out;
807
- refuses a Test key or localhost inside --out <file.zip>
816
+ refuses a Test key, localhost, or another of the Wall's hosts inside
817
+ --out <file.zip>
808
818
  leaves out keys, certificates, .env and credential files, source maps, secrets,
809
819
  git's files, a build's copy of a private file (compressed, UTF-16 or base64),
810
820
  an image that is none; refuses a build if one is a file the manifest names
package/lib/manifest.js CHANGED
@@ -1,4 +1,5 @@
1
1
  // Generated from wall/src/packages/wiring/manifest.ts by wall/scripts/pack-create.mjs — edit there.
2
+ import { isTestAddress } from './test-address.js';
2
3
  /**
3
4
  * What wiring an extension does to its manifest — the same in the browser
4
5
  * (Wire my extension, from the tool's page) and in the terminal
@@ -70,9 +71,9 @@ export const loadableName = (file) => {
70
71
  /** The background the manifest names, as the folder spells it: "./background.js" is background.js. */
71
72
  export const namedWorker = (manifest) => typeof manifest?.background?.service_worker === 'string' ? manifest.background.service_worker.trim().replace(/^(\.\/)+/, '') || null : null;
72
73
  /**
73
- * The match pattern a manifest names the Wall by: the origin of the key's site, whatever path follows it. Test shares
74
- * Live's hosts under /test (`https://northwind.toolaby.app/test`), so one pattern — `https://northwind.toolaby.app/*` —
75
- * lets the same build reach either side, and a Live wiring's manifest is the one it always was.
74
+ * The match pattern a manifest names the Wall by: the origin of the key's site, whatever path follows it —
75
+ * `https://northwind.toolaby.app/*` on Live; on Test, the address the Wall made for the workspace
76
+ * (`https://quiet-otter-4172.toolaby.app/*`, ./test-address). A wiring to one side takes the other's Test address out.
76
77
  */
77
78
  export function sitePattern(site) {
78
79
  try {
@@ -124,6 +125,26 @@ export function wireManifest(input, o) {
124
125
  added.push('storage');
125
126
  }
126
127
  const pattern = sitePattern(o.site);
128
+ // A Test address of the Wall's that is not this key's (./test-address): left by an earlier wiring to the other side.
129
+ // Test's workspaces have hosts of their own, and a build wired for Test and then for Live went on asking its buyers,
130
+ // at install, for access to Test's host too (29 Sep 2026). Only under the Wall's own address: a host of the
131
+ // developer's that reads like one, on a domain of theirs, was taken out of their build too (30 Sep 2026).
132
+ const apex = (o.apex ?? host.split('.').slice(1).join('.')).toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
133
+ const underWall = new RegExp(`^https://([a-z0-9-]+)\\.${apex}/\\*$`);
134
+ const stale = (p) => typeof p === 'string' && p !== pattern && isTestAddress(underWall.exec(p)?.[1]);
135
+ for (const field of ['host_permissions', 'externally_connectable']) {
136
+ const list = field === 'host_permissions' ? manifest.host_permissions : manifest.externally_connectable?.matches;
137
+ const gone = Array.isArray(list) ? list.filter(stale) : [];
138
+ if (!gone.length)
139
+ continue;
140
+ const kept = list.filter((p) => !stale(p));
141
+ if (field === 'host_permissions')
142
+ manifest.host_permissions = kept;
143
+ else
144
+ manifest.externally_connectable = { ...manifest.externally_connectable, matches: kept };
145
+ for (const p of gone)
146
+ did.push(`${field === 'host_permissions' ? 'the host permission' : 'externally_connectable'} for ${p.slice('https://'.length, -2)} taken out: a Test address that is not this key's`);
147
+ }
127
148
  if (!(Array.isArray(manifest.host_permissions) && manifest.host_permissions.includes(pattern))) {
128
149
  manifest.host_permissions = add(manifest.host_permissions, pattern);
129
150
  did.push(`a host permission for ${host}`);
@@ -0,0 +1,43 @@
1
+ // Generated from wall/src/packages/wiring/test-address.ts by wall/scripts/pack-create.mjs — edit there.
2
+ /**
3
+ * A Test workspace's address, as Clerk gives a development instance one (`<slug>.clerk.accounts.dev`): made by the Wall,
4
+ * never chosen, never changed — `quiet-otter-4172.toolaby.app/test`. Test and Live share the Wall's addresses, and a
5
+ * name is Live's: a Test workspace served under its chosen name sat at the address that name has on Live, which
6
+ * someone else could hold (29 Sep 2026). A Test address is of a shape no workspace may take as its name, on either side
7
+ * (isReservedSlug), so no Live workspace ever answers at one.
8
+ *
9
+ * The database computes it, from the workspace's id, which never changes (the column `test_address`, generated; its
10
+ * migration writes testAddressSql() out as it stands here). So neither list may ever change: every Test workspace's
11
+ * address, and every Test key that names one, would move.
12
+ */
13
+ export const TEST_ADDRESS_ADJECTIVES = [
14
+ 'amber', 'bold', 'brave', 'bright', 'brisk', 'calm', 'clever', 'cosmic', 'crisp', 'curious', 'daring', 'eager', 'early', 'fancy', 'fluffy', 'gentle',
15
+ 'glad', 'golden', 'happy', 'hardy', 'honest', 'humble', 'jolly', 'keen', 'kind', 'lively', 'lucky', 'lunar', 'mellow', 'merry', 'mighty', 'misty',
16
+ 'modest', 'nimble', 'noble', 'patient', 'plucky', 'polite', 'proud', 'quick', 'quiet', 'rapid', 'ready', 'rosy', 'rustic', 'shiny', 'silent', 'silver',
17
+ 'smooth', 'snowy', 'solar', 'spry', 'steady', 'sturdy', 'sunny', 'swift', 'tidy', 'tranquil', 'trusty', 'vivid', 'warm', 'wise', 'witty', 'zesty',
18
+ ];
19
+ export const TEST_ADDRESS_ANIMALS = [
20
+ 'badger', 'beaver', 'bison', 'bobcat', 'camel', 'cheetah', 'crane', 'dingo', 'dolphin', 'eagle', 'falcon', 'ferret', 'finch', 'fox', 'gazelle', 'gecko',
21
+ 'heron', 'hippo', 'ibis', 'jaguar', 'koala', 'lemur', 'leopard', 'llama', 'lynx', 'magpie', 'marmot', 'marten', 'meerkat', 'mink', 'moose', 'narwhal',
22
+ 'newt', 'ocelot', 'orca', 'osprey', 'otter', 'owl', 'panda', 'panther', 'parrot', 'pelican', 'penguin', 'puffin', 'quail', 'rabbit', 'raven', 'robin',
23
+ 'salmon', 'seal', 'sparrow', 'stork', 'swan', 'tapir', 'tiger', 'toucan', 'turtle', 'walrus', 'weasel', 'whale', 'wombat', 'wren', 'yak', 'zebra',
24
+ ];
25
+ const ADJECTIVES = new Set(TEST_ADDRESS_ADJECTIVES);
26
+ const ANIMALS = new Set(TEST_ADDRESS_ANIMALS);
27
+ const SHAPE = /^([a-z]+)-([a-z]+)-([1-9]\d{3})$/;
28
+ /** Whether a name is of a Test address's shape: one of the adjectives, one of the animals, a number of four digits. */
29
+ export function isTestAddress(s) {
30
+ if (typeof s !== 'string')
31
+ return false;
32
+ const m = SHAPE.exec(s);
33
+ return !!m && ADJECTIVES.has(m[1]) && ANIMALS.has(m[2]);
34
+ }
35
+ /**
36
+ * The address from the four first bytes of md5 of the id: an adjective, an animal, 1000 + a number below 9000. As SQL,
37
+ * for the column the database generates; a test holds the migration to it, and computes the same in Node.
38
+ */
39
+ export function testAddressSql(id = 'id') {
40
+ const h = `decode(md5(${id}), 'hex')`;
41
+ const list = (words) => `(array[${words.map((w) => `'${w}'`).join(', ')}]::text[])`;
42
+ return `${list(TEST_ADDRESS_ADJECTIVES)}[1 + get_byte(${h}, 0) % ${TEST_ADDRESS_ADJECTIVES.length}] || '-' || ${list(TEST_ADDRESS_ANIMALS)}[1 + get_byte(${h}, 1) % ${TEST_ADDRESS_ANIMALS.length}] || '-' || (1000 + (get_byte(${h}, 2) * 256 + get_byte(${h}, 3)) % 9000)::text`;
43
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolaby",
3
- "version": "1.8.1",
3
+ "version": "1.8.3",
4
4
  "description": "The Toolaby command line: sign in once, then wire any Chrome extension to your tool on the Wall — sign-in, the Free plan, licences and subscriptions — or start a new one; check it, and give coding agents the Wall as an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {