@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
|
@@ -1,34 +1,44 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ts from 'typescript'
|
|
2
|
+
|
|
3
|
+
import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from '../../integrations/disk.ts'
|
|
2
4
|
import { dirname, resolve } from 'node:path'
|
|
3
5
|
import { argv, stdout } from 'node:process'
|
|
4
6
|
import { pathToFileURL } from 'node:url'
|
|
5
7
|
|
|
6
|
-
import {
|
|
7
|
-
import { ROOT, TREE, descend } from '../../core/descend.ts'
|
|
8
|
+
import { ROOT, TREE, descend, forget } from '../../core/descend.ts'
|
|
8
9
|
import { exit, invoked } from '../../core/invoked.ts'
|
|
9
10
|
import { named as packageOf } from '../../core/scope.ts'
|
|
10
11
|
import { PREFIXES } from '../../core/thread.ts'
|
|
11
12
|
|
|
12
|
-
import type
|
|
13
|
-
import { servesBrowser, type Target } from '../../integrations/target.ts'
|
|
13
|
+
import { secrecyOf, type Secret } from '../../integrations/secret.ts'
|
|
14
|
+
import { reachOf, servesBrowser, standsHere, targetOf, type Call, type Target, type Webhook } from '../../integrations/target.ts'
|
|
14
15
|
|
|
15
16
|
import {
|
|
16
17
|
CAPABILITIES_AT,
|
|
17
18
|
CLIENT_AT,
|
|
18
|
-
DECLARED_SEAMS_AT,
|
|
19
19
|
MCP_AT,
|
|
20
20
|
OPENAPI_AT,
|
|
21
|
+
REACHING_AT,
|
|
22
|
+
ACTOR_AT,
|
|
23
|
+
DEFAULTS_AT,
|
|
21
24
|
DISPATCHING_AT,
|
|
22
|
-
|
|
25
|
+
inboundAt,
|
|
26
|
+
isSurface,
|
|
27
|
+
ROUTE,
|
|
23
28
|
routeAt,
|
|
24
29
|
SURFACES,
|
|
25
30
|
switched,
|
|
31
|
+
UNSTATED,
|
|
26
32
|
type Capability,
|
|
27
33
|
type Surface,
|
|
28
34
|
} from '../capabilities.ts'
|
|
29
35
|
import { ANONYMOUS, ENVIRONMENT } from '../../core/actor.ts'
|
|
30
36
|
import { PERMITS_AT } from '../access.ts'
|
|
31
|
-
import {
|
|
37
|
+
import { DISPATCH_AT } from '../dispatch.ts'
|
|
38
|
+
import { STREAMED, TOLD } from '../../integrations/https.ts'
|
|
39
|
+
import { holdsRow, listBounds, marked, nestedBounds, rowsIn, valuesIn, withField, withRow, withTopField, withoutRow } from '../../core/rows.ts'
|
|
40
|
+
import { said } from '../attach.ts'
|
|
41
|
+
import { MEMBERS_LIST } from '../operations.ts'
|
|
32
42
|
import { beside, operations, quoted, runtime, types } from './seed.ts'
|
|
33
43
|
|
|
34
44
|
interface Written {
|
|
@@ -38,6 +48,13 @@ interface Written {
|
|
|
38
48
|
|
|
39
49
|
const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
|
|
40
50
|
|
|
51
|
+
const listedIn = (held: ts.Expression | undefined): readonly string[] =>
|
|
52
|
+
held !== undefined && ts.isArrayLiteralExpression(held)
|
|
53
|
+
? held.elements.filter((one) => ts.isStringLiteral(one)).map((one) => (one as ts.StringLiteral).text)
|
|
54
|
+
: []
|
|
55
|
+
|
|
56
|
+
const trueIn = (held: ts.Expression | undefined): boolean => held !== undefined && held.kind === ts.SyntaxKind.TrueKeyword
|
|
57
|
+
|
|
41
58
|
function stable(value: unknown): unknown {
|
|
42
59
|
if (Array.isArray(value)) return value.map(stable)
|
|
43
60
|
if (typeof value === 'object' && value !== null) {
|
|
@@ -97,17 +114,28 @@ function browsing(requires: readonly string[], targets: readonly Target[], secre
|
|
|
97
114
|
return true
|
|
98
115
|
}
|
|
99
116
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
117
|
+
export interface Surfaced {
|
|
118
|
+
readonly live: readonly Surface[]
|
|
119
|
+
readonly stated: boolean
|
|
120
|
+
readonly wrong: readonly string[]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function defaulted(): Surfaced | undefined {
|
|
124
|
+
const at = resolve(ROOT, DEFAULTS_AT)
|
|
125
|
+
if (!existsSync(at)) return undefined
|
|
126
|
+
const row = Object.values(valuesIn(readFileSync(at, 'utf8'))).find(
|
|
127
|
+
(one): one is { readonly live?: unknown } =>
|
|
128
|
+
typeof one === 'object' && one !== null && (one as { readonly at?: unknown }).at === DEFAULTS_AT,
|
|
129
|
+
)
|
|
130
|
+
if (row === undefined || !Array.isArray(row.live)) return { live: UNSTATED, stated: false, wrong: [] }
|
|
131
|
+
const given = row.live.map(String)
|
|
132
|
+
return { live: SURFACES.filter((each) => given.includes(each)), stated: true, wrong: given.filter((each) => !isSurface(each)) }
|
|
106
133
|
}
|
|
107
134
|
|
|
108
135
|
export async function derive(): Promise<{
|
|
109
136
|
readonly found: readonly Capability[]
|
|
110
137
|
readonly refusals: readonly string[]
|
|
138
|
+
readonly live: readonly Surface[]
|
|
111
139
|
}> {
|
|
112
140
|
const pass = descend()
|
|
113
141
|
const known = types(pass)
|
|
@@ -117,11 +145,35 @@ export async function derive(): Promise<{
|
|
|
117
145
|
const targets = await sheet<Target>('targets.ts', 'TARGETS')
|
|
118
146
|
const secrets = await sheet<Secret>('secrets.ts', 'SECRETS')
|
|
119
147
|
|
|
120
|
-
const
|
|
121
|
-
|
|
148
|
+
for (const one of targets) {
|
|
149
|
+
if (reachOf(one.id) !== 'sdk' || (one.declares ?? []).length > 0) continue
|
|
150
|
+
refusals.push(
|
|
151
|
+
`\`${one.id}\` calls a package inside this app, and its row declares no file that does the calling. That file is the one ` +
|
|
152
|
+
'place the package is imported, and it exports `calls`, holding the function each call names as its path. Give the row ' +
|
|
153
|
+
`one, and the file with it, with \`thread edit ${one.id} --declares <id> --at <path> --describes <sentence> --body <text>\``,
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const tree = defaulted()
|
|
158
|
+
if (tree === undefined) {
|
|
159
|
+
refusals.push(
|
|
160
|
+
`${DEFAULTS_AT} is not here, so nothing says which surfaces an operation answers on, and working it out anyway would ` +
|
|
161
|
+
`take away every route this app has. \`thread attach ${packageOf('operations')}\` writes it`,
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
for (const one of tree?.wrong ?? []) {
|
|
165
|
+
refusals.push(
|
|
166
|
+
`${DEFAULTS_AT} says \`${one}\` is live, and there is no such surface. There are ${String(SURFACES.length)}: ` +
|
|
167
|
+
`${SURFACES.join(', ')}. \`thread create capability edit defaults\` rewrites it`,
|
|
168
|
+
)
|
|
169
|
+
}
|
|
122
170
|
|
|
123
171
|
for (const [id, one] of [...operations(pass, known)].sort((a, b) => compare(a[0], b[0]))) {
|
|
124
172
|
const stated = pass.stated.get(id)
|
|
173
|
+
const declared = {
|
|
174
|
+
webhooks: listedIn(stated?.fields.get('webhooks')),
|
|
175
|
+
signed: trueIn(stated?.fields.get('signed')),
|
|
176
|
+
}
|
|
125
177
|
const describes = stated?.describes
|
|
126
178
|
if (describes === undefined) {
|
|
127
179
|
refusals.push(`\`${id}\` states no \`describes\`, and a caller told nothing about an operation cannot choose it`)
|
|
@@ -148,7 +200,17 @@ export async function derive(): Promise<{
|
|
|
148
200
|
continue
|
|
149
201
|
}
|
|
150
202
|
|
|
151
|
-
const
|
|
203
|
+
const off = listedIn(stated?.fields.get('off'))
|
|
204
|
+
const stray = off.filter((each) => !isSurface(each))
|
|
205
|
+
if (stray.length > 0) {
|
|
206
|
+
refusals.push(
|
|
207
|
+
`\`${id}\` turns off ${stray.map((each) => `\`${each}\``).join(', ')}, and there is no such surface. There are ` +
|
|
208
|
+
`${String(SURFACES.length)}: ${SURFACES.join(', ')}. \`thread create capability edit ${id}\` rewrites it`,
|
|
209
|
+
)
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const live = (tree?.live ?? UNSTATED).filter((each) => !off.includes(each))
|
|
152
214
|
|
|
153
215
|
found.push({
|
|
154
216
|
operation: id,
|
|
@@ -157,15 +219,66 @@ export async function derive(): Promise<{
|
|
|
157
219
|
nodes: one.nodes.map((each) => each.id ?? '').filter((each) => each !== ''),
|
|
158
220
|
input: schema,
|
|
159
221
|
live: [...live].sort(compare) as readonly Surface[],
|
|
222
|
+
webhooks: [...declared.webhooks].sort(compare),
|
|
223
|
+
signed: declared.signed,
|
|
160
224
|
browsable: browsing(one.requires, targets, secrets),
|
|
161
|
-
local: browsing(one.requires, targets, secrets) && locally(one.requires),
|
|
162
225
|
mcp: { name: id, description: describes, inputSchema: schema },
|
|
163
226
|
cli: { invocation: `${id} --input '<json>'`, describes },
|
|
164
227
|
api: { method, path: `/operations/${id}` },
|
|
165
228
|
})
|
|
166
229
|
}
|
|
167
230
|
|
|
168
|
-
|
|
231
|
+
for (const one of await sheet<Webhook>('targets.ts', 'WEBHOOKS')) {
|
|
232
|
+
const runs = found.find((each) => each.operation === one.invokes)
|
|
233
|
+
if (runs === undefined) {
|
|
234
|
+
refusals.push(
|
|
235
|
+
`\`${one.id}\` invokes \`${one.invokes}\`, and this app declares no operation by that name. A webhook is a way in ` +
|
|
236
|
+
`to something already declared, so declare that operation or point this webhook at one that exists`,
|
|
237
|
+
)
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
const route = (one.derives ?? []).some((each) => each.at === inboundAt(one.id) && each.by !== undefined && each.runs !== undefined)
|
|
241
|
+
if (!route) {
|
|
242
|
+
refusals.push(
|
|
243
|
+
`\`${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 ` +
|
|
244
|
+
`this tree declares: state a \`derives\` on this webhook naming ${inboundAt(one.id)}, with \`by\` and \`runs\` set to ` +
|
|
245
|
+
'`thread capabilities` and `thread capabilities --check`, so what is written there is checked against what writes it',
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
if (!runs.webhooks.includes(one.id)) {
|
|
249
|
+
refusals.push(
|
|
250
|
+
`\`${one.id}\` invokes \`${one.invokes}\`, and that operation does not name it back. An operation is run from outside ` +
|
|
251
|
+
`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`,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
if (runs.signed && (one.arrives === undefined || (one.requires ?? []).length === 0)) {
|
|
255
|
+
refusals.push(
|
|
256
|
+
`\`${one.invokes}\` says it will only be run by a signed webhook, and \`${one.id}\` carries no signature to check. ` +
|
|
257
|
+
`Give this webhook the header its signature arrives on and the secret it is signed with`,
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
if (!runs.live.includes('webhook')) {
|
|
261
|
+
refusals.push(
|
|
262
|
+
`\`${one.id}\` invokes \`${one.invokes}\`, and that operation does not answer on the webhook surface. Both halves have to ` +
|
|
263
|
+
`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`,
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
const holds = (one.requires ?? []).filter((each) => secrecyOf(each) !== undefined)
|
|
267
|
+
if (one.arrives !== undefined && holds.length === 0) {
|
|
268
|
+
refusals.push(
|
|
269
|
+
`\`${one.id}\` says a signature arrives on \`${one.arrives}\` and requires no secret to check it against, so every ` +
|
|
270
|
+
`request would be taken on trust. Name the secret it is signed with, or take \`arrives\` off the row`,
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
if (one.arrives === undefined && holds.length > 0) {
|
|
274
|
+
refusals.push(
|
|
275
|
+
`\`${one.id}\` requires ${holds.join(', ')} and says no header a signature arrives on, so nothing would ever be checked ` +
|
|
276
|
+
`against it. Say which header carries the signature, or take the secret off the row`,
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { found, refusals, live: tree?.live ?? UNSTATED }
|
|
169
282
|
}
|
|
170
283
|
|
|
171
284
|
function named(): { readonly title: string; readonly version: string } {
|
|
@@ -250,11 +363,11 @@ function client(found: readonly Capability[]): string {
|
|
|
250
363
|
].join('\n'),
|
|
251
364
|
)
|
|
252
365
|
|
|
253
|
-
const near = found.filter((one) => one.
|
|
366
|
+
const near = found.filter((one) => one.browsable && compiled(one.operation))
|
|
254
367
|
|
|
255
368
|
const bound = near.flatMap((one) => [
|
|
256
|
-
|
|
257
|
-
` const held = await dispatch(${one.operation}, input, actor,
|
|
369
|
+
'export async function ' + one.operation + 'Locally(input: unknown, actor: Actor): Promise<Result<unknown>> {',
|
|
370
|
+
` const held = await dispatch(${one.operation}, input, actor, DECLARED, opening, counting)`,
|
|
258
371
|
' return held',
|
|
259
372
|
'}',
|
|
260
373
|
'',
|
|
@@ -266,9 +379,9 @@ function client(found: readonly Capability[]): string {
|
|
|
266
379
|
: [
|
|
267
380
|
`import type { Actor } from '${runtime(CLIENT_AT, '../../core/actor.ts')}'`,
|
|
268
381
|
`import type { Result } from '${runtime(CLIENT_AT, '../result.ts')}'`,
|
|
269
|
-
`import type { Seams } from '${runtime(CLIENT_AT, '../seams.ts')}'`,
|
|
270
382
|
'',
|
|
271
383
|
"import { dispatch } from './_dispatch.ts'",
|
|
384
|
+
`import { counting, DECLARED, opening } from '${beside(CLIENT_AT, REACHING_AT)}'`,
|
|
272
385
|
...near.map((one) => `import { operation as ${one.operation} } from './${one.operation}.ts'`),
|
|
273
386
|
'',
|
|
274
387
|
]),
|
|
@@ -292,12 +405,13 @@ function client(found: readonly Capability[]): string {
|
|
|
292
405
|
function route(one: Capability): string {
|
|
293
406
|
const at = routeAt(one.operation)
|
|
294
407
|
return [
|
|
295
|
-
|
|
408
|
+
`import { existsSync } from ${quoted(runtime(at, '../../integrations/disk.ts'))}`,
|
|
296
409
|
"import { join } from 'node:path'",
|
|
297
410
|
'',
|
|
411
|
+
'import { counting, DECLARED, opening } from ' + quoted(beside(at, REACHING_AT)),
|
|
412
|
+
'import { actor } from ' + quoted(beside(at, ACTOR_AT)),
|
|
413
|
+
'',
|
|
298
414
|
"import { dispatch } from '../../../operations/_dispatch.ts'",
|
|
299
|
-
`import { actor } from '${beside(at, DECLARED_SEAMS_AT)}'`,
|
|
300
|
-
`import { SEAMS } from '${beside(at, SEAMS_AT)}'`,
|
|
301
415
|
`import { operation } from '../../../operations/${one.operation}.ts'`,
|
|
302
416
|
'',
|
|
303
417
|
'const STATUS: Record<string, number> = {',
|
|
@@ -320,15 +434,111 @@ function route(one: Capability): string {
|
|
|
320
434
|
' }',
|
|
321
435
|
...(one.api.method === 'GET'
|
|
322
436
|
? [' const input = Object.fromEntries(new URL(request.url).searchParams)']
|
|
323
|
-
: [
|
|
324
|
-
|
|
437
|
+
: [
|
|
438
|
+
' const input = await request.json().catch(() => undefined)',
|
|
439
|
+
" if ((request.headers.get('accept') ?? '').includes(" + quoted(STREAMED) + ')) {',
|
|
440
|
+
' const lines = new TransformStream<string, string>()',
|
|
441
|
+
' const writing = lines.writable.getWriter()',
|
|
442
|
+
" const sent = (said: unknown): void => { writing.write(JSON.stringify(said) + '\\n').catch(() => undefined) }",
|
|
443
|
+
' void dispatch(operation, input, await actor(request), DECLARED, opening, counting, (said) => sent({ ' + TOLD + ': said })).then((held) => {',
|
|
444
|
+
' sent(held)',
|
|
445
|
+
' return writing.close()',
|
|
446
|
+
' }).catch(() => undefined)',
|
|
447
|
+
' return new Response(lines.readable.pipeThrough(new TextEncoderStream()), { headers: { ' + quoted('content-type') + ': ' + quoted(STREAMED) + ' } })',
|
|
448
|
+
' }',
|
|
449
|
+
]),
|
|
450
|
+
' const held = await dispatch(operation, input, await actor(request), DECLARED, opening, counting)',
|
|
325
451
|
' return Response.json(held, { status: held.ok ? 200 : (STATUS[held.code] ?? 500) })',
|
|
326
452
|
'}',
|
|
327
453
|
'',
|
|
328
454
|
].join('\n')
|
|
329
455
|
}
|
|
330
456
|
|
|
331
|
-
|
|
457
|
+
function inbound(one: Webhook): string {
|
|
458
|
+
const at = inboundAt(one.id)
|
|
459
|
+
const secret = (one.requires ?? [])[0]
|
|
460
|
+
const signed = one.arrives !== undefined && secret !== undefined
|
|
461
|
+
const refuse = (why: string): string =>
|
|
462
|
+
" return Response.json({ ok: false, code: " + quoted("denied") + ", error: " + quoted("not allowed") +
|
|
463
|
+
", because: " + quoted(why) + " }, { status: 403 })"
|
|
464
|
+
return [
|
|
465
|
+
...(signed ? ["import { createHmac, timingSafeEqual } from " + quoted("node:crypto")] : []),
|
|
466
|
+
"import { existsSync } from " + quoted(runtime(at, "../../integrations/disk.ts")),
|
|
467
|
+
"import { join } from " + quoted("node:path"),
|
|
468
|
+
"",
|
|
469
|
+
"import { counting, DECLARED, opening } from " + quoted(beside(at, REACHING_AT)),
|
|
470
|
+
"",
|
|
471
|
+
"import { dispatch } from " + quoted(beside(at, DISPATCH_AT)),
|
|
472
|
+
"import { operation } from " + quoted(beside(at, "operations/" + one.invokes + ".ts")),
|
|
473
|
+
"",
|
|
474
|
+
"const ARRIVING = " + JSON.stringify(ANONYMOUS),
|
|
475
|
+
"",
|
|
476
|
+
"const PASSED = existsSync(join(process.cwd(), " + quoted(".thread") + ", " + quoted("gate.json") + "))",
|
|
477
|
+
"",
|
|
478
|
+
"export async function POST(request: Request): Promise<Response> {",
|
|
479
|
+
" if (!PASSED) {",
|
|
480
|
+
refuse("this tree has no passing gate, so nothing in it may answer a request. Run thread gate"),
|
|
481
|
+
" }",
|
|
482
|
+
" const raw = await request.text()",
|
|
483
|
+
...(signed
|
|
484
|
+
? [
|
|
485
|
+
" const carried = request.headers.get(" + quoted(one.arrives as string) + ")",
|
|
486
|
+
" if (carried === null) {",
|
|
487
|
+
refuse("this webhook is signed and the request carried nothing on " + String(one.arrives)),
|
|
488
|
+
" }",
|
|
489
|
+
" const held = await opening.resolve([" + quoted(secret as string) + "])",
|
|
490
|
+
" const mine = Buffer.from(createHmac(" + quoted("sha256") + ", held[" + quoted(secret as string) +
|
|
491
|
+
"] ?? " + quoted("") + ").update(raw).digest(" + quoted("hex") + "))",
|
|
492
|
+
" const said = Buffer.from(carried)",
|
|
493
|
+
" if (said.length !== mine.length || !timingSafeEqual(said, mine)) {",
|
|
494
|
+
refuse("the signature on this request is not what " + String(secret) + " signs a body with"),
|
|
495
|
+
" }",
|
|
496
|
+
]
|
|
497
|
+
: []),
|
|
498
|
+
" const input: unknown = JSON.parse(raw)",
|
|
499
|
+
" const answer = await dispatch(operation, input, ARRIVING, DECLARED, opening, counting)",
|
|
500
|
+
" return Response.json(answer, { status: answer.ok ? 200 : 500 })",
|
|
501
|
+
"}",
|
|
502
|
+
"",
|
|
503
|
+
].join("\n")
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function declaring(webhooks: readonly Webhook[]): readonly string[] {
|
|
507
|
+
const sheet = resolve(ROOT, TREE + 'targets.ts')
|
|
508
|
+
if (!existsSync(sheet)) return []
|
|
509
|
+
let text = readFileSync(sheet, 'utf8')
|
|
510
|
+
const wrote: string[] = []
|
|
511
|
+
for (const one of webhooks) {
|
|
512
|
+
const id = one.id + '_route'
|
|
513
|
+
const where = listBounds(text, 'WEBHOOKS')
|
|
514
|
+
if (where === undefined) break
|
|
515
|
+
const inside = nestedBounds(text, where, one.id, 'derives')
|
|
516
|
+
if (inside !== undefined && holdsRow(text, inside, id)) continue
|
|
517
|
+
const row = [
|
|
518
|
+
' {',
|
|
519
|
+
" id: " + quoted(id) + ",",
|
|
520
|
+
" at: " + quoted(inboundAt(one.id)) + ",",
|
|
521
|
+
" describes: " + quoted(
|
|
522
|
+
'The file that answers what ' + one.id + ' posts. It checks the signature before anything runs and then hands the body ' +
|
|
523
|
+
'to the same dispatch every other caller reaches, so an inbound event is refused or dispatched and never anything else.',
|
|
524
|
+
) + ",",
|
|
525
|
+
' version: 1,',
|
|
526
|
+
" by: 'thread capabilities',",
|
|
527
|
+
" runs: 'thread capabilities --check',",
|
|
528
|
+
' },',
|
|
529
|
+
].join('\n')
|
|
530
|
+
const next = inside === undefined
|
|
531
|
+
? withField(text, where, one.id, 'derives', '[\n' + row + '\n ]')
|
|
532
|
+
: withRow(text, inside, row)
|
|
533
|
+
if (next === undefined || next === text) continue
|
|
534
|
+
text = next
|
|
535
|
+
wrote.push(id)
|
|
536
|
+
}
|
|
537
|
+
if (wrote.length > 0) writeFileSync(sheet, text, 'utf8')
|
|
538
|
+
return wrote
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const STORED = /export\s+const\s+store\b/
|
|
332
542
|
|
|
333
543
|
const STORES_AT = 'src/stores'
|
|
334
544
|
|
|
@@ -339,7 +549,7 @@ function kept(): readonly string[] {
|
|
|
339
549
|
.filter((one) => one.endsWith('.ts'))
|
|
340
550
|
.filter((one) => {
|
|
341
551
|
try {
|
|
342
|
-
return
|
|
552
|
+
return STORED.test(readFileSync(resolve(at, one), 'utf8'))
|
|
343
553
|
} catch {
|
|
344
554
|
return false
|
|
345
555
|
}
|
|
@@ -354,169 +564,6 @@ const OPERATIONS_AT = `${TREE}operations.ts`
|
|
|
354
564
|
|
|
355
565
|
const VAULT_AT = `src/vault.${'ts'}`
|
|
356
566
|
|
|
357
|
-
function settled(keeping: boolean, stores: readonly string[], ruled: boolean, vaulted: boolean): string {
|
|
358
|
-
const named = stores.map((one, at) => ({ from: beside(DECLARED_SEAMS_AT, one), as: `s${String(at)}` }))
|
|
359
|
-
const only = named[0]
|
|
360
|
-
return [
|
|
361
|
-
`import type { Actor, Environment } from '${runtime(DECLARED_SEAMS_AT, '../../core/actor.ts')}'`,
|
|
362
|
-
`import type { Seams } from '${runtime(DECLARED_SEAMS_AT, '../seams.ts')}'`,
|
|
363
|
-
...(keeping ? [`import { ENTITIES } from '${beside(DECLARED_SEAMS_AT, ENTITIES_AT)}'`] : []),
|
|
364
|
-
...(ruled ? [`import { permits } from '${beside(DECLARED_SEAMS_AT, PERMITS_AT)}'`] : []),
|
|
365
|
-
...(vaulted ? [`import { vault as opening } from '${beside(DECLARED_SEAMS_AT, VAULT_AT)}'`] : []),
|
|
366
|
-
...(stores.length === 1 && only !== undefined ? [`import { store as kept } from '${only.from}'`] : []),
|
|
367
|
-
...(stores.length > 1 ? named.map((one) => `import { keeps as ${one.as}Keeps, store as ${one.as} } from '${one.from}'`) : []),
|
|
368
|
-
'',
|
|
369
|
-
...(keeping
|
|
370
|
-
? [
|
|
371
|
-
'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 })) } },", '}',
|
|
372
|
-
]
|
|
373
|
-
: ['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 })) } },", '}']),
|
|
374
|
-
'',
|
|
375
|
-
...(stores.length > 1
|
|
376
|
-
? [
|
|
377
|
-
"const WHERE = new Map<string, Seams['store']>([",
|
|
378
|
-
...named.map((one) => ` ...${one.as}Keeps.map((entity) => [entity, ${one.as}] as const),`),
|
|
379
|
-
'])',
|
|
380
|
-
'',
|
|
381
|
-
'function at(entity: string) {',
|
|
382
|
-
' const found = WHERE.get(entity)',
|
|
383
|
-
" if (found === undefined) throw new Error(entity + ' is not kept in any target this app declares')",
|
|
384
|
-
' return found',
|
|
385
|
-
'}',
|
|
386
|
-
'',
|
|
387
|
-
"const store: Seams['store'] = {",
|
|
388
|
-
' insert: (actor, entity, row) => at(entity).insert(actor, entity, row),',
|
|
389
|
-
' get: (actor, entity, key, fields) => at(entity).get(actor, entity, key, fields),',
|
|
390
|
-
' list: (actor, entity, opts) => at(entity).list(actor, entity, opts),',
|
|
391
|
-
' update: (actor, entity, key, patch, expectedVersion) => at(entity).update(actor, entity, key, patch, expectedVersion),',
|
|
392
|
-
' remove: (actor, entity, key) => at(entity).remove(actor, entity, key),',
|
|
393
|
-
'}',
|
|
394
|
-
'',
|
|
395
|
-
]
|
|
396
|
-
: []),
|
|
397
|
-
`const ANONYMOUS: Actor = ${JSON.stringify(ANONYMOUS)}`,
|
|
398
|
-
'',
|
|
399
|
-
"const http: NonNullable<Seams['http']> = {",
|
|
400
|
-
' async call(request) {',
|
|
401
|
-
' const base = new URL(request.address)',
|
|
402
|
-
" if (base.protocol !== 'https:') throw new Error(request.target + ' is reached over ' + base.protocol + ' and an outward call goes over https only')",
|
|
403
|
-
' const stem = base.pathname.length === 1 ? String() : base.pathname',
|
|
404
|
-
' const url = new URL(base.origin + stem + request.path)',
|
|
405
|
-
" if (url.origin !== base.origin) throw new Error(request.path + ' resolves to ' + url.origin + ', which is not where ' + request.target + ' is')",
|
|
406
|
-
' const headers: Record<string, string> = { ...request.headers }',
|
|
407
|
-
" if (request.contentType !== undefined) headers['content-type'] = request.contentType",
|
|
408
|
-
' const carried = new Uint8Array(request.body === undefined ? 0 : request.body.byteLength)',
|
|
409
|
-
' if (request.body !== undefined) carried.set(request.body)',
|
|
410
|
-
' const began = Date.now()',
|
|
411
|
-
' const sent = JSON.stringify(headers).length + (request.body === undefined ? 0 : request.body.byteLength)',
|
|
412
|
-
' const answered = await fetch(url.toString(), { method: request.method, headers, ...(request.body === undefined ? {} : { body: carried }), signal: AbortSignal.timeout(10000) })',
|
|
413
|
-
' const body = new Uint8Array(await answered.arrayBuffer())',
|
|
414
|
-
' return { status: answered.status, headers: Object.fromEntries(answered.headers.entries()), body, bytesSent: sent, bytesReceived: body.byteLength, durationMs: Date.now() - began }',
|
|
415
|
-
' },',
|
|
416
|
-
'}',
|
|
417
|
-
'',
|
|
418
|
-
'const shared = globalThis as { __threadSeams?: Seams }',
|
|
419
|
-
'',
|
|
420
|
-
'export const DECLARED: Seams = (shared.__threadSeams ??= {',
|
|
421
|
-
' ...made,',
|
|
422
|
-
...(stores.length === 1 ? [' store: kept,'] : []),
|
|
423
|
-
...(stores.length > 1 ? [' store,'] : []),
|
|
424
|
-
' clock: { now: () => Date.now() },',
|
|
425
|
-
' random: { next: Math.random, uuid: () => crypto.randomUUID() },',
|
|
426
|
-
' http,',
|
|
427
|
-
...(ruled ? [' permits: { check: (actor, action, resource) => Promise.resolve(permits(actor, action, resource)) },'] : []),
|
|
428
|
-
...(vaulted ? [' vault: { resolve: opening },'] : []),
|
|
429
|
-
'})',
|
|
430
|
-
'',
|
|
431
|
-
'function environment(request: Request): Environment {',
|
|
432
|
-
...ENVIRONMENT.map(([fact, header]) => ` const ${fact} = request.headers.get(${quoted(header)})`),
|
|
433
|
-
' return {',
|
|
434
|
-
...ENVIRONMENT.map(([fact]) => ` ...(${fact} === null ? {} : { ${fact} }),`),
|
|
435
|
-
' }',
|
|
436
|
-
'}',
|
|
437
|
-
'',
|
|
438
|
-
...(keeping
|
|
439
|
-
? [
|
|
440
|
-
'const worn = (held: unknown): readonly string[] =>',
|
|
441
|
-
" Array.isArray(held) ? held.filter((one) => typeof one === 'string') : typeof held === 'string' ? [held] : []",
|
|
442
|
-
'',
|
|
443
|
-
'function addressed(request: Request, from: string): string | null {',
|
|
444
|
-
" if (from.startsWith('cookie:')) {",
|
|
445
|
-
" const jar = request.headers.get('cookie') ?? ''",
|
|
446
|
-
" const want = `${from.slice('cookie:'.length).trim()}=`",
|
|
447
|
-
" const found = jar.split(';').map((one) => one.trim()).find((one) => one.startsWith(want))",
|
|
448
|
-
' return found === undefined ? null : decodeURIComponent(found.slice(want.length))',
|
|
449
|
-
' }',
|
|
450
|
-
' const held = request.headers.get(from)',
|
|
451
|
-
' if (held === null) return null',
|
|
452
|
-
" const bare = held.startsWith('Bearer ') ? held.slice('Bearer '.length) : held",
|
|
453
|
-
" return bare.trim() === '' ? null : bare.trim()",
|
|
454
|
-
'}',
|
|
455
|
-
'',
|
|
456
|
-
'export async function actor(request: Request): Promise<Actor> {',
|
|
457
|
-
' const anyone: Actor = { ...ANONYMOUS, ...environment(request) }',
|
|
458
|
-
' const who = ENTITIES.find((one) => one.actor !== undefined)',
|
|
459
|
-
' if (who?.actor === undefined) return anyone',
|
|
460
|
-
'',
|
|
461
|
-
" const address = (who.must ?? []).flatMap((one) => ('unique' in one ? one.unique : []))[0]",
|
|
462
|
-
' if (address === undefined) return anyone',
|
|
463
|
-
'',
|
|
464
|
-
' const held = addressed(request, who.actor.from)',
|
|
465
|
-
' if (held === null) return anyone',
|
|
466
|
-
'',
|
|
467
|
-
' const token = who.actor.verifies',
|
|
468
|
-
'',
|
|
469
|
-
' const row =',
|
|
470
|
-
' token === undefined',
|
|
471
|
-
' ? await DECLARED.store.get(anyone, who.id, held, [',
|
|
472
|
-
' address,',
|
|
473
|
-
' who.actor.roles,',
|
|
474
|
-
' ...(who.actor.entitlements === undefined ? [] : [who.actor.entitlements]),',
|
|
475
|
-
' ])',
|
|
476
|
-
' : ((await DECLARED.store.list(anyone, who.id, { where: { [token]: held }, limit: 1 }))[0] ?? null)',
|
|
477
|
-
' if (row === null) return anyone',
|
|
478
|
-
'',
|
|
479
|
-
' return {',
|
|
480
|
-
' ...anyone,',
|
|
481
|
-
' id: String(row[address] ?? held),',
|
|
482
|
-
' roles: worn(row[who.actor.roles]),',
|
|
483
|
-
' entitlements: who.actor.entitlements === undefined ? [] : worn(row[who.actor.entitlements]),',
|
|
484
|
-
' }',
|
|
485
|
-
'}',
|
|
486
|
-
'',
|
|
487
|
-
]
|
|
488
|
-
: [
|
|
489
|
-
'export const actor = (request: Request): Promise<Actor> => Promise.resolve({ ...ANONYMOUS, ...environment(request) })',
|
|
490
|
-
'',
|
|
491
|
-
]),
|
|
492
|
-
].join('\n')
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const seeded = (): string =>
|
|
496
|
-
[
|
|
497
|
-
`import { DECLARED } from '${beside(SEAMS_AT, DECLARED_SEAMS_AT)}'`,
|
|
498
|
-
`import type { Seams } from '${runtime(SEAMS_AT, '../seams.ts')}'`,
|
|
499
|
-
'',
|
|
500
|
-
'export const SEAMS: Seams = {',
|
|
501
|
-
' ...DECLARED,',
|
|
502
|
-
'}',
|
|
503
|
-
'',
|
|
504
|
-
].join('\n')
|
|
505
|
-
|
|
506
|
-
const width = Math.max(...Object.keys(HANDED).map((one) => one.length))
|
|
507
|
-
|
|
508
|
-
const handing = (stores: readonly string[], keeping: boolean, vaulted: boolean): string =>
|
|
509
|
-
[
|
|
510
|
-
`capabilities: seeded ${SEAMS_AT} — what this app hands its operations, and yours from here.`,
|
|
511
|
-
` It hands on everything ${DECLARED_SEAMS_AT} settles from your declarations: rows go to ` +
|
|
512
|
-
(stores.length === 0 ? 'no store yet — no target keeps entities, so an operation that needs one is refused' : stores.join(', ')) +
|
|
513
|
-
`, who is asking is ${keeping ? 'read off the entity that names the caller' : 'nobody until an entity names the caller'}, ` +
|
|
514
|
-
`calls leave through http${vaulted ? ' with credentials opened by the vault' : ''}.`,
|
|
515
|
-
' Replace a seam, or add one, by naming it in SEAMS. Every seam an operation can be handed:',
|
|
516
|
-
...Object.entries(HANDED).map(([one, says]) => ` ${one.padEnd(width)} ${says}`),
|
|
517
|
-
'',
|
|
518
|
-
].join('\n')
|
|
519
|
-
|
|
520
567
|
const served = (one: Capability): boolean => one.live.includes('api') && compiled(one.operation)
|
|
521
568
|
|
|
522
569
|
const held = (one: Capability): boolean => one.browsable && one.live.includes('api')
|
|
@@ -525,8 +572,6 @@ function dispatching(found: readonly Capability[]): string {
|
|
|
525
572
|
|
|
526
573
|
const reachable = found.filter(served)
|
|
527
574
|
return [
|
|
528
|
-
'/* GENERATED by thread — do not edit. It is derived from what this app declares. */',
|
|
529
|
-
'',
|
|
530
575
|
`declare module '${packageOf('ui')}/ui/checked' {`,
|
|
531
576
|
...(reachable.length === 0
|
|
532
577
|
? [' interface Dispatching {}']
|
|
@@ -569,26 +614,312 @@ function dispatching(found: readonly Capability[]): string {
|
|
|
569
614
|
].join('\n')
|
|
570
615
|
}
|
|
571
616
|
|
|
572
|
-
|
|
617
|
+
export function reaching(
|
|
618
|
+
targets: readonly Target[],
|
|
619
|
+
calls: readonly Call[],
|
|
620
|
+
stores: readonly string[],
|
|
621
|
+
vaulted: boolean,
|
|
622
|
+
carried: boolean,
|
|
623
|
+
): string {
|
|
624
|
+
const taken = (one: Target): readonly string[] =>
|
|
625
|
+
calls.filter((each) => each.of === one.id && one.takes.includes(each.id)).map((each) => each.id.replace(/^cl_/, ''))
|
|
626
|
+
|
|
627
|
+
const rows = targets.map((one) => {
|
|
628
|
+
const mine = taken(one).map((each) => quoted(each))
|
|
629
|
+
return ' readonly ' + one.id + ': ' + (mine.length === 0 ? 'never' : mine.join(' | '))
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
const named = stores.map((one, at) => ({
|
|
633
|
+
from: beside(REACHING_AT, one),
|
|
634
|
+
as: 's' + String(at),
|
|
635
|
+
of: (one.split('/').pop() ?? '').replace(/\.ts$/, ''),
|
|
636
|
+
}))
|
|
637
|
+
|
|
638
|
+
const adapted = targets
|
|
639
|
+
.filter((one) => reachOf(one.id) === 'sdk')
|
|
640
|
+
.flatMap((one, at) => {
|
|
641
|
+
const file = (one.declares ?? [])[0]?.at
|
|
642
|
+
return file === undefined ? [] : [{ from: beside(REACHING_AT, file), as: 'c' + String(at), of: one.id }]
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
const opened = targets
|
|
646
|
+
.map((one) => {
|
|
647
|
+
const reach = reachOf(one.id)
|
|
648
|
+
if (reach === 'sdk') {
|
|
649
|
+
const held = adapted.find((each) => each.of === one.id)
|
|
650
|
+
if (held === undefined) return ''
|
|
651
|
+
const each = calls
|
|
652
|
+
.filter((call) => call.of === one.id && one.takes.includes(call.id))
|
|
653
|
+
.map((call) => quoted(call.id.replace(/^cl_/, '')) + ': ' + held.as + '[' + quoted(call.path) + ']')
|
|
654
|
+
return ' ' + one.id + ': { ' + each.join(', ') + ' },'
|
|
655
|
+
}
|
|
656
|
+
if (reach === 'disk') {
|
|
657
|
+
const through = one.requires.find((each) => reachOf(each) !== undefined)
|
|
658
|
+
return through === undefined ? ' ' + one.id + ': HERE,' : ''
|
|
659
|
+
}
|
|
660
|
+
if (reach === 'cli') {
|
|
661
|
+
const each = calls
|
|
662
|
+
.filter((call) => call.of === one.id && one.takes.includes(call.id))
|
|
663
|
+
.map((call) => quoted(call.id.replace(/^cl_/, '')) + ': ' + quoted(call.path))
|
|
664
|
+
return ' ' + one.id + ': running(' + quoted(one.id.replace(/^cli_/, '')) + ', { ' + each.join(', ') + ' }, process.cwd()),'
|
|
665
|
+
}
|
|
666
|
+
if (reach === 'https') return ' ' + one.id + ': sendingHttp(),'
|
|
667
|
+
const held = named.find((each) => each.of === one.id)
|
|
668
|
+
return held === undefined ? '' : ' ' + one.id + ': ' + held.as + ','
|
|
669
|
+
})
|
|
670
|
+
.filter((each) => each !== '')
|
|
671
|
+
|
|
672
|
+
return [
|
|
673
|
+
'import { HERE } from ' + quoted(runtime(REACHING_AT, '../../integrations/disk.ts')),
|
|
674
|
+
'import { running } from ' + quoted(runtime(REACHING_AT, '../../integrations/cli.ts')),
|
|
675
|
+
'import { sendingHttp } from ' + quoted(runtime(REACHING_AT, '../../integrations/https.ts')),
|
|
676
|
+
'import type { Counting } from ' +
|
|
677
|
+
quoted(carried ? packageOf('operations') + '/operations/operations' : runtime(REACHING_AT, '../operations.ts')),
|
|
678
|
+
...named.map((each) => 'import { store as ' + each.as + ' } from ' + quoted(each.from)),
|
|
679
|
+
...adapted.map((each) => 'import { calls as ' + each.as + ' } from ' + quoted(each.from)),
|
|
680
|
+
...(vaulted ? ['import { vault } from ' + quoted(beside(REACHING_AT, VAULT_AT))] : []),
|
|
681
|
+
'',
|
|
682
|
+
'declare module ' +
|
|
683
|
+
quoted(carried ? packageOf('operations') + '/operations/operations' : runtime(REACHING_AT, '../operations.ts')) +
|
|
684
|
+
' {',
|
|
685
|
+
...(rows.length === 0 ? [' interface Reaching {}'] : [' interface Reaching {', ...rows, ' }']),
|
|
686
|
+
'}',
|
|
687
|
+
'',
|
|
688
|
+
'export const DECLARED: Readonly<Record<string, unknown>> = {',
|
|
689
|
+
...opened,
|
|
690
|
+
'}',
|
|
691
|
+
'',
|
|
692
|
+
vaulted ? 'export const opening = { resolve: vault }' : 'export const opening = undefined',
|
|
693
|
+
'',
|
|
694
|
+
'export const counting: Counting | undefined = undefined',
|
|
695
|
+
'',
|
|
696
|
+
].join('\n')
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const reading = (header: string): string => 'request.headers.get(' + quoted(header) + ')'
|
|
700
|
+
|
|
701
|
+
const CARRIED = ENVIRONMENT.flatMap(([fact, header, fills]) => {
|
|
702
|
+
const taken = header === null ? fills : fills === null ? reading(header) : reading(header) + ' ?? ' + fills
|
|
703
|
+
return taken === null ? [] : [{ fact, taken, always: fills !== null }]
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
function caller(keeping: boolean, kept: string | undefined): string {
|
|
707
|
+
const looking = keeping && kept !== undefined
|
|
708
|
+
return [
|
|
709
|
+
'import type { Actor, Environment } from ' + quoted(runtime(ACTOR_AT, '../../core/actor.ts')),
|
|
710
|
+
...(looking ? ['import { ENTITIES } from ' + quoted(beside(ACTOR_AT, ENTITIES_AT))] : []),
|
|
711
|
+
...(looking ? ['import { DECLARED } from ' + quoted(beside(ACTOR_AT, REACHING_AT))] : []),
|
|
712
|
+
...(looking ? ['import type { Store } from ' + quoted(runtime(ACTOR_AT, '../../integrations/target.ts'))] : []),
|
|
713
|
+
'',
|
|
714
|
+
'const ANONYMOUS: Actor = ' + JSON.stringify(ANONYMOUS),
|
|
715
|
+
'',
|
|
716
|
+
'function environment(request: Request): Environment {',
|
|
717
|
+
...CARRIED.map((one) => ' const ' + one.fact + ' = ' + one.taken),
|
|
718
|
+
' return {',
|
|
719
|
+
...CARRIED.map((one) =>
|
|
720
|
+
one.always ? ' ' + one.fact + ',' : ' ...(' + one.fact + ' === null ? {} : { ' + one.fact + ' }),',
|
|
721
|
+
),
|
|
722
|
+
' }',
|
|
723
|
+
'}',
|
|
724
|
+
'',
|
|
725
|
+
...(looking
|
|
726
|
+
? [
|
|
727
|
+
'const worn = (held: unknown): readonly string[] =>',
|
|
728
|
+
' Array.isArray(held) ? held.filter((one) => typeof one === ' + quoted('string') + ') : typeof held === ' + quoted('string') + ' ? [held] : []',
|
|
729
|
+
'',
|
|
730
|
+
'function addressed(request: Request, from: string): string | null {',
|
|
731
|
+
' if (from.startsWith(' + quoted('cookie:') + ')) {',
|
|
732
|
+
' const jar = request.headers.get(' + quoted('cookie') + ') ?? ' + quoted(''),
|
|
733
|
+
' const want = from.slice(' + quoted('cookie:') + '.length).trim() + ' + quoted('='),
|
|
734
|
+
' const found = jar.split(' + quoted(';') + ').map((one) => one.trim()).find((one) => one.startsWith(want))',
|
|
735
|
+
' return found === undefined ? null : decodeURIComponent(found.slice(want.length))',
|
|
736
|
+
' }',
|
|
737
|
+
' const held = request.headers.get(from)',
|
|
738
|
+
' if (held === null) return null',
|
|
739
|
+
' const bare = held.startsWith(' + quoted('Bearer ') + ') ? held.slice(' + quoted('Bearer ') + '.length) : held',
|
|
740
|
+
' return bare.trim() === ' + quoted('') + ' ? null : bare.trim()',
|
|
741
|
+
'}',
|
|
742
|
+
'',
|
|
743
|
+
]
|
|
744
|
+
: []),
|
|
745
|
+
'export async function actor(request: Request): Promise<Actor> {',
|
|
746
|
+
' const anyone: Actor = { ...ANONYMOUS, ...environment(request) }',
|
|
747
|
+
...(looking
|
|
748
|
+
? [
|
|
749
|
+
' const who = ENTITIES.find((one) => one.actor !== undefined)',
|
|
750
|
+
' if (who?.actor === undefined) return anyone',
|
|
751
|
+
' const address = (who.must ?? []).flatMap((one) => (' + quoted('unique') + ' in one ? one.unique : []))[0]',
|
|
752
|
+
' if (address === undefined) return anyone',
|
|
753
|
+
' const held = addressed(request, who.actor.from)',
|
|
754
|
+
' if (held === null) return anyone',
|
|
755
|
+
' const keeper = DECLARED[' + quoted(kept ?? '') + '] as Store | undefined',
|
|
756
|
+
' if (keeper === undefined) return anyone',
|
|
757
|
+
' const token = who.actor.verifies',
|
|
758
|
+
' const row =',
|
|
759
|
+
' token === undefined',
|
|
760
|
+
' ? await keeper.get(anyone, who.id, held, [',
|
|
761
|
+
' address,',
|
|
762
|
+
' who.actor.roles,',
|
|
763
|
+
' ...(who.actor.entitlements === undefined ? [] : [who.actor.entitlements]),',
|
|
764
|
+
' ])',
|
|
765
|
+
' : ((await keeper.list(anyone, who.id, { where: { [token]: held }, limit: 1 }))[0] ?? null)',
|
|
766
|
+
' if (row === null) return anyone',
|
|
767
|
+
' return {',
|
|
768
|
+
' ...anyone,',
|
|
769
|
+
' id: String(row[address] ?? held),',
|
|
770
|
+
' roles: worn(row[who.actor.roles]),',
|
|
771
|
+
' entitlements: who.actor.entitlements === undefined ? [] : worn(row[who.actor.entitlements]),',
|
|
772
|
+
' }',
|
|
773
|
+
]
|
|
774
|
+
: [' return anyone']),
|
|
775
|
+
'}',
|
|
776
|
+
'',
|
|
777
|
+
].join('\n')
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const carries = (): boolean => existsSync(resolve(ROOT, 'node_modules', packageOf('operations')))
|
|
781
|
+
|
|
782
|
+
const installed = (): boolean => existsSync(resolve(ROOT, 'node_modules', packageOf('ui')))
|
|
573
783
|
|
|
574
|
-
const files = (
|
|
784
|
+
const files = (
|
|
785
|
+
found: readonly Capability[],
|
|
786
|
+
targets: readonly Target[],
|
|
787
|
+
calls: readonly Call[],
|
|
788
|
+
keeps: string | undefined,
|
|
789
|
+
webhooks: readonly Webhook[] = [],
|
|
790
|
+
): readonly Written[] => [
|
|
575
791
|
{ at: CAPABILITIES_AT, text: printed(found) },
|
|
576
792
|
{ at: OPENAPI_AT, text: printed(openapi(found)) },
|
|
577
793
|
{ at: MCP_AT, text: printed(mcp(found)) },
|
|
578
794
|
{ at: CLIENT_AT, text: client(found) },
|
|
579
795
|
|
|
796
|
+
{ at: REACHING_AT, text: reaching(targets, calls, kept(), existsSync(resolve(ROOT, VAULT_AT)), carries()) },
|
|
797
|
+
{ at: ACTOR_AT, text: caller(existsSync(resolve(ROOT, ENTITIES_AT)), keeps) },
|
|
580
798
|
...(installed() ? [{ at: DISPATCHING_AT, text: dispatching(found) }] : []),
|
|
581
799
|
...found.filter(served).map((one) => ({ at: routeAt(one.operation), text: route(one) })),
|
|
800
|
+
...webhooks
|
|
801
|
+
.filter((one) => found.some((each) => each.operation === one.invokes && each.live.includes('webhook') && each.webhooks.includes(one.id) && compiled(one.invokes)))
|
|
802
|
+
.flatMap((one) => {
|
|
803
|
+
const at = (one.derives ?? []).map((each) => each.at).find((each) => each === inboundAt(one.id))
|
|
804
|
+
return at === undefined ? [] : [{ at, text: inbound(one) }]
|
|
805
|
+
}),
|
|
582
806
|
]
|
|
583
807
|
|
|
808
|
+
interface Carried {
|
|
809
|
+
readonly live: readonly Surface[]
|
|
810
|
+
readonly off: ReadonlyMap<string, readonly Surface[]>
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function unmoved(): Carried | undefined {
|
|
814
|
+
const tree = defaulted()
|
|
815
|
+
if (tree === undefined || tree.stated) return undefined
|
|
816
|
+
const at = resolve(ROOT, CAPABILITIES_AT)
|
|
817
|
+
const switches = switched(existsSync(at) ? readFileSync(at, 'utf8') : undefined)
|
|
818
|
+
if (switches.size === 0) return undefined
|
|
819
|
+
const live = SURFACES.filter((each) => [...switches.values()].some((one) => one.includes(each)))
|
|
820
|
+
const off = new Map(
|
|
821
|
+
[...switches]
|
|
822
|
+
.map(([id, one]) => [id, live.filter((each) => !one.includes(each))] as const)
|
|
823
|
+
.filter(([, one]) => one.length > 0),
|
|
824
|
+
)
|
|
825
|
+
const same = off.size === 0 && live.length === UNSTATED.length && live.every((each) => UNSTATED.includes(each))
|
|
826
|
+
return same ? undefined : { live, off }
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export const asArray = (surfaces: readonly Surface[]): string => `[${surfaces.map((one) => quoted(one)).join(', ')}]`
|
|
830
|
+
|
|
831
|
+
function carry(plan: Carried): readonly string[] {
|
|
832
|
+
const at = resolve(ROOT, DEFAULTS_AT)
|
|
833
|
+
const put = withTopField(readFileSync(at, 'utf8'), 'live', asArray(plan.live))
|
|
834
|
+
if (put === undefined) return []
|
|
835
|
+
writeFileSync(at, put, 'utf8')
|
|
836
|
+
const sheet = resolve(ROOT, OPERATIONS_AT)
|
|
837
|
+
let text = readFileSync(sheet, 'utf8')
|
|
838
|
+
const told = [`${DEFAULTS_AT} is live on ${plan.live.join(', ') || 'nothing'}`]
|
|
839
|
+
for (const [id, off] of plan.off) {
|
|
840
|
+
const where = listBounds(text, MEMBERS_LIST)
|
|
841
|
+
const next = where === undefined ? undefined : withField(text, where, id, 'off', asArray(off))
|
|
842
|
+
if (next === undefined) continue
|
|
843
|
+
text = next
|
|
844
|
+
told.push(`${id} turns off ${off.join(', ')}`)
|
|
845
|
+
}
|
|
846
|
+
writeFileSync(sheet, text, 'utf8')
|
|
847
|
+
return told
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function routed(found: readonly Capability[], writing: boolean): readonly string[] {
|
|
851
|
+
const sheet = resolve(ROOT, OPERATIONS_AT)
|
|
852
|
+
if (!existsSync(sheet)) return []
|
|
853
|
+
let text = readFileSync(sheet, 'utf8')
|
|
854
|
+
const wrong: string[] = []
|
|
855
|
+
for (const one of found) {
|
|
856
|
+
const id = `${one.operation}_route`
|
|
857
|
+
const row = said(
|
|
858
|
+
{ id, at: routeAt(one.operation), describes: ROUTE, by: 'thread capabilities', runs: 'thread capabilities --check' },
|
|
859
|
+
' ',
|
|
860
|
+
)
|
|
861
|
+
const members = listBounds(text, MEMBERS_LIST)
|
|
862
|
+
const inside = members === undefined ? undefined : nestedBounds(text, members, one.operation, 'derives')
|
|
863
|
+
const standing = inside === undefined ? undefined : rowsIn(text, inside).find((each) => each.id === id)
|
|
864
|
+
const wants = one.live.includes('api')
|
|
865
|
+
if (wants ? standing?.source.trim() === row.trim() : standing === undefined) continue
|
|
866
|
+
wrong.push(
|
|
867
|
+
wants
|
|
868
|
+
? `${one.operation} answers on api and its row ${standing === undefined ? 'declares no route' : 'says something else about its route'}`
|
|
869
|
+
: `${one.operation} does not answer on api and its row still declares ${routeAt(one.operation)}`,
|
|
870
|
+
)
|
|
871
|
+
if (!writing) continue
|
|
872
|
+
let next = inside !== undefined && standing !== undefined ? withoutRow(text, inside, id) : text
|
|
873
|
+
if (wants) {
|
|
874
|
+
const list = listBounds(next, MEMBERS_LIST)
|
|
875
|
+
const within = list === undefined ? undefined : nestedBounds(next, list, one.operation, 'derives')
|
|
876
|
+
const put =
|
|
877
|
+
within !== undefined
|
|
878
|
+
? withRow(next, within, row)
|
|
879
|
+
: list === undefined
|
|
880
|
+
? undefined
|
|
881
|
+
: withField(next, list, one.operation, 'derives', `[\n${row}\n ]`)
|
|
882
|
+
if (put !== undefined) next = put
|
|
883
|
+
}
|
|
884
|
+
text = next
|
|
885
|
+
}
|
|
886
|
+
if (writing && wrong.length > 0) writeFileSync(sheet, text, 'utf8')
|
|
887
|
+
return wrong
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const unserved = (found: readonly Capability[]): readonly string[] =>
|
|
891
|
+
found
|
|
892
|
+
.filter((one) => !one.live.includes('api') && existsSync(resolve(ROOT, routeAt(one.operation))))
|
|
893
|
+
.map((one) => routeAt(one.operation))
|
|
894
|
+
|
|
584
895
|
if (invoked(import.meta.url)) {
|
|
585
|
-
const
|
|
896
|
+
const words = argv.slice(2).filter((one) => !one.startsWith('-'))
|
|
897
|
+
if (words[0] !== undefined && words[0] !== 'read') {
|
|
898
|
+
console.error(
|
|
899
|
+
'capabilities: `' + words[0] + '` is not something this command does. `thread capabilities` writes what can be reached, and ' +
|
|
900
|
+
'`thread capabilities read <mark>` prints one. Nothing was written.',
|
|
901
|
+
)
|
|
902
|
+
exit(1)
|
|
903
|
+
}
|
|
904
|
+
if (!argv.includes('--check')) {
|
|
905
|
+
const declared = declaring(await sheet<Webhook>('targets.ts', 'WEBHOOKS'))
|
|
906
|
+
for (const one of declared) console.log(`capabilities: declared ${one} in ${TREE}targets.ts, so the route it writes has a row that says the address was agreed to`)
|
|
907
|
+
if (declared.length > 0) forget()
|
|
908
|
+
const pending = unmoved()
|
|
909
|
+
if (pending !== undefined) {
|
|
910
|
+
console.log(
|
|
911
|
+
`capabilities: carried what ${CAPABILITIES_AT} had switched onto the rows it is worked out from now — ` +
|
|
912
|
+
`${carry(pending).join('; ')}. Nothing in it is set by hand any more.`,
|
|
913
|
+
)
|
|
914
|
+
forget()
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
const { found, refusals, live } = await derive()
|
|
586
918
|
for (const one of refusals) console.error(`capabilities: ${one}`)
|
|
587
919
|
|
|
588
920
|
const mark = (one: Capability): string =>
|
|
589
921
|
marked([one.operation, one.describes, one.requires.join(','), one.nodes.join(','), one.live.join(',')])
|
|
590
922
|
|
|
591
|
-
const words = wordsIn(['read'])
|
|
592
923
|
const verb = words[0]
|
|
593
924
|
|
|
594
925
|
if (verb === 'read') {
|
|
@@ -609,7 +940,6 @@ if (invoked(import.meta.url)) {
|
|
|
609
940
|
stdout.write(
|
|
610
941
|
`${one.browsable ? 'may be called from a page' : 'server only — it reaches a target that needs a server secret'}\n`,
|
|
611
942
|
)
|
|
612
|
-
stdout.write(`${one.local ? 'runs where it is called, with nothing sent' : 'reaches a target over the network'}\n`)
|
|
613
943
|
stdout.write(`api: ${one.api.method} ${one.api.path}\n`)
|
|
614
944
|
stdout.write(`cli: ${one.cli.invocation}\n`)
|
|
615
945
|
stdout.write(`mcp: ${one.mcp.name} — ${one.mcp.description}\n`)
|
|
@@ -623,43 +953,22 @@ if (invoked(import.meta.url)) {
|
|
|
623
953
|
|
|
624
954
|
const keeping = existsSync(resolve(ROOT, ENTITIES_AT))
|
|
625
955
|
|
|
626
|
-
if (existsSync(resolve(ROOT, OPERATIONS_AT))) {
|
|
627
|
-
const stores = kept()
|
|
628
|
-
const vaulted = existsSync(resolve(ROOT, VAULT_AT))
|
|
629
|
-
const text = settled(keeping, stores, existsSync(resolve(ROOT, PERMITS_AT)), vaulted)
|
|
630
|
-
const at = resolve(ROOT, DECLARED_SEAMS_AT)
|
|
631
|
-
if ((existsSync(at) ? readFileSync(at, 'utf8') : '') !== text) {
|
|
632
|
-
if (checking) {
|
|
633
|
-
adrift += 1
|
|
634
|
-
console.error(
|
|
635
|
-
`capabilities: ${DECLARED_SEAMS_AT} is not what the declarations settle. Run \`thread capabilities\` and commit the result.`,
|
|
636
|
-
)
|
|
637
|
-
} else {
|
|
638
|
-
writeFileSync(at, text, 'utf8')
|
|
639
|
-
console.log(`capabilities: wrote ${DECLARED_SEAMS_AT} — the seams your targets, entities, grants and secrets settle.`)
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const seed = resolve(ROOT, SEAMS_AT)
|
|
644
|
-
if (!existsSync(seed)) {
|
|
645
|
-
if (checking) {
|
|
646
|
-
adrift += 1
|
|
647
|
-
console.error(
|
|
648
|
-
`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.`,
|
|
649
|
-
)
|
|
650
|
-
} else {
|
|
651
|
-
writeFileSync(seed, seeded(), 'utf8')
|
|
652
|
-
stdout.write(handing(stores, keeping, vaulted))
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
956
|
if (found.length === 0 && refusals.length === 0) {
|
|
658
957
|
console.log('capabilities: this app declares no operation, so nothing is reachable through one yet.')
|
|
659
958
|
exit(adrift > 0 ? 1 : 0)
|
|
660
959
|
}
|
|
661
960
|
|
|
662
|
-
const
|
|
961
|
+
const all = await sheet<Target>('targets.ts', 'TARGETS')
|
|
962
|
+
const people = await sheet<{ readonly id: string; readonly actor?: unknown }>('entities.ts', 'ENTITIES')
|
|
963
|
+
const one = people.find((each) => each.actor !== undefined)
|
|
964
|
+
const where = one === undefined ? undefined : targetOf(one.id, all)
|
|
965
|
+
const written = files(
|
|
966
|
+
found,
|
|
967
|
+
all,
|
|
968
|
+
await sheet<Call>('targets.ts', 'CALLS'),
|
|
969
|
+
where === undefined || typeof where === 'string' ? undefined : where.id,
|
|
970
|
+
await sheet<Webhook>('targets.ts', 'WEBHOOKS'),
|
|
971
|
+
)
|
|
663
972
|
|
|
664
973
|
if (checking) {
|
|
665
974
|
const drifted = written.filter((one) => {
|
|
@@ -671,7 +980,14 @@ if (invoked(import.meta.url)) {
|
|
|
671
980
|
`capabilities: ${one.at} is not what the operations derive. Run \`thread capabilities\` and commit the result.`,
|
|
672
981
|
)
|
|
673
982
|
}
|
|
674
|
-
const
|
|
983
|
+
const unsettled = [
|
|
984
|
+
...(unmoved() === undefined ? [] : [`${CAPABILITIES_AT} switches surfaces that no row states yet`]),
|
|
985
|
+
...routed(found, false),
|
|
986
|
+
...unserved(found).map((at) => `${at} is still on disk to be served, and its operation does not answer on api`),
|
|
987
|
+
]
|
|
988
|
+
for (const one of unsettled) console.error(`capabilities: ${one}. Run \`thread capabilities\` and commit the result.`)
|
|
989
|
+
adrift += unsettled.length
|
|
990
|
+
const off = found.filter((one) => one.live.length < live.length)
|
|
675
991
|
console.log(
|
|
676
992
|
`capabilities: ${String(found.length)} operation(s) reachable, ${String(off.length)} with a surface switched off, ` +
|
|
677
993
|
`${String(drifted.length + adrift)} finding(s).`,
|
|
@@ -686,6 +1002,19 @@ if (invoked(import.meta.url)) {
|
|
|
686
1002
|
exit(1)
|
|
687
1003
|
}
|
|
688
1004
|
|
|
1005
|
+
if (!installed()) {
|
|
1006
|
+
const gone = resolve(ROOT, DISPATCHING_AT)
|
|
1007
|
+
if (existsSync(gone)) rmSync(gone)
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
for (const one of routed(found, true)) console.log(`capabilities: ${one}, so ${OPERATIONS_AT} was changed to match`)
|
|
1011
|
+
for (const at of unserved(found)) {
|
|
1012
|
+
rmSync(resolve(ROOT, at))
|
|
1013
|
+
const folder = dirname(resolve(ROOT, at))
|
|
1014
|
+
if (readdirSync(folder).length === 0) rmSync(folder, { recursive: true, force: true })
|
|
1015
|
+
console.log(`capabilities: deleted ${at}, because its operation does not answer on api and nothing may serve it`)
|
|
1016
|
+
}
|
|
1017
|
+
|
|
689
1018
|
for (const one of written) {
|
|
690
1019
|
const path = resolve(ROOT, one.at)
|
|
691
1020
|
writeFileSync(path, one.text)
|