@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.
- package/CHANGELOG.md +563 -0
- package/dist/core/actor.d.ts +1 -1
- package/dist/core/actor.js +5 -4
- package/dist/core/actor.ts +5 -4
- package/dist/core/attach.d.ts +0 -7
- package/dist/core/attach.js +38 -67
- package/dist/core/attach.ts +34 -80
- package/dist/core/chain.js +13 -17
- package/dist/core/chain.ts +11 -15
- package/dist/core/condition.d.ts +4 -4
- package/dist/core/condition.js +2 -2
- package/dist/core/descend.d.ts +4 -2
- package/dist/core/descend.js +91 -23
- package/dist/core/descend.ts +90 -21
- package/dist/core/errors/catalogue.d.ts +52 -0
- package/dist/{operations/errors/errors.js → core/errors/catalogue.js} +1 -1
- package/dist/{operations/errors/errors.ts → core/errors/catalogue.ts} +1 -1
- package/dist/{operations/errors/catalog.d.ts → core/errors/error.d.ts} +2 -3
- package/dist/{operations/errors/catalog.js → core/errors/error.js} +1 -16
- package/dist/{operations/errors/catalog.ts → core/errors/error.ts} +3 -19
- package/dist/core/errors/namespaces.d.ts +3 -0
- package/dist/core/errors/namespaces.js +15 -0
- package/dist/core/errors/namespaces.ts +19 -0
- package/dist/core/finding.js +2 -2
- package/dist/core/finding.ts +1 -2
- package/dist/core/init.d.ts +1 -1
- package/dist/core/init.js +51 -5
- package/dist/core/init.ts +58 -5
- package/dist/core/invoked.d.ts +1 -1
- package/dist/core/invoked.js +3 -1
- package/dist/core/invoked.ts +5 -2
- package/dist/core/round.js +36 -22
- package/dist/core/round.ts +26 -9
- package/dist/core/rows.d.ts +4 -0
- package/dist/core/rows.js +31 -2
- package/dist/core/rows.ts +33 -1
- package/dist/core/scope.d.ts +1 -0
- package/dist/core/scope.js +1 -0
- package/dist/core/scope.ts +2 -0
- package/dist/core/thread.d.ts +3 -3
- package/dist/core/tree.d.ts +4 -1
- package/dist/core/tree.js +26 -6
- package/dist/core/tree.ts +29 -6
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +3 -1
- package/dist/core/types.ts +2 -1
- package/dist/derivation/attach.js +3 -2
- package/dist/derivation/attach.ts +2 -1
- package/dist/derivation/generators/build.js +6 -6
- package/dist/derivation/generators/build.ts +1 -1
- package/dist/derivation/generators/check.js +1 -1
- package/dist/derivation/generators/determinism.js +1 -1
- package/dist/derivation/generators/release.js +60 -47
- package/dist/derivation/generators/release.ts +66 -43
- package/dist/derivation/release.d.ts +7 -3
- package/dist/derivation/release.js +14 -1
- package/dist/derivation/release.ts +22 -2
- package/dist/integrations/attach.d.ts +4 -0
- package/dist/integrations/attach.js +37 -56
- package/dist/integrations/attach.ts +54 -60
- package/dist/integrations/cli.d.ts +37 -0
- package/dist/integrations/cli.js +440 -0
- package/dist/integrations/cli.ts +543 -0
- package/dist/{core/files.d.ts → integrations/disk.d.ts} +16 -24
- package/dist/integrations/disk.js +435 -0
- package/dist/integrations/disk.ts +465 -0
- package/dist/integrations/generators/read.js +6 -6
- package/dist/integrations/generators/read.ts +4 -4
- package/dist/integrations/generators/vault.js +10 -11
- package/dist/integrations/generators/vault.ts +4 -5
- package/dist/{operations/http.d.ts → integrations/https.d.ts} +10 -6
- package/dist/integrations/https.js +170 -0
- package/dist/integrations/https.ts +216 -0
- package/dist/integrations/pglite.d.ts +26 -0
- package/dist/integrations/pglite.js +143 -0
- package/dist/integrations/pglite.ts +206 -0
- package/dist/integrations/secret.d.ts +8 -2
- package/dist/integrations/secret.js +3 -5
- package/dist/integrations/secret.ts +10 -6
- package/dist/integrations/target.d.ts +155 -14
- package/dist/integrations/target.js +171 -50
- package/dist/integrations/target.ts +377 -61
- package/dist/integrations/vault.d.ts +1 -0
- package/dist/integrations/vault.js +20 -5
- package/dist/integrations/vault.ts +17 -5
- package/dist/integrity/attach.js +8 -77
- package/dist/integrity/attach.ts +7 -76
- package/dist/integrity/check-threads.js +5 -5
- package/dist/integrity/check-threads.ts +2 -2
- package/dist/integrity/constraint.d.ts +0 -1
- package/dist/integrity/constraint.js +267 -366
- package/dist/integrity/constraint.ts +267 -375
- package/dist/integrity/generators/compiler.js +14 -17
- package/dist/integrity/generators/compiler.ts +10 -15
- package/dist/integrity/generators/gate.js +15 -15
- package/dist/integrity/generators/gate.ts +10 -10
- package/dist/integrity/generators/issues.js +7 -7
- package/dist/integrity/generators/publish.js +39 -57
- package/dist/integrity/generators/publish.ts +44 -55
- package/dist/integrity/generators/trial.d.ts +3 -3
- package/dist/integrity/generators/trial.js +133 -165
- package/dist/integrity/generators/trial.ts +145 -169
- package/dist/integrity/lint.js +9 -13
- package/dist/integrity/lint.ts +7 -10
- package/dist/integrity/lsp/bin.js +5 -4
- package/dist/integrity/lsp/bin.ts +3 -1
- package/dist/integrity/lsp/diagnostics.js +3 -3
- package/dist/integrity/lsp/diagnostics.ts +1 -1
- package/dist/integrity/lsp/lint.js +19 -30
- package/dist/integrity/lsp/lint.ts +16 -32
- package/dist/integrity/lsp/server.js +3 -3
- package/dist/integrity/slopnator.d.ts +3 -13
- package/dist/integrity/slopnator.js +59 -38
- package/dist/integrity/slopnator.ts +54 -38
- package/dist/integrity/slopnators/sl_broken_lineage.js +2 -2
- package/dist/integrity/slopnators/sl_declared_elsewhere.js +1 -1
- package/dist/integrity/slopnators/sl_derived_drift.js +4 -4
- package/dist/integrity/slopnators/sl_derived_drift.ts +1 -1
- package/dist/integrity/slopnators/sl_door_in_a_node.js +220 -0
- package/dist/integrity/slopnators/sl_door_in_a_node.ts +250 -0
- package/dist/integrity/slopnators/sl_inert_island.js +10 -8
- package/dist/integrity/slopnators/sl_inert_island.ts +8 -6
- package/dist/integrity/slopnators/sl_missing_row.js +6 -51
- package/dist/integrity/slopnators/sl_missing_row.ts +4 -50
- package/dist/integrity/slopnators/sl_no_barrel.js +1 -1
- package/dist/integrity/slopnators/sl_no_child_process.js +36 -18
- package/dist/integrity/slopnators/sl_no_child_process.ts +44 -18
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.js +80 -149
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +95 -147
- package/dist/integrity/slopnators/sl_no_import_cycle.js +15 -4
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +20 -4
- package/dist/integrity/slopnators/sl_no_such_command.js +2 -2
- package/dist/integrity/slopnators/sl_no_such_command.ts +1 -1
- package/dist/integrity/slopnators/sl_outside_an_operation.js +20 -0
- package/dist/integrity/slopnators/sl_outside_an_operation.ts +25 -0
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.d.ts +2 -0
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.js +61 -0
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +70 -0
- package/dist/integrity/slopnators/sl_secret_literal.js +1 -33
- package/dist/integrity/slopnators/sl_secret_literal.ts +1 -33
- package/dist/integrity/slopnators/sl_stale_path.js +5 -22
- package/dist/integrity/slopnators/sl_stale_path.ts +4 -20
- package/dist/integrity/slopnators/sl_twice_meant.js +1 -1
- package/dist/integrity/slopnators/sl_twice_parented.js +29 -5
- package/dist/integrity/slopnators/sl_twice_parented.ts +28 -4
- package/dist/integrity/slopnators/sl_undeclared_crossing.d.ts +4 -1
- package/dist/integrity/slopnators/sl_undeclared_crossing.js +426 -62
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +454 -70
- package/dist/integrity/slopnators/sl_undeclared_destination.js +59 -25
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +69 -21
- package/dist/integrity/slopnators/sl_undeclared_list.d.ts +2 -0
- package/dist/integrity/slopnators/sl_undeclared_list.js +363 -0
- package/dist/integrity/slopnators/sl_undeclared_list.ts +358 -0
- package/dist/integrity/slopnators/sl_undeclared_node.js +1 -11
- package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -10
- package/dist/integrity/slopnators/sl_undeclared_operation.js +111 -29
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +134 -26
- package/dist/integrity/slopnators/sl_undeclared_variant.js +2 -2
- package/dist/integrity/slopnators/sl_undeclared_webhook.d.ts +2 -0
- package/dist/integrity/slopnators/sl_undeclared_webhook.js +90 -0
- package/dist/integrity/slopnators/sl_undeclared_webhook.ts +108 -0
- package/dist/integrity/slopnators/sl_underived_route.d.ts +2 -0
- package/dist/integrity/slopnators/sl_underived_route.js +61 -0
- package/dist/integrity/slopnators/sl_underived_route.ts +72 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.js +3 -3
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +1 -1
- package/dist/integrity/slopnators/sl_unlisted_program.d.ts +2 -0
- package/dist/integrity/slopnators/sl_unlisted_program.js +39 -0
- package/dist/integrity/slopnators/sl_unlisted_program.ts +40 -0
- package/dist/integrity/slopnators/sl_unnamed_island.js +1 -1
- package/dist/integrity/slopnators/sl_unpaired_member.js +3 -3
- package/dist/integrity/slopnators/sl_unpaired_member.ts +1 -1
- package/dist/integrity/slopnators/sl_unpaired_slopnator.js +4 -4
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +1 -1
- package/dist/integrity/slopnators/sl_unprefixed_member.js +5 -31
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +2 -28
- package/dist/integrity/slopnators/sl_unthreaded.js +11 -7
- package/dist/integrity/slopnators/sl_unthreaded.ts +6 -7
- package/dist/integrity/slopnators/sl_untranslated_key.js +2 -25
- package/dist/integrity/slopnators/sl_untranslated_key.ts +1 -25
- package/dist/integrity/slopnators/sl_unvaulted_secret.d.ts +2 -0
- package/dist/integrity/slopnators/sl_unvaulted_secret.js +107 -0
- package/dist/integrity/slopnators/sl_unvaulted_secret.ts +132 -0
- package/dist/integrity/slopnators/sl_unversioned.js +6 -4
- package/dist/integrity/slopnators/sl_unversioned.ts +5 -3
- package/dist/integrity/sweep.d.ts +1 -0
- package/dist/integrity/sweep.js +105 -42
- package/dist/integrity/sweep.ts +115 -42
- package/dist/knowledge/attach.js +4 -3
- package/dist/knowledge/attach.ts +3 -2
- package/dist/knowledge/generators/docsgen.js +8 -6
- package/dist/knowledge/generators/docsgen.ts +4 -2
- package/dist/knowledge/generators/glossary.js +4 -4
- package/dist/knowledge/generators/glossary.ts +1 -1
- package/dist/operations/access.d.ts +23 -0
- package/dist/operations/access.js +28 -1
- package/dist/operations/access.ts +43 -0
- package/dist/operations/attach.js +33 -113
- package/dist/operations/attach.ts +51 -117
- package/dist/operations/capabilities.d.ts +14 -5
- package/dist/operations/capabilities.js +13 -4
- package/dist/operations/capabilities.ts +27 -8
- package/dist/operations/dispatch.d.ts +8 -2
- package/dist/operations/dispatch.js +128 -185
- package/dist/operations/dispatch.ts +138 -191
- package/dist/operations/error-results.d.ts +1 -1
- package/dist/operations/error-results.js +2 -2
- package/dist/operations/error-results.ts +2 -2
- package/dist/operations/generators/capability.d.ts +11 -1
- package/dist/operations/generators/capability.js +481 -224
- package/dist/operations/generators/capability.ts +562 -233
- package/dist/operations/generators/compile.js +66 -39
- package/dist/operations/generators/compile.ts +78 -39
- package/dist/operations/generators/permits.js +11 -12
- package/dist/operations/generators/permits.ts +3 -4
- package/dist/operations/generators/seed.d.ts +1 -0
- package/dist/operations/generators/seed.js +19 -17
- package/dist/operations/generators/seed.ts +22 -14
- package/dist/operations/generators/trial.d.ts +6 -8
- package/dist/operations/generators/trial.js +200 -70
- package/dist/operations/generators/trial.ts +230 -93
- package/dist/operations/nodes.d.ts +15 -16
- package/dist/operations/nodes.ts +16 -17
- package/dist/operations/operations.d.ts +40 -4
- package/dist/operations/operations.js +1 -0
- package/dist/operations/operations.ts +61 -4
- package/dist/operations/result.d.ts +2 -2
- package/dist/operations/result.ts +2 -2
- package/package.json +1 -4
- package/dist/core/answers.d.ts +0 -171
- package/dist/core/answers.js +0 -789
- package/dist/core/answers.ts +0 -999
- package/dist/core/client.d.ts +0 -63
- package/dist/core/client.js +0 -249
- package/dist/core/client.ts +0 -289
- package/dist/core/files.js +0 -171
- package/dist/core/files.ts +0 -222
- package/dist/integrations/module.d.ts +0 -6
- package/dist/integrations/module.js +0 -153
- package/dist/integrations/module.ts +0 -191
- package/dist/integrity/slopnators/sl_unkept_rows.js +0 -104
- package/dist/integrity/slopnators/sl_unkept_rows.ts +0 -111
- package/dist/integrity/slopnators/sl_unseamed_reach.js +0 -73
- package/dist/integrity/slopnators/sl_unseamed_reach.ts +0 -79
- package/dist/operations/cost.d.ts +0 -14
- package/dist/operations/cost.js +0 -37
- package/dist/operations/cost.ts +0 -58
- package/dist/operations/errors/errors.d.ts +0 -52
- package/dist/operations/http.js +0 -124
- package/dist/operations/http.ts +0 -169
- package/dist/operations/memory.d.ts +0 -104
- package/dist/operations/memory.js +0 -365
- package/dist/operations/memory.ts +0 -470
- package/dist/operations/metering.d.ts +0 -55
- package/dist/operations/metering.js +0 -247
- package/dist/operations/metering.ts +0 -328
- package/dist/operations/queue-policy.d.ts +0 -3
- package/dist/operations/queue-policy.js +0 -8
- package/dist/operations/queue-policy.ts +0 -10
- package/dist/operations/seams.d.ts +0 -239
- package/dist/operations/seams.js +0 -45
- package/dist/operations/seams.ts +0 -364
- package/dist/operations/store.d.ts +0 -14
- package/dist/operations/store.js +0 -1
- package/dist/operations/store.ts +0 -29
- /package/dist/{operations → core}/errors/classes.d.ts +0 -0
- /package/dist/{operations → core}/errors/classes.js +0 -0
- /package/dist/{operations → core}/errors/classes.ts +0 -0
- /package/dist/integrity/slopnators/{sl_unkept_rows.d.ts → sl_door_in_a_node.d.ts} +0 -0
- /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
|
|
10
|
-
import {
|
|
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 {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { servesBrowser } from
|
|
19
|
-
import { CAPABILITIES_AT, CLIENT_AT,
|
|
20
|
-
import { ANONYMOUS, ENVIRONMENT } from
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
103
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
287
|
+
const near = found.filter((one) => one.browsable && compiled(one.operation));
|
|
218
288
|
const bound = near.flatMap((one) => [
|
|
219
|
-
|
|
220
|
-
` const held = await dispatch(${one.operation}, input, actor,
|
|
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
|
-
|
|
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
|
-
: [
|
|
285
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
`
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
491
|
+
'interface Reaches {',
|
|
492
|
+
' readonly operation: string',
|
|
493
|
+
" readonly method: 'GET' | 'POST'",
|
|
494
|
+
' readonly path: string',
|
|
495
|
+
'}',
|
|
330
496
|
'',
|
|
331
|
-
|
|
332
|
-
?
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
356
|
-
'
|
|
357
|
-
'
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
'
|
|
363
|
-
|
|
364
|
-
'
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
'
|
|
368
|
-
'
|
|
369
|
-
'
|
|
370
|
-
'
|
|
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
|
|
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
|
-
'
|
|
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
|
-
...
|
|
608
|
+
...CARRIED.map((one) => ' const ' + one.fact + ' = ' + one.taken),
|
|
389
609
|
' return {',
|
|
390
|
-
...
|
|
610
|
+
...CARRIED.map((one) => one.always ? ' ' + one.fact + ',' : ' ...(' + one.fact + ' === null ? {} : { ' + one.fact + ' }),'),
|
|
391
611
|
' }',
|
|
392
612
|
'}',
|
|
393
613
|
'',
|
|
394
|
-
...(
|
|
614
|
+
...(looking
|
|
395
615
|
? [
|
|
396
616
|
'const worn = (held: unknown): readonly string[] =>',
|
|
397
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
413
|
-
|
|
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
|
|
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
|
|
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
|
|
514
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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);
|