@prestack/thread 0.1.3 → 0.1.5

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 (270) hide show
  1. package/CHANGELOG.md +563 -0
  2. package/dist/core/actor.d.ts +1 -1
  3. package/dist/core/actor.js +5 -4
  4. package/dist/core/actor.ts +5 -4
  5. package/dist/core/attach.d.ts +0 -7
  6. package/dist/core/attach.js +38 -67
  7. package/dist/core/attach.ts +34 -80
  8. package/dist/core/chain.js +13 -17
  9. package/dist/core/chain.ts +11 -15
  10. package/dist/core/condition.d.ts +4 -4
  11. package/dist/core/condition.js +2 -2
  12. package/dist/core/descend.d.ts +4 -2
  13. package/dist/core/descend.js +91 -23
  14. package/dist/core/descend.ts +90 -21
  15. package/dist/core/errors/catalogue.d.ts +52 -0
  16. package/dist/{operations/errors/errors.js → core/errors/catalogue.js} +1 -1
  17. package/dist/{operations/errors/errors.ts → core/errors/catalogue.ts} +1 -1
  18. package/dist/{operations/errors/catalog.d.ts → core/errors/error.d.ts} +2 -3
  19. package/dist/{operations/errors/catalog.js → core/errors/error.js} +1 -16
  20. package/dist/{operations/errors/catalog.ts → core/errors/error.ts} +3 -19
  21. package/dist/core/errors/namespaces.d.ts +3 -0
  22. package/dist/core/errors/namespaces.js +15 -0
  23. package/dist/core/errors/namespaces.ts +19 -0
  24. package/dist/core/finding.js +2 -2
  25. package/dist/core/finding.ts +1 -2
  26. package/dist/core/init.d.ts +1 -1
  27. package/dist/core/init.js +51 -5
  28. package/dist/core/init.ts +58 -5
  29. package/dist/core/invoked.d.ts +1 -1
  30. package/dist/core/invoked.js +3 -1
  31. package/dist/core/invoked.ts +5 -2
  32. package/dist/core/round.js +36 -22
  33. package/dist/core/round.ts +26 -9
  34. package/dist/core/rows.d.ts +4 -0
  35. package/dist/core/rows.js +31 -2
  36. package/dist/core/rows.ts +33 -1
  37. package/dist/core/scope.d.ts +1 -0
  38. package/dist/core/scope.js +1 -0
  39. package/dist/core/scope.ts +2 -0
  40. package/dist/core/thread.d.ts +3 -3
  41. package/dist/core/tree.d.ts +4 -1
  42. package/dist/core/tree.js +26 -6
  43. package/dist/core/tree.ts +29 -6
  44. package/dist/core/types.d.ts +3 -3
  45. package/dist/core/types.js +3 -1
  46. package/dist/core/types.ts +2 -1
  47. package/dist/derivation/attach.js +3 -2
  48. package/dist/derivation/attach.ts +2 -1
  49. package/dist/derivation/generators/build.js +6 -6
  50. package/dist/derivation/generators/build.ts +1 -1
  51. package/dist/derivation/generators/check.js +1 -1
  52. package/dist/derivation/generators/determinism.js +1 -1
  53. package/dist/derivation/generators/release.js +60 -47
  54. package/dist/derivation/generators/release.ts +66 -43
  55. package/dist/derivation/release.d.ts +7 -3
  56. package/dist/derivation/release.js +14 -1
  57. package/dist/derivation/release.ts +22 -2
  58. package/dist/integrations/attach.d.ts +4 -0
  59. package/dist/integrations/attach.js +37 -56
  60. package/dist/integrations/attach.ts +54 -60
  61. package/dist/integrations/cli.d.ts +37 -0
  62. package/dist/integrations/cli.js +440 -0
  63. package/dist/integrations/cli.ts +543 -0
  64. package/dist/{core/files.d.ts → integrations/disk.d.ts} +16 -24
  65. package/dist/integrations/disk.js +435 -0
  66. package/dist/integrations/disk.ts +465 -0
  67. package/dist/integrations/generators/read.js +6 -6
  68. package/dist/integrations/generators/read.ts +4 -4
  69. package/dist/integrations/generators/vault.js +10 -11
  70. package/dist/integrations/generators/vault.ts +4 -5
  71. package/dist/{operations/http.d.ts → integrations/https.d.ts} +10 -6
  72. package/dist/integrations/https.js +170 -0
  73. package/dist/integrations/https.ts +216 -0
  74. package/dist/integrations/pglite.d.ts +26 -0
  75. package/dist/integrations/pglite.js +143 -0
  76. package/dist/integrations/pglite.ts +206 -0
  77. package/dist/integrations/secret.d.ts +8 -2
  78. package/dist/integrations/secret.js +3 -5
  79. package/dist/integrations/secret.ts +10 -6
  80. package/dist/integrations/target.d.ts +155 -14
  81. package/dist/integrations/target.js +171 -50
  82. package/dist/integrations/target.ts +377 -61
  83. package/dist/integrations/vault.d.ts +1 -0
  84. package/dist/integrations/vault.js +20 -5
  85. package/dist/integrations/vault.ts +17 -5
  86. package/dist/integrity/attach.js +8 -77
  87. package/dist/integrity/attach.ts +7 -76
  88. package/dist/integrity/check-threads.js +5 -5
  89. package/dist/integrity/check-threads.ts +2 -2
  90. package/dist/integrity/constraint.d.ts +0 -1
  91. package/dist/integrity/constraint.js +267 -366
  92. package/dist/integrity/constraint.ts +267 -375
  93. package/dist/integrity/generators/compiler.js +14 -17
  94. package/dist/integrity/generators/compiler.ts +10 -15
  95. package/dist/integrity/generators/gate.js +15 -15
  96. package/dist/integrity/generators/gate.ts +10 -10
  97. package/dist/integrity/generators/issues.js +7 -7
  98. package/dist/integrity/generators/publish.js +39 -57
  99. package/dist/integrity/generators/publish.ts +44 -55
  100. package/dist/integrity/generators/trial.d.ts +3 -3
  101. package/dist/integrity/generators/trial.js +133 -165
  102. package/dist/integrity/generators/trial.ts +145 -169
  103. package/dist/integrity/lint.js +9 -13
  104. package/dist/integrity/lint.ts +7 -10
  105. package/dist/integrity/lsp/bin.js +5 -4
  106. package/dist/integrity/lsp/bin.ts +3 -1
  107. package/dist/integrity/lsp/diagnostics.js +3 -3
  108. package/dist/integrity/lsp/diagnostics.ts +1 -1
  109. package/dist/integrity/lsp/lint.js +19 -30
  110. package/dist/integrity/lsp/lint.ts +16 -32
  111. package/dist/integrity/lsp/server.js +3 -3
  112. package/dist/integrity/slopnator.d.ts +3 -13
  113. package/dist/integrity/slopnator.js +59 -38
  114. package/dist/integrity/slopnator.ts +54 -38
  115. package/dist/integrity/slopnators/sl_broken_lineage.js +2 -2
  116. package/dist/integrity/slopnators/sl_declared_elsewhere.js +1 -1
  117. package/dist/integrity/slopnators/sl_derived_drift.js +4 -4
  118. package/dist/integrity/slopnators/sl_derived_drift.ts +1 -1
  119. package/dist/integrity/slopnators/sl_door_in_a_node.js +220 -0
  120. package/dist/integrity/slopnators/sl_door_in_a_node.ts +250 -0
  121. package/dist/integrity/slopnators/sl_inert_island.js +10 -8
  122. package/dist/integrity/slopnators/sl_inert_island.ts +8 -6
  123. package/dist/integrity/slopnators/sl_missing_row.js +6 -51
  124. package/dist/integrity/slopnators/sl_missing_row.ts +4 -50
  125. package/dist/integrity/slopnators/sl_no_barrel.js +1 -1
  126. package/dist/integrity/slopnators/sl_no_child_process.js +36 -18
  127. package/dist/integrity/slopnators/sl_no_child_process.ts +44 -18
  128. package/dist/integrity/slopnators/sl_no_dispatch_bypass.js +80 -149
  129. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +95 -147
  130. package/dist/integrity/slopnators/sl_no_import_cycle.js +15 -4
  131. package/dist/integrity/slopnators/sl_no_import_cycle.ts +20 -4
  132. package/dist/integrity/slopnators/sl_no_such_command.js +2 -2
  133. package/dist/integrity/slopnators/sl_no_such_command.ts +1 -1
  134. package/dist/integrity/slopnators/sl_outside_an_operation.js +20 -0
  135. package/dist/integrity/slopnators/sl_outside_an_operation.ts +25 -0
  136. package/dist/integrity/slopnators/sl_screen_holds_only_declared.d.ts +2 -0
  137. package/dist/integrity/slopnators/sl_screen_holds_only_declared.js +61 -0
  138. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +70 -0
  139. package/dist/integrity/slopnators/sl_secret_literal.js +1 -33
  140. package/dist/integrity/slopnators/sl_secret_literal.ts +1 -33
  141. package/dist/integrity/slopnators/sl_stale_path.js +5 -22
  142. package/dist/integrity/slopnators/sl_stale_path.ts +4 -20
  143. package/dist/integrity/slopnators/sl_twice_meant.js +1 -1
  144. package/dist/integrity/slopnators/sl_twice_parented.js +29 -5
  145. package/dist/integrity/slopnators/sl_twice_parented.ts +28 -4
  146. package/dist/integrity/slopnators/sl_undeclared_crossing.d.ts +4 -1
  147. package/dist/integrity/slopnators/sl_undeclared_crossing.js +426 -62
  148. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +454 -70
  149. package/dist/integrity/slopnators/sl_undeclared_destination.js +59 -25
  150. package/dist/integrity/slopnators/sl_undeclared_destination.ts +69 -21
  151. package/dist/integrity/slopnators/sl_undeclared_list.d.ts +2 -0
  152. package/dist/integrity/slopnators/sl_undeclared_list.js +363 -0
  153. package/dist/integrity/slopnators/sl_undeclared_list.ts +358 -0
  154. package/dist/integrity/slopnators/sl_undeclared_node.js +1 -11
  155. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -10
  156. package/dist/integrity/slopnators/sl_undeclared_operation.js +111 -29
  157. package/dist/integrity/slopnators/sl_undeclared_operation.ts +134 -26
  158. package/dist/integrity/slopnators/sl_undeclared_variant.js +2 -2
  159. package/dist/integrity/slopnators/sl_undeclared_webhook.d.ts +2 -0
  160. package/dist/integrity/slopnators/sl_undeclared_webhook.js +90 -0
  161. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +108 -0
  162. package/dist/integrity/slopnators/sl_underived_route.d.ts +2 -0
  163. package/dist/integrity/slopnators/sl_underived_route.js +61 -0
  164. package/dist/integrity/slopnators/sl_underived_route.ts +72 -0
  165. package/dist/integrity/slopnators/sl_ungoverned_island.js +3 -3
  166. package/dist/integrity/slopnators/sl_ungoverned_island.ts +1 -1
  167. package/dist/integrity/slopnators/sl_unlisted_program.d.ts +2 -0
  168. package/dist/integrity/slopnators/sl_unlisted_program.js +39 -0
  169. package/dist/integrity/slopnators/sl_unlisted_program.ts +40 -0
  170. package/dist/integrity/slopnators/sl_unnamed_island.js +1 -1
  171. package/dist/integrity/slopnators/sl_unpaired_member.js +3 -3
  172. package/dist/integrity/slopnators/sl_unpaired_member.ts +1 -1
  173. package/dist/integrity/slopnators/sl_unpaired_slopnator.js +4 -4
  174. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +1 -1
  175. package/dist/integrity/slopnators/sl_unprefixed_member.js +5 -31
  176. package/dist/integrity/slopnators/sl_unprefixed_member.ts +2 -28
  177. package/dist/integrity/slopnators/sl_unthreaded.js +11 -7
  178. package/dist/integrity/slopnators/sl_unthreaded.ts +6 -7
  179. package/dist/integrity/slopnators/sl_untranslated_key.js +2 -25
  180. package/dist/integrity/slopnators/sl_untranslated_key.ts +1 -25
  181. package/dist/integrity/slopnators/sl_unvaulted_secret.d.ts +2 -0
  182. package/dist/integrity/slopnators/sl_unvaulted_secret.js +107 -0
  183. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +132 -0
  184. package/dist/integrity/slopnators/sl_unversioned.js +6 -4
  185. package/dist/integrity/slopnators/sl_unversioned.ts +5 -3
  186. package/dist/integrity/sweep.d.ts +1 -0
  187. package/dist/integrity/sweep.js +105 -42
  188. package/dist/integrity/sweep.ts +115 -42
  189. package/dist/knowledge/attach.js +4 -3
  190. package/dist/knowledge/attach.ts +3 -2
  191. package/dist/knowledge/generators/docsgen.js +8 -6
  192. package/dist/knowledge/generators/docsgen.ts +4 -2
  193. package/dist/knowledge/generators/glossary.js +4 -4
  194. package/dist/knowledge/generators/glossary.ts +1 -1
  195. package/dist/operations/access.d.ts +23 -0
  196. package/dist/operations/access.js +28 -1
  197. package/dist/operations/access.ts +43 -0
  198. package/dist/operations/attach.js +33 -113
  199. package/dist/operations/attach.ts +51 -117
  200. package/dist/operations/capabilities.d.ts +14 -5
  201. package/dist/operations/capabilities.js +13 -4
  202. package/dist/operations/capabilities.ts +27 -8
  203. package/dist/operations/dispatch.d.ts +8 -2
  204. package/dist/operations/dispatch.js +128 -185
  205. package/dist/operations/dispatch.ts +138 -191
  206. package/dist/operations/error-results.d.ts +1 -1
  207. package/dist/operations/error-results.js +2 -2
  208. package/dist/operations/error-results.ts +2 -2
  209. package/dist/operations/generators/capability.d.ts +11 -1
  210. package/dist/operations/generators/capability.js +481 -224
  211. package/dist/operations/generators/capability.ts +562 -233
  212. package/dist/operations/generators/compile.js +66 -39
  213. package/dist/operations/generators/compile.ts +78 -39
  214. package/dist/operations/generators/permits.js +11 -12
  215. package/dist/operations/generators/permits.ts +3 -4
  216. package/dist/operations/generators/seed.d.ts +1 -0
  217. package/dist/operations/generators/seed.js +19 -17
  218. package/dist/operations/generators/seed.ts +22 -14
  219. package/dist/operations/generators/trial.d.ts +6 -8
  220. package/dist/operations/generators/trial.js +200 -70
  221. package/dist/operations/generators/trial.ts +230 -93
  222. package/dist/operations/nodes.d.ts +15 -16
  223. package/dist/operations/nodes.ts +16 -17
  224. package/dist/operations/operations.d.ts +40 -4
  225. package/dist/operations/operations.js +1 -0
  226. package/dist/operations/operations.ts +61 -4
  227. package/dist/operations/result.d.ts +2 -2
  228. package/dist/operations/result.ts +2 -2
  229. package/package.json +1 -4
  230. package/dist/core/answers.d.ts +0 -171
  231. package/dist/core/answers.js +0 -789
  232. package/dist/core/answers.ts +0 -999
  233. package/dist/core/client.d.ts +0 -63
  234. package/dist/core/client.js +0 -249
  235. package/dist/core/client.ts +0 -289
  236. package/dist/core/files.js +0 -171
  237. package/dist/core/files.ts +0 -222
  238. package/dist/integrations/module.d.ts +0 -6
  239. package/dist/integrations/module.js +0 -153
  240. package/dist/integrations/module.ts +0 -191
  241. package/dist/integrity/slopnators/sl_unkept_rows.js +0 -104
  242. package/dist/integrity/slopnators/sl_unkept_rows.ts +0 -111
  243. package/dist/integrity/slopnators/sl_unseamed_reach.js +0 -73
  244. package/dist/integrity/slopnators/sl_unseamed_reach.ts +0 -79
  245. package/dist/operations/cost.d.ts +0 -14
  246. package/dist/operations/cost.js +0 -37
  247. package/dist/operations/cost.ts +0 -58
  248. package/dist/operations/errors/errors.d.ts +0 -52
  249. package/dist/operations/http.js +0 -124
  250. package/dist/operations/http.ts +0 -169
  251. package/dist/operations/memory.d.ts +0 -104
  252. package/dist/operations/memory.js +0 -365
  253. package/dist/operations/memory.ts +0 -470
  254. package/dist/operations/metering.d.ts +0 -55
  255. package/dist/operations/metering.js +0 -247
  256. package/dist/operations/metering.ts +0 -328
  257. package/dist/operations/queue-policy.d.ts +0 -3
  258. package/dist/operations/queue-policy.js +0 -8
  259. package/dist/operations/queue-policy.ts +0 -10
  260. package/dist/operations/seams.d.ts +0 -239
  261. package/dist/operations/seams.js +0 -45
  262. package/dist/operations/seams.ts +0 -364
  263. package/dist/operations/store.d.ts +0 -14
  264. package/dist/operations/store.js +0 -1
  265. package/dist/operations/store.ts +0 -29
  266. /package/dist/{operations → core}/errors/classes.d.ts +0 -0
  267. /package/dist/{operations → core}/errors/classes.js +0 -0
  268. /package/dist/{operations → core}/errors/classes.ts +0 -0
  269. /package/dist/integrity/slopnators/{sl_unkept_rows.d.ts → sl_door_in_a_node.d.ts} +0 -0
  270. /package/dist/integrity/slopnators/{sl_unseamed_reach.d.ts → sl_outside_an_operation.d.ts} +0 -0
@@ -6,22 +6,30 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
- import { existsSync, readdirSync, readFileSync, writeFileSync } from "../../core/files.js";
10
- import { resolve } from 'node:path';
9
+ import ts from 'typescript';
10
+ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from '../../integrations/disk.js';
11
+ import { dirname, resolve } from 'node:path';
11
12
  import { argv, stdout } from 'node:process';
12
13
  import { pathToFileURL } from 'node:url';
13
- import { marked, wordsIn } from "../../core/answers.js";
14
- import { ROOT, TREE, descend } from "../../core/descend.js";
15
- import { exit, invoked } from "../../core/invoked.js";
16
- import { named as packageOf } from "../../core/scope.js";
17
- import { PREFIXES } from "../../core/thread.js";
18
- import { servesBrowser } from "../../integrations/target.js";
19
- import { CAPABILITIES_AT, CLIENT_AT, DECLARED_SEAMS_AT, MCP_AT, OPENAPI_AT, DISPATCHING_AT, SEAMS_AT, routeAt, SURFACES, switched, } from "../capabilities.js";
20
- import { ANONYMOUS, ENVIRONMENT } from "../../core/actor.js";
21
- import { PERMITS_AT } from "../access.js";
22
- import { SAID as HANDED, supplies } from "../seams.js";
23
- import { beside, operations, quoted, runtime, types } from "./seed.js";
14
+ import { ROOT, TREE, descend, forget } from '../../core/descend.js';
15
+ import { exit, invoked } from '../../core/invoked.js';
16
+ import { named as packageOf } from '../../core/scope.js';
17
+ import { PREFIXES } from '../../core/thread.js';
18
+ import { secrecyOf } from '../../integrations/secret.js';
19
+ import { reachOf, servesBrowser, targetOf } from '../../integrations/target.js';
20
+ import { CAPABILITIES_AT, CLIENT_AT, MCP_AT, OPENAPI_AT, REACHING_AT, ACTOR_AT, DEFAULTS_AT, DISPATCHING_AT, inboundAt, isSurface, ROUTE, routeAt, SURFACES, switched, UNSTATED, } from '../capabilities.js';
21
+ import { ANONYMOUS, ENVIRONMENT } from '../../core/actor.js';
22
+ import { DISPATCH_AT } from '../dispatch.js';
23
+ import { STREAMED, TOLD } from '../../integrations/https.js';
24
+ import { holdsRow, listBounds, marked, nestedBounds, rowsIn, valuesIn, withField, withRow, withTopField, withoutRow } from '../../core/rows.js';
25
+ import { said } from '../attach.js';
26
+ import { MEMBERS_LIST } from '../operations.js';
27
+ import { beside, operations, quoted, runtime, types } from './seed.js';
24
28
  const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
29
+ const listedIn = (held) => held !== undefined && ts.isArrayLiteralExpression(held)
30
+ ? held.elements.filter((one) => ts.isStringLiteral(one)).map((one) => one.text)
31
+ : [];
32
+ const trueIn = (held) => held !== undefined && held.kind === ts.SyntaxKind.TrueKeyword;
25
33
  function stable(value) {
26
34
  if (Array.isArray(value))
27
35
  return value.map(stable);
@@ -83,14 +91,15 @@ function browsing(requires, targets, secrets) {
83
91
  }
84
92
  return true;
85
93
  }
86
- function locally(requires) {
87
- for (const word of requires) {
88
- if (!supplies(word))
89
- return false;
90
- if (word === 'sm_vault')
91
- return false;
92
- }
93
- return true;
94
+ export function defaulted() {
95
+ const at = resolve(ROOT, DEFAULTS_AT);
96
+ if (!existsSync(at))
97
+ return undefined;
98
+ const row = Object.values(valuesIn(readFileSync(at, 'utf8'))).find((one) => typeof one === 'object' && one !== null && one.at === DEFAULTS_AT);
99
+ if (row === undefined || !Array.isArray(row.live))
100
+ return { live: UNSTATED, stated: false, wrong: [] };
101
+ const given = row.live.map(String);
102
+ return { live: SURFACES.filter((each) => given.includes(each)), stated: true, wrong: given.filter((each) => !isSurface(each)) };
94
103
  }
95
104
  export async function derive() {
96
105
  const pass = descend();
@@ -99,10 +108,28 @@ export async function derive() {
99
108
  const found = [];
100
109
  const targets = await sheet('targets.ts', 'TARGETS');
101
110
  const secrets = await sheet('secrets.ts', 'SECRETS');
102
- const at = resolve(ROOT, CAPABILITIES_AT);
103
- const already = switched(existsSync(at) ? readFileSync(at, 'utf8') : undefined);
111
+ for (const one of targets) {
112
+ if (reachOf(one.id) !== 'sdk' || (one.declares ?? []).length > 0)
113
+ continue;
114
+ refusals.push(`\`${one.id}\` calls a package inside this app, and its row declares no file that does the calling. That file is the one ` +
115
+ 'place the package is imported, and it exports `calls`, holding the function each call names as its path. Give the row ' +
116
+ `one, and the file with it, with \`thread edit ${one.id} --declares <id> --at <path> --describes <sentence> --body <text>\``);
117
+ }
118
+ const tree = defaulted();
119
+ if (tree === undefined) {
120
+ refusals.push(`${DEFAULTS_AT} is not here, so nothing says which surfaces an operation answers on, and working it out anyway would ` +
121
+ `take away every route this app has. \`thread attach ${packageOf('operations')}\` writes it`);
122
+ }
123
+ for (const one of tree?.wrong ?? []) {
124
+ refusals.push(`${DEFAULTS_AT} says \`${one}\` is live, and there is no such surface. There are ${String(SURFACES.length)}: ` +
125
+ `${SURFACES.join(', ')}. \`thread create capability edit defaults\` rewrites it`);
126
+ }
104
127
  for (const [id, one] of [...operations(pass, known)].sort((a, b) => compare(a[0], b[0]))) {
105
128
  const stated = pass.stated.get(id);
129
+ const declared = {
130
+ webhooks: listedIn(stated?.fields.get('webhooks')),
131
+ signed: trueIn(stated?.fields.get('signed')),
132
+ };
106
133
  const describes = stated?.describes;
107
134
  if (describes === undefined) {
108
135
  refusals.push(`\`${id}\` states no \`describes\`, and a caller told nothing about an operation cannot choose it`);
@@ -123,7 +150,14 @@ export async function derive() {
123
150
  refusals.push(`\`${id}\`: ${schema}`);
124
151
  continue;
125
152
  }
126
- const live = already.get(id) ?? [...SURFACES];
153
+ const off = listedIn(stated?.fields.get('off'));
154
+ const stray = off.filter((each) => !isSurface(each));
155
+ if (stray.length > 0) {
156
+ refusals.push(`\`${id}\` turns off ${stray.map((each) => `\`${each}\``).join(', ')}, and there is no such surface. There are ` +
157
+ `${String(SURFACES.length)}: ${SURFACES.join(', ')}. \`thread create capability edit ${id}\` rewrites it`);
158
+ continue;
159
+ }
160
+ const live = (tree?.live ?? UNSTATED).filter((each) => !off.includes(each));
127
161
  found.push({
128
162
  operation: id,
129
163
  describes,
@@ -131,14 +165,50 @@ export async function derive() {
131
165
  nodes: one.nodes.map((each) => each.id ?? '').filter((each) => each !== ''),
132
166
  input: schema,
133
167
  live: [...live].sort(compare),
168
+ webhooks: [...declared.webhooks].sort(compare),
169
+ signed: declared.signed,
134
170
  browsable: browsing(one.requires, targets, secrets),
135
- local: browsing(one.requires, targets, secrets) && locally(one.requires),
136
171
  mcp: { name: id, description: describes, inputSchema: schema },
137
172
  cli: { invocation: `${id} --input '<json>'`, describes },
138
173
  api: { method, path: `/operations/${id}` },
139
174
  });
140
175
  }
141
- return { found, refusals };
176
+ for (const one of await sheet('targets.ts', 'WEBHOOKS')) {
177
+ const runs = found.find((each) => each.operation === one.invokes);
178
+ if (runs === undefined) {
179
+ refusals.push(`\`${one.id}\` invokes \`${one.invokes}\`, and this app declares no operation by that name. A webhook is a way in ` +
180
+ `to something already declared, so declare that operation or point this webhook at one that exists`);
181
+ continue;
182
+ }
183
+ const route = (one.derives ?? []).some((each) => each.at === inboundAt(one.id) && each.by !== undefined && each.runs !== undefined);
184
+ if (!route) {
185
+ refusals.push(`\`${one.id}\` derives no route. A webhook is an address anybody may post to, so the file that answers it has to be a row ` +
186
+ `this tree declares: state a \`derives\` on this webhook naming ${inboundAt(one.id)}, with \`by\` and \`runs\` set to ` +
187
+ '`thread capabilities` and `thread capabilities --check`, so what is written there is checked against what writes it');
188
+ }
189
+ if (!runs.webhooks.includes(one.id)) {
190
+ refusals.push(`\`${one.id}\` invokes \`${one.invokes}\`, and that operation does not name it back. An operation is run from outside ` +
191
+ `only by a webhook it named, so add ${one.id} to \`webhooks\` on that operation's row in ${TREE}operations.ts, or point this webhook elsewhere`);
192
+ }
193
+ if (runs.signed && (one.arrives === undefined || (one.requires ?? []).length === 0)) {
194
+ refusals.push(`\`${one.invokes}\` says it will only be run by a signed webhook, and \`${one.id}\` carries no signature to check. ` +
195
+ `Give this webhook the header its signature arrives on and the secret it is signed with`);
196
+ }
197
+ if (!runs.live.includes('webhook')) {
198
+ refusals.push(`\`${one.id}\` invokes \`${one.invokes}\`, and that operation does not answer on the webhook surface. Both halves have to ` +
199
+ `agree before anything outside can run it: webhook has to be live in ${DEFAULTS_AT}, which \`thread create capability edit defaults\` changes, and not in the \`off\` on its row, which \`thread create capability edit ${one.invokes}\` changes`);
200
+ }
201
+ const holds = (one.requires ?? []).filter((each) => secrecyOf(each) !== undefined);
202
+ if (one.arrives !== undefined && holds.length === 0) {
203
+ refusals.push(`\`${one.id}\` says a signature arrives on \`${one.arrives}\` and requires no secret to check it against, so every ` +
204
+ `request would be taken on trust. Name the secret it is signed with, or take \`arrives\` off the row`);
205
+ }
206
+ if (one.arrives === undefined && holds.length > 0) {
207
+ refusals.push(`\`${one.id}\` requires ${holds.join(', ')} and says no header a signature arrives on, so nothing would ever be checked ` +
208
+ `against it. Say which header carries the signature, or take the secret off the row`);
209
+ }
210
+ }
211
+ return { found, refusals, live: tree?.live ?? UNSTATED };
142
212
  }
143
213
  function named() {
144
214
  try {
@@ -214,10 +284,10 @@ function client(found) {
214
284
  ` path: ${JSON.stringify(one.api.path)},`,
215
285
  ' },',
216
286
  ].join('\n'));
217
- const near = found.filter((one) => one.local && compiled(one.operation));
287
+ const near = found.filter((one) => one.browsable && compiled(one.operation));
218
288
  const bound = near.flatMap((one) => [
219
- `export async function ${one.operation}Locally(input: unknown, actor: Actor, seams: Seams): Promise<Result<unknown>> {`,
220
- ` const held = await dispatch(${one.operation}, input, actor, seams)`,
289
+ 'export async function ' + one.operation + 'Locally(input: unknown, actor: Actor): Promise<Result<unknown>> {',
290
+ ` const held = await dispatch(${one.operation}, input, actor, DECLARED, opening, counting)`,
221
291
  ' return held',
222
292
  '}',
223
293
  '',
@@ -228,9 +298,9 @@ function client(found) {
228
298
  : [
229
299
  `import type { Actor } from '${runtime(CLIENT_AT, '../../core/actor.ts')}'`,
230
300
  `import type { Result } from '${runtime(CLIENT_AT, '../result.ts')}'`,
231
- `import type { Seams } from '${runtime(CLIENT_AT, '../seams.ts')}'`,
232
301
  '',
233
302
  "import { dispatch } from './_dispatch.ts'",
303
+ `import { counting, DECLARED, opening } from '${beside(CLIENT_AT, REACHING_AT)}'`,
234
304
  ...near.map((one) => `import { operation as ${one.operation} } from './${one.operation}.ts'`),
235
305
  '',
236
306
  ]),
@@ -253,12 +323,13 @@ function client(found) {
253
323
  function route(one) {
254
324
  const at = routeAt(one.operation);
255
325
  return [
256
- "import { existsSync } from 'node:fs'",
326
+ `import { existsSync } from ${quoted(runtime(at, '../../integrations/disk.ts'))}`,
257
327
  "import { join } from 'node:path'",
258
328
  '',
329
+ 'import { counting, DECLARED, opening } from ' + quoted(beside(at, REACHING_AT)),
330
+ 'import { actor } from ' + quoted(beside(at, ACTOR_AT)),
331
+ '',
259
332
  "import { dispatch } from '../../../operations/_dispatch.ts'",
260
- `import { actor } from '${beside(at, DECLARED_SEAMS_AT)}'`,
261
- `import { SEAMS } from '${beside(at, SEAMS_AT)}'`,
262
333
  `import { operation } from '../../../operations/${one.operation}.ts'`,
263
334
  '',
264
335
  'const STATUS: Record<string, number> = {',
@@ -281,14 +352,110 @@ function route(one) {
281
352
  ' }',
282
353
  ...(one.api.method === 'GET'
283
354
  ? [' const input = Object.fromEntries(new URL(request.url).searchParams)']
284
- : [' const input = await request.json().catch(() => undefined)']),
285
- ' const held = await dispatch(operation, input, await actor(request), SEAMS)',
355
+ : [
356
+ ' const input = await request.json().catch(() => undefined)',
357
+ " if ((request.headers.get('accept') ?? '').includes(" + quoted(STREAMED) + ')) {',
358
+ ' const lines = new TransformStream<string, string>()',
359
+ ' const writing = lines.writable.getWriter()',
360
+ " const sent = (said: unknown): void => { writing.write(JSON.stringify(said) + '\\n').catch(() => undefined) }",
361
+ ' void dispatch(operation, input, await actor(request), DECLARED, opening, counting, (said) => sent({ ' + TOLD + ': said })).then((held) => {',
362
+ ' sent(held)',
363
+ ' return writing.close()',
364
+ ' }).catch(() => undefined)',
365
+ ' return new Response(lines.readable.pipeThrough(new TextEncoderStream()), { headers: { ' + quoted('content-type') + ': ' + quoted(STREAMED) + ' } })',
366
+ ' }',
367
+ ]),
368
+ ' const held = await dispatch(operation, input, await actor(request), DECLARED, opening, counting)',
286
369
  ' return Response.json(held, { status: held.ok ? 200 : (STATUS[held.code] ?? 500) })',
287
370
  '}',
288
371
  '',
289
372
  ].join('\n');
290
373
  }
291
- const SEAM = /export\s+const\s+store\s*:/;
374
+ function inbound(one) {
375
+ const at = inboundAt(one.id);
376
+ const secret = (one.requires ?? [])[0];
377
+ const signed = one.arrives !== undefined && secret !== undefined;
378
+ const refuse = (why) => " return Response.json({ ok: false, code: " + quoted("denied") + ", error: " + quoted("not allowed") +
379
+ ", because: " + quoted(why) + " }, { status: 403 })";
380
+ return [
381
+ ...(signed ? ["import { createHmac, timingSafeEqual } from " + quoted("node:crypto")] : []),
382
+ "import { existsSync } from " + quoted(runtime(at, "../../integrations/disk.ts")),
383
+ "import { join } from " + quoted("node:path"),
384
+ "",
385
+ "import { counting, DECLARED, opening } from " + quoted(beside(at, REACHING_AT)),
386
+ "",
387
+ "import { dispatch } from " + quoted(beside(at, DISPATCH_AT)),
388
+ "import { operation } from " + quoted(beside(at, "operations/" + one.invokes + ".ts")),
389
+ "",
390
+ "const ARRIVING = " + JSON.stringify(ANONYMOUS),
391
+ "",
392
+ "const PASSED = existsSync(join(process.cwd(), " + quoted(".thread") + ", " + quoted("gate.json") + "))",
393
+ "",
394
+ "export async function POST(request: Request): Promise<Response> {",
395
+ " if (!PASSED) {",
396
+ refuse("this tree has no passing gate, so nothing in it may answer a request. Run thread gate"),
397
+ " }",
398
+ " const raw = await request.text()",
399
+ ...(signed
400
+ ? [
401
+ " const carried = request.headers.get(" + quoted(one.arrives) + ")",
402
+ " if (carried === null) {",
403
+ refuse("this webhook is signed and the request carried nothing on " + String(one.arrives)),
404
+ " }",
405
+ " const held = await opening.resolve([" + quoted(secret) + "])",
406
+ " const mine = Buffer.from(createHmac(" + quoted("sha256") + ", held[" + quoted(secret) +
407
+ "] ?? " + quoted("") + ").update(raw).digest(" + quoted("hex") + "))",
408
+ " const said = Buffer.from(carried)",
409
+ " if (said.length !== mine.length || !timingSafeEqual(said, mine)) {",
410
+ refuse("the signature on this request is not what " + String(secret) + " signs a body with"),
411
+ " }",
412
+ ]
413
+ : []),
414
+ " const input: unknown = JSON.parse(raw)",
415
+ " const answer = await dispatch(operation, input, ARRIVING, DECLARED, opening, counting)",
416
+ " return Response.json(answer, { status: answer.ok ? 200 : 500 })",
417
+ "}",
418
+ "",
419
+ ].join("\n");
420
+ }
421
+ function declaring(webhooks) {
422
+ const sheet = resolve(ROOT, TREE + 'targets.ts');
423
+ if (!existsSync(sheet))
424
+ return [];
425
+ let text = readFileSync(sheet, 'utf8');
426
+ const wrote = [];
427
+ for (const one of webhooks) {
428
+ const id = one.id + '_route';
429
+ const where = listBounds(text, 'WEBHOOKS');
430
+ if (where === undefined)
431
+ break;
432
+ const inside = nestedBounds(text, where, one.id, 'derives');
433
+ if (inside !== undefined && holdsRow(text, inside, id))
434
+ continue;
435
+ const row = [
436
+ ' {',
437
+ " id: " + quoted(id) + ",",
438
+ " at: " + quoted(inboundAt(one.id)) + ",",
439
+ " describes: " + quoted('The file that answers what ' + one.id + ' posts. It checks the signature before anything runs and then hands the body ' +
440
+ 'to the same dispatch every other caller reaches, so an inbound event is refused or dispatched and never anything else.') + ",",
441
+ ' version: 1,',
442
+ " by: 'thread capabilities',",
443
+ " runs: 'thread capabilities --check',",
444
+ ' },',
445
+ ].join('\n');
446
+ const next = inside === undefined
447
+ ? withField(text, where, one.id, 'derives', '[\n' + row + '\n ]')
448
+ : withRow(text, inside, row);
449
+ if (next === undefined || next === text)
450
+ continue;
451
+ text = next;
452
+ wrote.push(id);
453
+ }
454
+ if (wrote.length > 0)
455
+ writeFileSync(sheet, text, 'utf8');
456
+ return wrote;
457
+ }
458
+ const STORED = /export\s+const\s+store\b/;
292
459
  const STORES_AT = 'src/stores';
293
460
  function kept() {
294
461
  const at = resolve(ROOT, STORES_AT);
@@ -298,7 +465,7 @@ function kept() {
298
465
  .filter((one) => one.endsWith('.ts'))
299
466
  .filter((one) => {
300
467
  try {
301
- return SEAM.test(readFileSync(resolve(at, one), 'utf8'));
468
+ return STORED.test(readFileSync(resolve(at, one), 'utf8'));
302
469
  }
303
470
  catch {
304
471
  return false;
@@ -310,221 +477,313 @@ function kept() {
310
477
  const ENTITIES_AT = `${TREE}entities.ts`;
311
478
  const OPERATIONS_AT = `${TREE}operations.ts`;
312
479
  const VAULT_AT = `src/vault.${'ts'}`;
313
- function settled(keeping, stores, ruled, vaulted) {
314
- const named = stores.map((one, at) => ({ from: beside(DECLARED_SEAMS_AT, one), as: `s${String(at)}` }));
315
- const only = named[0];
480
+ const served = (one) => one.live.includes('api') && compiled(one.operation);
481
+ const held = (one) => one.browsable && one.live.includes('api');
482
+ function dispatching(found) {
483
+ const reachable = found.filter(served);
316
484
  return [
317
- `import type { Actor, Environment } from '${runtime(DECLARED_SEAMS_AT, '../../core/actor.ts')}'`,
318
- `import type { Seams } from '${runtime(DECLARED_SEAMS_AT, '../seams.ts')}'`,
319
- ...(keeping ? [`import { ENTITIES } from '${beside(DECLARED_SEAMS_AT, ENTITIES_AT)}'`] : []),
320
- ...(ruled ? [`import { permits } from '${beside(DECLARED_SEAMS_AT, PERMITS_AT)}'`] : []),
321
- ...(vaulted ? [`import { vault as opening } from '${beside(DECLARED_SEAMS_AT, VAULT_AT)}'`] : []),
322
- ...(stores.length === 1 && only !== undefined ? [`import { store as kept } from '${only.from}'`] : []),
323
- ...(stores.length > 1 ? named.map((one) => `import { keeps as ${one.as}Keeps, store as ${one.as} } from '${one.from}'`) : []),
485
+ `declare module '${packageOf('ui')}/ui/checked' {`,
486
+ ...(reachable.length === 0
487
+ ? [' interface Dispatching {}']
488
+ : [' interface Dispatching {', ...reachable.map((one) => ` readonly ${one.operation}: true`), ' }']),
489
+ '}',
324
490
  '',
325
- ...(keeping
326
- ? [
327
- 'const made = {', " log: { write(level: string, action: string, who: Actor, data: Record<string, unknown>) { console.log(JSON.stringify({ level, action, who, data })) } },", " audit: { async append(who: Actor, record: unknown) { console.log(JSON.stringify({ audit: record, who })) } },", " errors: { async record(who: Actor, failure: unknown) { console.error(JSON.stringify({ failure, who })) } },", '}',
328
- ]
329
- : ['const made = {', " log: { write(level: string, action: string, who: Actor, data: Record<string, unknown>) { console.log(JSON.stringify({ level, action, who, data })) } },", " audit: { async append(who: Actor, record: unknown) { console.log(JSON.stringify({ audit: record, who })) } },", " errors: { async record(who: Actor, failure: unknown) { console.error(JSON.stringify({ failure, who })) } },", '}']),
491
+ 'interface Reaches {',
492
+ ' readonly operation: string',
493
+ " readonly method: 'GET' | 'POST'",
494
+ ' readonly path: string',
495
+ '}',
330
496
  '',
331
- ...(stores.length > 1
332
- ? [
333
- "const WHERE = new Map<string, Seams['store']>([",
334
- ...named.map((one) => ` ...${one.as}Keeps.map((entity) => [entity, ${one.as}] as const),`),
335
- '])',
336
- '',
337
- 'function at(entity: string) {',
338
- ' const found = WHERE.get(entity)',
339
- " if (found === undefined) throw new Error(entity + ' is not kept in any target this app declares')",
340
- ' return found',
341
- '}',
342
- '',
343
- "const store: Seams['store'] = {",
344
- ' insert: (actor, entity, row) => at(entity).insert(actor, entity, row),',
345
- ' get: (actor, entity, key, fields) => at(entity).get(actor, entity, key, fields),',
346
- ' list: (actor, entity, opts) => at(entity).list(actor, entity, opts),',
347
- ' update: (actor, entity, key, patch, expectedVersion) => at(entity).update(actor, entity, key, patch, expectedVersion),',
348
- ' remove: (actor, entity, key) => at(entity).remove(actor, entity, key),',
349
- '}',
350
- '',
351
- ]
352
- : []),
353
- `const ANONYMOUS: Actor = ${JSON.stringify(ANONYMOUS)}`,
497
+ `const REACHES: readonly Reaches[] = [${reachable.length === 0
498
+ ? ''
499
+ : `\n${reachable
500
+ .map((one) => ` { operation: ${JSON.stringify(one.operation)}, method: ${JSON.stringify(one.api?.method)}, path: ${JSON.stringify(one.api?.path)} },`)
501
+ .join('\n')}\n`}]`,
354
502
  '',
355
- "const http: NonNullable<Seams['http']> = {",
356
- ' async call(request) {',
357
- ' const base = new URL(request.address)',
358
- " if (base.protocol !== 'https:') throw new Error(request.target + ' is reached over ' + base.protocol + ' and an outward call goes over https only')",
359
- ' const stem = base.pathname.length === 1 ? String() : base.pathname',
360
- ' const url = new URL(base.origin + stem + request.path)',
361
- " if (url.origin !== base.origin) throw new Error(request.path + ' resolves to ' + url.origin + ', which is not where ' + request.target + ' is')",
362
- ' const headers: Record<string, string> = { ...request.headers }',
363
- " if (request.contentType !== undefined) headers['content-type'] = request.contentType",
364
- ' const carried = new Uint8Array(request.body === undefined ? 0 : request.body.byteLength)',
365
- ' if (request.body !== undefined) carried.set(request.body)',
366
- ' const began = Date.now()',
367
- ' const sent = JSON.stringify(headers).length + (request.body === undefined ? 0 : request.body.byteLength)',
368
- ' const answered = await fetch(url.toString(), { method: request.method, headers, ...(request.body === undefined ? {} : { body: carried }), signal: AbortSignal.timeout(10000) })',
369
- ' const body = new Uint8Array(await answered.arrayBuffer())',
370
- ' return { status: answered.status, headers: Object.fromEntries(answered.headers.entries()), body, bytesSent: sent, bytesReceived: body.byteLength, durationMs: Date.now() - began }',
371
- ' },',
503
+ 'export async function dispatchTo(operation: string, input?: Readonly<Record<string, unknown>>): Promise<unknown> {',
504
+ ' const one = REACHES.find((each) => each.operation === operation)',
505
+ ' if (one === undefined) {',
506
+ ' throw new Error(',
507
+ " `thread: nothing declares an operation called ${operation}, so there is nowhere to send this. ` +",
508
+ " `This app reaches: ${REACHES.map((each) => each.operation).join(', ') || '(nothing)'}`,",
509
+ ' )',
510
+ ' }',
511
+ ' const sent = await fetch(one.path, {',
512
+ ' method: one.method,',
513
+ " headers: { 'content-type': 'application/json' },",
514
+ " ...(one.method === 'GET' ? {} : { body: JSON.stringify(input ?? {}) }),",
515
+ ' })',
516
+ ' return await sent.json()',
517
+ '}',
518
+ '',
519
+ ].join('\n');
520
+ }
521
+ export function reaching(targets, calls, stores, vaulted, carried) {
522
+ const taken = (one) => calls.filter((each) => each.of === one.id && one.takes.includes(each.id)).map((each) => each.id.replace(/^cl_/, ''));
523
+ const rows = targets.map((one) => {
524
+ const mine = taken(one).map((each) => quoted(each));
525
+ return ' readonly ' + one.id + ': ' + (mine.length === 0 ? 'never' : mine.join(' | '));
526
+ });
527
+ const named = stores.map((one, at) => ({
528
+ from: beside(REACHING_AT, one),
529
+ as: 's' + String(at),
530
+ of: (one.split('/').pop() ?? '').replace(/\.ts$/, ''),
531
+ }));
532
+ const adapted = targets
533
+ .filter((one) => reachOf(one.id) === 'sdk')
534
+ .flatMap((one, at) => {
535
+ const file = (one.declares ?? [])[0]?.at;
536
+ return file === undefined ? [] : [{ from: beside(REACHING_AT, file), as: 'c' + String(at), of: one.id }];
537
+ });
538
+ const opened = targets
539
+ .map((one) => {
540
+ const reach = reachOf(one.id);
541
+ if (reach === 'sdk') {
542
+ const held = adapted.find((each) => each.of === one.id);
543
+ if (held === undefined)
544
+ return '';
545
+ const each = calls
546
+ .filter((call) => call.of === one.id && one.takes.includes(call.id))
547
+ .map((call) => quoted(call.id.replace(/^cl_/, '')) + ': ' + held.as + '[' + quoted(call.path) + ']');
548
+ return ' ' + one.id + ': { ' + each.join(', ') + ' },';
549
+ }
550
+ if (reach === 'disk') {
551
+ const through = one.requires.find((each) => reachOf(each) !== undefined);
552
+ return through === undefined ? ' ' + one.id + ': HERE,' : '';
553
+ }
554
+ if (reach === 'cli') {
555
+ const each = calls
556
+ .filter((call) => call.of === one.id && one.takes.includes(call.id))
557
+ .map((call) => quoted(call.id.replace(/^cl_/, '')) + ': ' + quoted(call.path));
558
+ return ' ' + one.id + ': running(' + quoted(one.id.replace(/^cli_/, '')) + ', { ' + each.join(', ') + ' }, process.cwd()),';
559
+ }
560
+ if (reach === 'https')
561
+ return ' ' + one.id + ': sendingHttp(),';
562
+ const held = named.find((each) => each.of === one.id);
563
+ return held === undefined ? '' : ' ' + one.id + ': ' + held.as + ',';
564
+ })
565
+ .filter((each) => each !== '');
566
+ return [
567
+ 'import { HERE } from ' + quoted(runtime(REACHING_AT, '../../integrations/disk.ts')),
568
+ 'import { running } from ' + quoted(runtime(REACHING_AT, '../../integrations/cli.ts')),
569
+ 'import { sendingHttp } from ' + quoted(runtime(REACHING_AT, '../../integrations/https.ts')),
570
+ 'import type { Counting } from ' +
571
+ quoted(carried ? packageOf('operations') + '/operations/operations' : runtime(REACHING_AT, '../operations.ts')),
572
+ ...named.map((each) => 'import { store as ' + each.as + ' } from ' + quoted(each.from)),
573
+ ...adapted.map((each) => 'import { calls as ' + each.as + ' } from ' + quoted(each.from)),
574
+ ...(vaulted ? ['import { vault } from ' + quoted(beside(REACHING_AT, VAULT_AT))] : []),
575
+ '',
576
+ 'declare module ' +
577
+ quoted(carried ? packageOf('operations') + '/operations/operations' : runtime(REACHING_AT, '../operations.ts')) +
578
+ ' {',
579
+ ...(rows.length === 0 ? [' interface Reaching {}'] : [' interface Reaching {', ...rows, ' }']),
580
+ '}',
581
+ '',
582
+ 'export const DECLARED: Readonly<Record<string, unknown>> = {',
583
+ ...opened,
372
584
  '}',
373
585
  '',
374
- 'const shared = globalThis as { __threadSeams?: Seams }',
586
+ vaulted ? 'export const opening = { resolve: vault }' : 'export const opening = undefined',
587
+ '',
588
+ 'export const counting: Counting | undefined = undefined',
589
+ '',
590
+ ].join('\n');
591
+ }
592
+ const reading = (header) => 'request.headers.get(' + quoted(header) + ')';
593
+ const CARRIED = ENVIRONMENT.flatMap(([fact, header, fills]) => {
594
+ const taken = header === null ? fills : fills === null ? reading(header) : reading(header) + ' ?? ' + fills;
595
+ return taken === null ? [] : [{ fact, taken, always: fills !== null }];
596
+ });
597
+ function caller(keeping, kept) {
598
+ const looking = keeping && kept !== undefined;
599
+ return [
600
+ 'import type { Actor, Environment } from ' + quoted(runtime(ACTOR_AT, '../../core/actor.ts')),
601
+ ...(looking ? ['import { ENTITIES } from ' + quoted(beside(ACTOR_AT, ENTITIES_AT))] : []),
602
+ ...(looking ? ['import { DECLARED } from ' + quoted(beside(ACTOR_AT, REACHING_AT))] : []),
603
+ ...(looking ? ['import type { Store } from ' + quoted(runtime(ACTOR_AT, '../../integrations/target.ts'))] : []),
375
604
  '',
376
- 'export const DECLARED: Seams = (shared.__threadSeams ??= {',
377
- ' ...made,',
378
- ...(stores.length === 1 ? [' store: kept,'] : []),
379
- ...(stores.length > 1 ? [' store,'] : []),
380
- ' clock: { now: () => Date.now() },',
381
- ' random: { next: Math.random, uuid: () => crypto.randomUUID() },',
382
- ' http,',
383
- ...(ruled ? [' permits: { check: (actor, action, resource) => Promise.resolve(permits(actor, action, resource)) },'] : []),
384
- ...(vaulted ? [' vault: { resolve: opening },'] : []),
385
- '})',
605
+ 'const ANONYMOUS: Actor = ' + JSON.stringify(ANONYMOUS),
386
606
  '',
387
607
  'function environment(request: Request): Environment {',
388
- ...ENVIRONMENT.map(([fact, header]) => ` const ${fact} = request.headers.get(${quoted(header)})`),
608
+ ...CARRIED.map((one) => ' const ' + one.fact + ' = ' + one.taken),
389
609
  ' return {',
390
- ...ENVIRONMENT.map(([fact]) => ` ...(${fact} === null ? {} : { ${fact} }),`),
610
+ ...CARRIED.map((one) => one.always ? ' ' + one.fact + ',' : ' ...(' + one.fact + ' === null ? {} : { ' + one.fact + ' }),'),
391
611
  ' }',
392
612
  '}',
393
613
  '',
394
- ...(keeping
614
+ ...(looking
395
615
  ? [
396
616
  'const worn = (held: unknown): readonly string[] =>',
397
- " Array.isArray(held) ? held.filter((one) => typeof one === 'string') : typeof held === 'string' ? [held] : []",
617
+ ' Array.isArray(held) ? held.filter((one) => typeof one === ' + quoted('string') + ') : typeof held === ' + quoted('string') + ' ? [held] : []',
398
618
  '',
399
619
  'function addressed(request: Request, from: string): string | null {',
400
- " if (from.startsWith('cookie:')) {",
401
- " const jar = request.headers.get('cookie') ?? ''",
402
- " const want = `${from.slice('cookie:'.length).trim()}=`",
403
- " const found = jar.split(';').map((one) => one.trim()).find((one) => one.startsWith(want))",
620
+ ' if (from.startsWith(' + quoted('cookie:') + ')) {',
621
+ ' const jar = request.headers.get(' + quoted('cookie') + ') ?? ' + quoted(''),
622
+ ' const want = from.slice(' + quoted('cookie:') + '.length).trim() + ' + quoted('='),
623
+ ' const found = jar.split(' + quoted(';') + ').map((one) => one.trim()).find((one) => one.startsWith(want))',
404
624
  ' return found === undefined ? null : decodeURIComponent(found.slice(want.length))',
405
625
  ' }',
406
626
  ' const held = request.headers.get(from)',
407
627
  ' if (held === null) return null',
408
- " const bare = held.startsWith('Bearer ') ? held.slice('Bearer '.length) : held",
409
- " return bare.trim() === '' ? null : bare.trim()",
628
+ ' const bare = held.startsWith(' + quoted('Bearer ') + ') ? held.slice(' + quoted('Bearer ') + '.length) : held',
629
+ ' return bare.trim() === ' + quoted('') + ' ? null : bare.trim()',
410
630
  '}',
411
631
  '',
412
- 'export async function actor(request: Request): Promise<Actor> {',
413
- ' const anyone: Actor = { ...ANONYMOUS, ...environment(request) }',
632
+ ]
633
+ : []),
634
+ 'export async function actor(request: Request): Promise<Actor> {',
635
+ ' const anyone: Actor = { ...ANONYMOUS, ...environment(request) }',
636
+ ...(looking
637
+ ? [
414
638
  ' const who = ENTITIES.find((one) => one.actor !== undefined)',
415
639
  ' if (who?.actor === undefined) return anyone',
416
- '',
417
- " const address = (who.must ?? []).flatMap((one) => ('unique' in one ? one.unique : []))[0]",
640
+ ' const address = (who.must ?? []).flatMap((one) => (' + quoted('unique') + ' in one ? one.unique : []))[0]',
418
641
  ' if (address === undefined) return anyone',
419
- '',
420
642
  ' const held = addressed(request, who.actor.from)',
421
643
  ' if (held === null) return anyone',
422
- '',
644
+ ' const keeper = DECLARED[' + quoted(kept ?? '') + '] as Store | undefined',
645
+ ' if (keeper === undefined) return anyone',
423
646
  ' const token = who.actor.verifies',
424
- '',
425
647
  ' const row =',
426
648
  ' token === undefined',
427
- ' ? await DECLARED.store.get(anyone, who.id, held, [',
649
+ ' ? await keeper.get(anyone, who.id, held, [',
428
650
  ' address,',
429
651
  ' who.actor.roles,',
430
652
  ' ...(who.actor.entitlements === undefined ? [] : [who.actor.entitlements]),',
431
653
  ' ])',
432
- ' : ((await DECLARED.store.list(anyone, who.id, { where: { [token]: held }, limit: 1 }))[0] ?? null)',
654
+ ' : ((await keeper.list(anyone, who.id, { where: { [token]: held }, limit: 1 }))[0] ?? null)',
433
655
  ' if (row === null) return anyone',
434
- '',
435
656
  ' return {',
436
657
  ' ...anyone,',
437
658
  ' id: String(row[address] ?? held),',
438
659
  ' roles: worn(row[who.actor.roles]),',
439
660
  ' entitlements: who.actor.entitlements === undefined ? [] : worn(row[who.actor.entitlements]),',
440
661
  ' }',
441
- '}',
442
- '',
443
662
  ]
444
- : [
445
- 'export const actor = (request: Request): Promise<Actor> => Promise.resolve({ ...ANONYMOUS, ...environment(request) })',
446
- '',
447
- ]),
448
- ].join('\n');
449
- }
450
- const seeded = () => [
451
- `import { DECLARED } from '${beside(SEAMS_AT, DECLARED_SEAMS_AT)}'`,
452
- `import type { Seams } from '${runtime(SEAMS_AT, '../seams.ts')}'`,
453
- '',
454
- 'export const SEAMS: Seams = {',
455
- ' ...DECLARED,',
456
- '}',
457
- '',
458
- ].join('\n');
459
- const width = Math.max(...Object.keys(HANDED).map((one) => one.length));
460
- const handing = (stores, keeping, vaulted) => [
461
- `capabilities: seeded ${SEAMS_AT} — what this app hands its operations, and yours from here.`,
462
- ` It hands on everything ${DECLARED_SEAMS_AT} settles from your declarations: rows go to ` +
463
- (stores.length === 0 ? 'no store yet — no target keeps entities, so an operation that needs one is refused' : stores.join(', ')) +
464
- `, who is asking is ${keeping ? 'read off the entity that names the caller' : 'nobody until an entity names the caller'}, ` +
465
- `calls leave through http${vaulted ? ' with credentials opened by the vault' : ''}.`,
466
- ' Replace a seam, or add one, by naming it in SEAMS. Every seam an operation can be handed:',
467
- ...Object.entries(HANDED).map(([one, says]) => ` ${one.padEnd(width)} ${says}`),
468
- '',
469
- ].join('\n');
470
- const served = (one) => one.live.includes('api') && compiled(one.operation);
471
- const held = (one) => one.browsable && one.live.includes('api');
472
- function dispatching(found) {
473
- const reachable = found.filter(served);
474
- return [
475
- '/* GENERATED by thread — do not edit. It is derived from what this app declares. */',
476
- '',
477
- `declare module '${packageOf('ui')}/ui/checked' {`,
478
- ...(reachable.length === 0
479
- ? [' interface Dispatching {}']
480
- : [' interface Dispatching {', ...reachable.map((one) => ` readonly ${one.operation}: true`), ' }']),
481
- '}',
482
- '',
483
- 'interface Reaches {',
484
- ' readonly operation: string',
485
- " readonly method: 'GET' | 'POST'",
486
- ' readonly path: string',
487
- '}',
488
- '',
489
- `const REACHES: readonly Reaches[] = [${reachable.length === 0
490
- ? ''
491
- : `\n${reachable
492
- .map((one) => ` { operation: ${JSON.stringify(one.operation)}, method: ${JSON.stringify(one.api?.method)}, path: ${JSON.stringify(one.api?.path)} },`)
493
- .join('\n')}\n`}]`,
494
- '',
495
- 'export async function dispatchTo(operation: string, input?: Readonly<Record<string, unknown>>): Promise<unknown> {',
496
- ' const one = REACHES.find((each) => each.operation === operation)',
497
- ' if (one === undefined) {',
498
- ' throw new Error(',
499
- " `thread: nothing declares an operation called ${operation}, so there is nowhere to send this. ` +",
500
- " `This app reaches: ${REACHES.map((each) => each.operation).join(', ') || '(nothing)'}`,",
501
- ' )',
502
- ' }',
503
- ' const sent = await fetch(one.path, {',
504
- ' method: one.method,',
505
- " headers: { 'content-type': 'application/json' },",
506
- " ...(one.method === 'GET' ? {} : { body: JSON.stringify(input ?? {}) }),",
507
- ' })',
508
- ' return await sent.json()',
663
+ : [' return anyone']),
509
664
  '}',
510
665
  '',
511
666
  ].join('\n');
512
667
  }
513
- const installed = () => existsSync(resolve(ROOT, '_threads', 'ui.ts'));
514
- const files = (found) => [
668
+ const carries = () => existsSync(resolve(ROOT, 'node_modules', packageOf('operations')));
669
+ const installed = () => existsSync(resolve(ROOT, 'node_modules', packageOf('ui')));
670
+ const files = (found, targets, calls, keeps, webhooks = []) => [
515
671
  { at: CAPABILITIES_AT, text: printed(found) },
516
672
  { at: OPENAPI_AT, text: printed(openapi(found)) },
517
673
  { at: MCP_AT, text: printed(mcp(found)) },
518
674
  { at: CLIENT_AT, text: client(found) },
675
+ { at: REACHING_AT, text: reaching(targets, calls, kept(), existsSync(resolve(ROOT, VAULT_AT)), carries()) },
676
+ { at: ACTOR_AT, text: caller(existsSync(resolve(ROOT, ENTITIES_AT)), keeps) },
519
677
  ...(installed() ? [{ at: DISPATCHING_AT, text: dispatching(found) }] : []),
520
678
  ...found.filter(served).map((one) => ({ at: routeAt(one.operation), text: route(one) })),
679
+ ...webhooks
680
+ .filter((one) => found.some((each) => each.operation === one.invokes && each.live.includes('webhook') && each.webhooks.includes(one.id) && compiled(one.invokes)))
681
+ .flatMap((one) => {
682
+ const at = (one.derives ?? []).map((each) => each.at).find((each) => each === inboundAt(one.id));
683
+ return at === undefined ? [] : [{ at, text: inbound(one) }];
684
+ }),
521
685
  ];
686
+ function unmoved() {
687
+ const tree = defaulted();
688
+ if (tree === undefined || tree.stated)
689
+ return undefined;
690
+ const at = resolve(ROOT, CAPABILITIES_AT);
691
+ const switches = switched(existsSync(at) ? readFileSync(at, 'utf8') : undefined);
692
+ if (switches.size === 0)
693
+ return undefined;
694
+ const live = SURFACES.filter((each) => [...switches.values()].some((one) => one.includes(each)));
695
+ const off = new Map([...switches]
696
+ .map(([id, one]) => [id, live.filter((each) => !one.includes(each))])
697
+ .filter(([, one]) => one.length > 0));
698
+ const same = off.size === 0 && live.length === UNSTATED.length && live.every((each) => UNSTATED.includes(each));
699
+ return same ? undefined : { live, off };
700
+ }
701
+ export const asArray = (surfaces) => `[${surfaces.map((one) => quoted(one)).join(', ')}]`;
702
+ function carry(plan) {
703
+ const at = resolve(ROOT, DEFAULTS_AT);
704
+ const put = withTopField(readFileSync(at, 'utf8'), 'live', asArray(plan.live));
705
+ if (put === undefined)
706
+ return [];
707
+ writeFileSync(at, put, 'utf8');
708
+ const sheet = resolve(ROOT, OPERATIONS_AT);
709
+ let text = readFileSync(sheet, 'utf8');
710
+ const told = [`${DEFAULTS_AT} is live on ${plan.live.join(', ') || 'nothing'}`];
711
+ for (const [id, off] of plan.off) {
712
+ const where = listBounds(text, MEMBERS_LIST);
713
+ const next = where === undefined ? undefined : withField(text, where, id, 'off', asArray(off));
714
+ if (next === undefined)
715
+ continue;
716
+ text = next;
717
+ told.push(`${id} turns off ${off.join(', ')}`);
718
+ }
719
+ writeFileSync(sheet, text, 'utf8');
720
+ return told;
721
+ }
722
+ function routed(found, writing) {
723
+ const sheet = resolve(ROOT, OPERATIONS_AT);
724
+ if (!existsSync(sheet))
725
+ return [];
726
+ let text = readFileSync(sheet, 'utf8');
727
+ const wrong = [];
728
+ for (const one of found) {
729
+ const id = `${one.operation}_route`;
730
+ const row = said({ id, at: routeAt(one.operation), describes: ROUTE, by: 'thread capabilities', runs: 'thread capabilities --check' }, ' ');
731
+ const members = listBounds(text, MEMBERS_LIST);
732
+ const inside = members === undefined ? undefined : nestedBounds(text, members, one.operation, 'derives');
733
+ const standing = inside === undefined ? undefined : rowsIn(text, inside).find((each) => each.id === id);
734
+ const wants = one.live.includes('api');
735
+ if (wants ? standing?.source.trim() === row.trim() : standing === undefined)
736
+ continue;
737
+ wrong.push(wants
738
+ ? `${one.operation} answers on api and its row ${standing === undefined ? 'declares no route' : 'says something else about its route'}`
739
+ : `${one.operation} does not answer on api and its row still declares ${routeAt(one.operation)}`);
740
+ if (!writing)
741
+ continue;
742
+ let next = inside !== undefined && standing !== undefined ? withoutRow(text, inside, id) : text;
743
+ if (wants) {
744
+ const list = listBounds(next, MEMBERS_LIST);
745
+ const within = list === undefined ? undefined : nestedBounds(next, list, one.operation, 'derives');
746
+ const put = within !== undefined
747
+ ? withRow(next, within, row)
748
+ : list === undefined
749
+ ? undefined
750
+ : withField(next, list, one.operation, 'derives', `[\n${row}\n ]`);
751
+ if (put !== undefined)
752
+ next = put;
753
+ }
754
+ text = next;
755
+ }
756
+ if (writing && wrong.length > 0)
757
+ writeFileSync(sheet, text, 'utf8');
758
+ return wrong;
759
+ }
760
+ const unserved = (found) => found
761
+ .filter((one) => !one.live.includes('api') && existsSync(resolve(ROOT, routeAt(one.operation))))
762
+ .map((one) => routeAt(one.operation));
522
763
  if (invoked(import.meta.url)) {
523
- const { found, refusals } = await derive();
764
+ const words = argv.slice(2).filter((one) => !one.startsWith('-'));
765
+ if (words[0] !== undefined && words[0] !== 'read') {
766
+ console.error('capabilities: `' + words[0] + '` is not something this command does. `thread capabilities` writes what can be reached, and ' +
767
+ '`thread capabilities read <mark>` prints one. Nothing was written.');
768
+ exit(1);
769
+ }
770
+ if (!argv.includes('--check')) {
771
+ const declared = declaring(await sheet('targets.ts', 'WEBHOOKS'));
772
+ for (const one of declared)
773
+ console.log(`capabilities: declared ${one} in ${TREE}targets.ts, so the route it writes has a row that says the address was agreed to`);
774
+ if (declared.length > 0)
775
+ forget();
776
+ const pending = unmoved();
777
+ if (pending !== undefined) {
778
+ console.log(`capabilities: carried what ${CAPABILITIES_AT} had switched onto the rows it is worked out from now — ` +
779
+ `${carry(pending).join('; ')}. Nothing in it is set by hand any more.`);
780
+ forget();
781
+ }
782
+ }
783
+ const { found, refusals, live } = await derive();
524
784
  for (const one of refusals)
525
785
  console.error(`capabilities: ${one}`);
526
786
  const mark = (one) => marked([one.operation, one.describes, one.requires.join(','), one.nodes.join(','), one.live.join(',')]);
527
- const words = wordsIn(['read']);
528
787
  const verb = words[0];
529
788
  if (verb === 'read') {
530
789
  const which = words[1];
@@ -538,7 +797,6 @@ if (invoked(import.meta.url)) {
538
797
  stdout.write(`nodes: ${one.nodes.length === 0 ? '(none)' : one.nodes.join(', ')}\n`);
539
798
  stdout.write(`live: ${SURFACES.map((each) => `${one.live.includes(each) ? '[on] ' : '[off]'} ${each}`).join(' ')}\n`);
540
799
  stdout.write(`${one.browsable ? 'may be called from a page' : 'server only — it reaches a target that needs a server secret'}\n`);
541
- stdout.write(`${one.local ? 'runs where it is called, with nothing sent' : 'reaches a target over the network'}\n`);
542
800
  stdout.write(`api: ${one.api.method} ${one.api.path}\n`);
543
801
  stdout.write(`cli: ${one.cli.invocation}\n`);
544
802
  stdout.write(`mcp: ${one.mcp.name} — ${one.mcp.description}\n`);
@@ -550,38 +808,15 @@ if (invoked(import.meta.url)) {
550
808
  const checking = argv.includes('--check');
551
809
  let adrift = 0;
552
810
  const keeping = existsSync(resolve(ROOT, ENTITIES_AT));
553
- if (existsSync(resolve(ROOT, OPERATIONS_AT))) {
554
- const stores = kept();
555
- const vaulted = existsSync(resolve(ROOT, VAULT_AT));
556
- const text = settled(keeping, stores, existsSync(resolve(ROOT, PERMITS_AT)), vaulted);
557
- const at = resolve(ROOT, DECLARED_SEAMS_AT);
558
- if ((existsSync(at) ? readFileSync(at, 'utf8') : '') !== text) {
559
- if (checking) {
560
- adrift += 1;
561
- console.error(`capabilities: ${DECLARED_SEAMS_AT} is not what the declarations settle. Run \`thread capabilities\` and commit the result.`);
562
- }
563
- else {
564
- writeFileSync(at, text, 'utf8');
565
- console.log(`capabilities: wrote ${DECLARED_SEAMS_AT} — the seams your targets, entities, grants and secrets settle.`);
566
- }
567
- }
568
- const seed = resolve(ROOT, SEAMS_AT);
569
- if (!existsSync(seed)) {
570
- if (checking) {
571
- adrift += 1;
572
- console.error(`capabilities: ${SEAMS_AT} is not there, and it is what hands every operation its seams. Run \`thread capabilities\` — it seeds the file once, and from then on it is yours.`);
573
- }
574
- else {
575
- writeFileSync(seed, seeded(), 'utf8');
576
- stdout.write(handing(stores, keeping, vaulted));
577
- }
578
- }
579
- }
580
811
  if (found.length === 0 && refusals.length === 0) {
581
812
  console.log('capabilities: this app declares no operation, so nothing is reachable through one yet.');
582
813
  exit(adrift > 0 ? 1 : 0);
583
814
  }
584
- const written = files(found);
815
+ const all = await sheet('targets.ts', 'TARGETS');
816
+ const people = await sheet('entities.ts', 'ENTITIES');
817
+ const one = people.find((each) => each.actor !== undefined);
818
+ const where = one === undefined ? undefined : targetOf(one.id, all);
819
+ const written = files(found, all, await sheet('targets.ts', 'CALLS'), where === undefined || typeof where === 'string' ? undefined : where.id, await sheet('targets.ts', 'WEBHOOKS'));
585
820
  if (checking) {
586
821
  const drifted = written.filter((one) => {
587
822
  const path = resolve(ROOT, one.at);
@@ -590,7 +825,15 @@ if (invoked(import.meta.url)) {
590
825
  for (const one of drifted) {
591
826
  console.error(`capabilities: ${one.at} is not what the operations derive. Run \`thread capabilities\` and commit the result.`);
592
827
  }
593
- const off = found.filter((one) => one.live.length < SURFACES.length);
828
+ const unsettled = [
829
+ ...(unmoved() === undefined ? [] : [`${CAPABILITIES_AT} switches surfaces that no row states yet`]),
830
+ ...routed(found, false),
831
+ ...unserved(found).map((at) => `${at} is still on disk to be served, and its operation does not answer on api`),
832
+ ];
833
+ for (const one of unsettled)
834
+ console.error(`capabilities: ${one}. Run \`thread capabilities\` and commit the result.`);
835
+ adrift += unsettled.length;
836
+ const off = found.filter((one) => one.live.length < live.length);
594
837
  console.log(`capabilities: ${String(found.length)} operation(s) reachable, ${String(off.length)} with a surface switched off, ` +
595
838
  `${String(drifted.length + adrift)} finding(s).`);
596
839
  exit(drifted.length + adrift > 0 || refusals.length > 0 ? 1 : 0);
@@ -599,6 +842,20 @@ if (invoked(import.meta.url)) {
599
842
  console.error(`capabilities: ${String(refusals.length)} refusal(s) above, so nothing was written`);
600
843
  exit(1);
601
844
  }
845
+ if (!installed()) {
846
+ const gone = resolve(ROOT, DISPATCHING_AT);
847
+ if (existsSync(gone))
848
+ rmSync(gone);
849
+ }
850
+ for (const one of routed(found, true))
851
+ console.log(`capabilities: ${one}, so ${OPERATIONS_AT} was changed to match`);
852
+ for (const at of unserved(found)) {
853
+ rmSync(resolve(ROOT, at));
854
+ const folder = dirname(resolve(ROOT, at));
855
+ if (readdirSync(folder).length === 0)
856
+ rmSync(folder, { recursive: true, force: true });
857
+ console.log(`capabilities: deleted ${at}, because its operation does not answer on api and nothing may serve it`);
858
+ }
602
859
  for (const one of written) {
603
860
  const path = resolve(ROOT, one.at);
604
861
  writeFileSync(path, one.text);