@prestack/thread 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
@@ -16,14 +16,13 @@ import { named as packageOf } from "../../core/scope.js";
16
16
  import { findTree, inTree, strayFiles } from "../../core/tree.js";
17
17
  import { unclassified, valued } from "../secret.js";
18
18
  import { refusals, servesBrowser } from "../target.js";
19
- import { SEALED_AT } from "../vault.js";
19
+ import { ALGORITHM, KEY_BYTES, KEY_NAME, SEALED_AT, SEALED_NAME, VAULT_DIR } from "../vault.js";
20
20
  const TREE = findTree();
21
21
  const AT = 'src/vault.ts';
22
22
  const here = (one) => relative(TREE.at, one).split('\\').join('/');
23
23
  const from = inTree(TREE, 'secrets.ts');
24
24
  const path = join(TREE.at, AT);
25
25
  const toward = relative(dirname(path), fileURLToPath(new URL('../vault.ts', import.meta.url))).split('\\').join('/');
26
- const opens = toward.startsWith('.') ? toward : `./${toward}`;
27
26
  const declares = relative(dirname(path), from).split('\\').join('/');
28
27
  const sheet = declares.startsWith('.') ? declares : `./${declares}`;
29
28
  function refuse(because) {
@@ -39,12 +38,43 @@ catch {
39
38
  refuse(`${here(from)} could not be read. Attach \`${packageOf('integrations')}\` first — an app states its secrets there.`);
40
39
  }
41
40
  const declared = secrets.map((one) => one.id).sort();
42
- const SOURCE = `import { opened } from '${opens}'
43
-
44
- import { SECRETS } from '${sheet}'
45
-
46
- export const vault = opened(process.cwd(), SECRETS.map((one) => one.id), process.env.NODE_ENV === 'production' ? 'production' : 'sandbox')
47
- `;
41
+ const SOURCE = [
42
+ "import { createDecipheriv } from 'node:crypto'",
43
+ "import { existsSync, readFileSync } from 'node:fs'",
44
+ "import { join } from 'node:path'",
45
+ "",
46
+ `import { SECRETS } from '${sheet}'`,
47
+ "",
48
+ "const ENV = process.env.NODE_ENV === 'production' ? 'production' : 'sandbox'",
49
+ "",
50
+ "export async function vault(refs: readonly string[]): Promise<Readonly<Record<string, string>>> {",
51
+ " if (refs.length === 0) return {}",
52
+ ` const at = join(process.cwd(), '${VAULT_DIR}', '${KEY_NAME}')`,
53
+ " if (!existsSync(at)) throw new Error('there is no vault key at ' + at + ', and nothing here can open a sealed value without it.')",
54
+ " const key = Buffer.from(readFileSync(at, 'utf8').trim(), 'base64')",
55
+ ` if (key.length !== ${String(KEY_BYTES)}) throw new Error('the vault key at ' + at + ' is not the size anything here was sealed under.')`,
56
+ ` const where = join(process.cwd(), '${VAULT_DIR}', '${SEALED_NAME}')`,
57
+ " if (!existsSync(where)) throw new Error('there is no sealed store at ' + where + ', so no declared secret has a value yet.')",
58
+ " const store = JSON.parse(readFileSync(where, 'utf8')) as Readonly<Record<string, Readonly<Record<string, string>> | undefined>>",
59
+ " const known = SECRETS.map((one) => one.id)",
60
+ " const out: Record<string, string> = {}",
61
+ " for (const named of refs) {",
62
+ " if (!known.includes(named)) throw new Error(named + ' is not a secret this app declares.')",
63
+ " const sealed = store[named]?.[ENV]",
64
+ " if (sealed === undefined) throw new Error(named + ' is declared and has no value sealed for ' + ENV + '.')",
65
+ " const parts = sealed.split('.').map((one) => Buffer.from(one, 'base64'))",
66
+ " const iv = parts[0]",
67
+ " const tag = parts[1]",
68
+ " const body = parts[2]",
69
+ " if (iv === undefined || tag === undefined || body === undefined) throw new Error(named + ' is not three base64 parts.')",
70
+ ` const decipher = createDecipheriv('${ALGORITHM}', key, iv)`,
71
+ " decipher.setAuthTag(tag)",
72
+ " out[named] = decipher.update(body).toString('utf8') + decipher.final('utf8')",
73
+ " }",
74
+ " return out",
75
+ "}",
76
+ "",
77
+ ].join("\n");
48
78
  async function taken(file, list) {
49
79
  const at = inTree(TREE, file);
50
80
  if (!existsSync(at))
@@ -144,21 +174,23 @@ if (checking) {
144
174
  ...wrong.map((one) => ` ${one}`),
145
175
  ].join('\n'));
146
176
  }
147
- if (!existsSync(path)) {
148
- refuse(`${AT} is not there, and it is what opens this app's sealed values. Run \`thread vault\` — it seeds the file ` +
149
- 'once, and from then on it is yours.');
177
+ const current = existsSync(path) ? readFileSync(path, 'utf8') : '';
178
+ if (current !== SOURCE) {
179
+ refuse(current === ''
180
+ ? `${AT} is declared and not on disk, and it is what opens this app's sealed values. Run \`thread vault\` and commit what it writes.`
181
+ : `${AT} is not what the secrets in ${here(from)} produce. A derived file that differs from its generator is a hand edit wearing a generated name. Run \`thread vault\` and commit what it writes.`);
150
182
  }
151
183
  stdout.write(`vault: ${AT} is seeded and opens ${opening}, read off ${here(from)} as they are declared.\n`);
152
184
  exit(0);
153
185
  }
154
- if (existsSync(path)) {
186
+ if (existsSync(path) && readFileSync(path, 'utf8') === SOURCE) {
155
187
  stdout.write(`vault: ${AT} is already yours — it opens ${opening}, read off ${here(from)} as they are declared.\n`);
156
188
  exit(0);
157
189
  }
158
190
  mkdirSync(dirname(path), { recursive: true });
159
191
  writeFileSync(path, SOURCE, 'utf8');
160
- stdout.write(`vault: seeded ${AT} — what opens a secret's value at the moment of use, and yours from here.\n` +
192
+ stdout.write(`vault: wrote ${AT} — what opens a secret's value at the moment of use.\n` +
161
193
  ` It opens the values sealed under ${SEALED_AT} for the secrets declared in ${here(from)}, and refuses every other name.\n` +
162
194
  ' `thread create secret` declares one and seals its value; nothing else ever takes a value.\n' +
163
- ' To open values from somewhere else in production, replace opened here with what reads them.\n');
195
+ ' It imports nothing of this framework: what a host runs holds none of the scaffolding that assembled it.\n');
164
196
  exit(0);
@@ -0,0 +1,212 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, join, relative } from 'node:path'
3
+ import { argv, stderr, stdout } from 'node:process'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ import ts from 'typescript'
7
+
8
+ import { exit } from '../../core/invoked.ts'
9
+ import { named as packageOf } from '../../core/scope.ts'
10
+ import { findTree, inTree, strayFiles } from '../../core/tree.ts'
11
+
12
+ import { unclassified, valued, type Secret } from '../secret.ts'
13
+ import { refusals, servesBrowser, type Call, type Target } from '../target.ts'
14
+ import { ALGORITHM, KEY_BYTES, KEY_NAME, SEALED_AT, SEALED_NAME, VAULT_DIR } from '../vault.ts'
15
+
16
+ const TREE = findTree()
17
+
18
+ const AT = 'src/vault.ts'
19
+
20
+ const here = (one: string): string => relative(TREE.at, one).split('\\').join('/')
21
+
22
+ const from = inTree(TREE, 'secrets.ts')
23
+
24
+ const path = join(TREE.at, AT)
25
+
26
+ const toward = relative(dirname(path), fileURLToPath(new URL('../vault.ts', import.meta.url))).split('\\').join('/')
27
+
28
+ const declares = relative(dirname(path), from).split('\\').join('/')
29
+
30
+ const sheet = declares.startsWith('.') ? declares : `./${declares}`
31
+
32
+ function refuse(because: string): never {
33
+ stderr.write(`vault: ${because}\n`)
34
+ exit(1)
35
+ }
36
+
37
+ let secrets: readonly Secret[] = []
38
+ try {
39
+ const taken = (await import(new URL(`file://${from}`).href)) as { SECRETS?: readonly Secret[] }
40
+ secrets = taken.SECRETS ?? []
41
+ } catch {
42
+ refuse(`${here(from)} could not be read. Attach \`${packageOf('integrations')}\` first — an app states its secrets there.`)
43
+ }
44
+
45
+ const declared = secrets.map((one) => one.id).sort()
46
+
47
+ const SOURCE = [
48
+ "import { createDecipheriv } from 'node:crypto'",
49
+ "import { existsSync, readFileSync } from 'node:fs'",
50
+ "import { join } from 'node:path'",
51
+ "",
52
+ `import { SECRETS } from '${sheet}'`,
53
+ "",
54
+ "const ENV = process.env.NODE_ENV === 'production' ? 'production' : 'sandbox'",
55
+ "",
56
+ "export async function vault(refs: readonly string[]): Promise<Readonly<Record<string, string>>> {",
57
+ " if (refs.length === 0) return {}",
58
+ ` const at = join(process.cwd(), '${VAULT_DIR}', '${KEY_NAME}')`,
59
+ " if (!existsSync(at)) throw new Error('there is no vault key at ' + at + ', and nothing here can open a sealed value without it.')",
60
+ " const key = Buffer.from(readFileSync(at, 'utf8').trim(), 'base64')",
61
+ ` if (key.length !== ${String(KEY_BYTES)}) throw new Error('the vault key at ' + at + ' is not the size anything here was sealed under.')`,
62
+ ` const where = join(process.cwd(), '${VAULT_DIR}', '${SEALED_NAME}')`,
63
+ " if (!existsSync(where)) throw new Error('there is no sealed store at ' + where + ', so no declared secret has a value yet.')",
64
+ " const store = JSON.parse(readFileSync(where, 'utf8')) as Readonly<Record<string, Readonly<Record<string, string>> | undefined>>",
65
+ " const known = SECRETS.map((one) => one.id)",
66
+ " const out: Record<string, string> = {}",
67
+ " for (const named of refs) {",
68
+ " if (!known.includes(named)) throw new Error(named + ' is not a secret this app declares.')",
69
+ " const sealed = store[named]?.[ENV]",
70
+ " if (sealed === undefined) throw new Error(named + ' is declared and has no value sealed for ' + ENV + '.')",
71
+ " const parts = sealed.split('.').map((one) => Buffer.from(one, 'base64'))",
72
+ " const iv = parts[0]",
73
+ " const tag = parts[1]",
74
+ " const body = parts[2]",
75
+ " if (iv === undefined || tag === undefined || body === undefined) throw new Error(named + ' is not three base64 parts.')",
76
+ ` const decipher = createDecipheriv('${ALGORITHM}', key, iv)`,
77
+ " decipher.setAuthTag(tag)",
78
+ " out[named] = decipher.update(body).toString('utf8') + decipher.final('utf8')",
79
+ " }",
80
+ " return out",
81
+ "}",
82
+ "",
83
+ ].join("\n")
84
+
85
+ async function taken<T>(file: string, list: string): Promise<readonly T[]> {
86
+ const at = inTree(TREE, file)
87
+ if (!existsSync(at)) return []
88
+ try {
89
+ const held = (await import(new URL(`file://${at}`).href)) as Record<string, readonly T[] | undefined>
90
+ return held[list] ?? []
91
+ } catch {
92
+ return []
93
+ }
94
+ }
95
+
96
+ function imported(at: string, text: string): readonly string[] {
97
+ const source = ts.createSourceFile(at, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
98
+ const found: string[] = []
99
+ const walk = (node: ts.Node): void => {
100
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
101
+ const said = node.moduleSpecifier
102
+ if (said !== undefined && ts.isStringLiteral(said)) found.push(said.text)
103
+ } else if (ts.isCallExpression(node)) {
104
+ const first = node.arguments[0]
105
+ const dynamic = node.expression.kind === ts.SyntaxKind.ImportKeyword
106
+ const legacy = ts.isIdentifier(node.expression) && node.expression.text === 'require'
107
+ if ((dynamic || legacy) && first !== undefined && ts.isStringLiteral(first)) found.push(first.text)
108
+ }
109
+ ts.forEachChild(node, walk)
110
+ }
111
+ walk(source)
112
+ return found
113
+ }
114
+
115
+ function names(specifier: string, id: string): boolean {
116
+ const bare = specifier.replace(/\.(?:m|c)?[jt]sx?$/, '')
117
+ return bare === `operations/${id}` || bare.endsWith(`/operations/${id}`)
118
+ }
119
+
120
+ async function reaching(): Promise<readonly string[]> {
121
+ const targets = await taken<Target>('targets.ts', 'TARGETS')
122
+ const withheld = new Map<string, string>()
123
+ for (const one of targets) {
124
+ const said = servesBrowser(one, secrets)
125
+ if (said !== true) withheld.set(one.id, said)
126
+ }
127
+ if (withheld.size === 0) return []
128
+
129
+ const operations = await taken<{ readonly id: string; readonly requires?: readonly string[] }>('operations.ts', 'MEMBERS')
130
+ const risky = new Map<string, string>()
131
+ for (const one of operations) {
132
+ for (const word of one.requires ?? []) {
133
+ const said = withheld.get(word)
134
+ if (said !== undefined) risky.set(one.id, said)
135
+ }
136
+ }
137
+ if (risky.size === 0) return []
138
+
139
+ const found: string[] = []
140
+ for (const one of strayFiles(TREE.at, '', '.tsx')) {
141
+ let text: string
142
+ try {
143
+ text = readFileSync(join(TREE.at, one), 'utf8')
144
+ } catch {
145
+ continue
146
+ }
147
+ const specifiers = imported(one, text)
148
+ for (const [id, said] of risky) {
149
+ if (!specifiers.some((each) => names(each, id))) continue
150
+ found.push(
151
+ `${one} is a \`.tsx\` file and it imports \`${id}\`. ${said} This holds whoever bundles it — ` +
152
+ 'a page is a page whether Next, Vite or anything else compiles it, so the refusal is about the ' +
153
+ 'import rather than about any one build. Call it from a server and let the page ask the server',
154
+ )
155
+ }
156
+ }
157
+ return found
158
+ }
159
+
160
+ async function stated(): Promise<readonly string[]> {
161
+ const targets = await taken<Target>('targets.ts', 'TARGETS')
162
+ const calls = await taken<Call>('targets.ts', 'CALLS')
163
+ const entities = (await taken<{ readonly id: string }>('entities.ts', 'ENTITIES')).map((one) => one.id)
164
+ return [
165
+ ...unclassified(secrets),
166
+ ...valued(secrets),
167
+ ...refusals(targets, secrets, calls, entities),
168
+ ...(await reaching()),
169
+ ]
170
+ }
171
+
172
+ const checking = argv.includes('--check')
173
+
174
+ const opening = `${String(declared.length)} declared secret(s)`
175
+
176
+ if (checking) {
177
+ const wrong = await stated()
178
+ if (wrong.length > 0) {
179
+ refuse(
180
+ [
181
+ `${String(wrong.length)} thing(s) this app states about what it reaches will not hold:`,
182
+ ...wrong.map((one) => ` ${one}`),
183
+ ].join('\n'),
184
+ )
185
+ }
186
+
187
+ const current = existsSync(path) ? readFileSync(path, 'utf8') : ''
188
+ if (current !== SOURCE) {
189
+ refuse(
190
+ current === ''
191
+ ? `${AT} is declared and not on disk, and it is what opens this app's sealed values. Run \`thread vault\` and commit what it writes.`
192
+ : `${AT} is not what the secrets in ${here(from)} produce. A derived file that differs from its generator is a hand edit wearing a generated name. Run \`thread vault\` and commit what it writes.`,
193
+ )
194
+ }
195
+ stdout.write(`vault: ${AT} is seeded and opens ${opening}, read off ${here(from)} as they are declared.\n`)
196
+ exit(0)
197
+ }
198
+
199
+ if (existsSync(path) && readFileSync(path, 'utf8') === SOURCE) {
200
+ stdout.write(`vault: ${AT} is already yours — it opens ${opening}, read off ${here(from)} as they are declared.\n`)
201
+ exit(0)
202
+ }
203
+
204
+ mkdirSync(dirname(path), { recursive: true })
205
+ writeFileSync(path, SOURCE, 'utf8')
206
+ stdout.write(
207
+ `vault: wrote ${AT} — what opens a secret's value at the moment of use.\n` +
208
+ ` It opens the values sealed under ${SEALED_AT} for the secrets declared in ${here(from)}, and refuses every other name.\n` +
209
+ ' `thread create secret` declares one and seals its value; nothing else ever takes a value.\n' +
210
+ ' It imports nothing of this framework: what a host runs holds none of the scaffolding that assembled it.\n',
211
+ )
212
+ exit(0)
@@ -40,9 +40,9 @@ export async function ran(name) {
40
40
  catch {
41
41
  return say('`_threads/targets.ts` could not be read, so nothing says what this integration is.');
42
42
  }
43
- const mine = targets.find((one) => one.id === `${REACH.cli}${name}` || one.id === `${REACH.sdk}${name}`);
43
+ const mine = targets.find((one) => one.id === `${REACH.cli}${name}` || one.id === `${REACH.module}${name}`);
44
44
  if (mine === undefined) {
45
- return say(`no target called \`${REACH.cli}${name}\` or \`${REACH.sdk}${name}\` is declared here, so this command should ` +
45
+ return say(`no target called \`${REACH.cli}${name}\` or \`${REACH.module}${name}\` is declared here, so this command should ` +
46
46
  'not exist. Declare it with `thread create target`, or take its row out of `_threads/derivation.ts`.');
47
47
  }
48
48
  const does = mine.offers ?? [];
@@ -60,7 +60,7 @@ export async function ran(name) {
60
60
  `was declared. It declared: ${does.join(', ') || '(nothing)'}. If the tool has changed, read it again with ` +
61
61
  `\`thread create target edit ${mine.id}\`.`);
62
62
  }
63
- if (reachOf(mine.id) === 'sdk') {
63
+ if (reachOf(mine.id) === 'module') {
64
64
  return run({ name, module: mine.uses ?? '', requires: mine.requires });
65
65
  }
66
66
  const went = spawnSync(mine.uses ?? '', passed, { cwd: tree.at, stdio: 'inherit' });
@@ -124,7 +124,7 @@ export async function run(declared) {
124
124
  for (const segment of method.split('.')) {
125
125
  if (REACHES.has(segment)) {
126
126
  return refuse(`\`${segment}\` is not an operation of \`${declared.module}\` — it is a step every JavaScript value has, and ` +
127
- 'walking to it leaves what was installed. What may run here is what the SDK exports, and a person fixed that ' +
127
+ 'walking to it leaves what was installed. What may run here is what the module exports, and a person fixed that ' +
128
128
  'set when they declared it.');
129
129
  }
130
130
  owner = target;
@@ -0,0 +1,189 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { argv, stderr, stdout } from 'node:process'
3
+
4
+ import type { Env } from '../core/actor.ts'
5
+ import { answer, asking, given, only, wordsIn } from '../core/asking.ts'
6
+ import { exit } from '../core/invoked.ts'
7
+ import { findTree, inTree } from '../core/tree.ts'
8
+
9
+ import { REACH, reachOf, type Target } from './target.ts'
10
+ import { opened } from './vault.ts'
11
+
12
+ const PRODUCTION = '--production'
13
+
14
+ const METHOD = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/
15
+
16
+ function isClass(fn: (...args: readonly unknown[]) => unknown): boolean {
17
+ return /^class[\s{]/.test(Function.prototype.toString.call(fn))
18
+ }
19
+
20
+ const ASKS: readonly { readonly key: string; readonly says: string }[] = [
21
+ {
22
+ key: 'args',
23
+ says: 'a JSON value — an array for positional arguments, anything else for one, blank for none',
24
+ },
25
+ ]
26
+
27
+ const TAKES: readonly string[] = ASKS.map((one) => one.key)
28
+
29
+ const ASKED: readonly string[] = ASKS.map((one) => `\`${one.key}\`: ${one.says}`)
30
+
31
+ export async function ran(name: string): Promise<void> {
32
+ const say = (because: string): never => {
33
+ stderr.write(`${name}: ${because}\n`)
34
+ exit(1)
35
+ }
36
+
37
+ const tree = findTree()
38
+ let targets: readonly Target[] = []
39
+ try {
40
+ const taken = (await import(new URL(`file://${inTree(tree, 'targets.ts')}`).href)) as {
41
+ TARGETS?: readonly Target[]
42
+ }
43
+ targets = taken.TARGETS ?? []
44
+ } catch {
45
+ return say('`_threads/targets.ts` could not be read, so nothing says what this integration is.')
46
+ }
47
+
48
+ const mine = targets.find((one) => one.id === `${REACH.cli}${name}` || one.id === `${REACH.module}${name}`)
49
+ if (mine === undefined) {
50
+ return say(
51
+ `no target called \`${REACH.cli}${name}\` or \`${REACH.module}${name}\` is declared here, so this command should ` +
52
+ 'not exist. Declare it with `thread create target`, or take its row out of `_threads/derivation.ts`.',
53
+ )
54
+ }
55
+
56
+ const does = mine.offers ?? []
57
+ const passed = argv.slice(2)
58
+ const sub = passed[0]
59
+
60
+ if (sub === undefined) {
61
+ stdout.write(
62
+ `${name}: ${mine.describes}\n\n${String(does.length)} command(s), read off \`${mine.uses ?? ''}\` when \`${mine.id}\` was declared:\n` +
63
+ `${does.join(', ') || '(none were readable — name them with `thread create target edit`)'}\n\n` +
64
+ `thread run ${name} <command> [anything else] — arguments and flags reach \`${mine.uses ?? ''}\` untouched.\n` +
65
+ `It takes ${String(ASKED.length)} answer(s): ${ASKED.join('; ')}.\n`,
66
+ )
67
+ exit(0)
68
+ }
69
+
70
+ if (!does.includes(sub)) {
71
+ return say(
72
+ `\`${sub}\` is not one of the ${String(does.length)} command(s) \`${mine.uses ?? ''}\` stated when \`${mine.id}\` ` +
73
+ `was declared. It declared: ${does.join(', ') || '(nothing)'}. If the tool has changed, read it again with ` +
74
+ `\`thread create target edit ${mine.id}\`.`,
75
+ )
76
+ }
77
+
78
+ if (reachOf(mine.id) === 'module') {
79
+ return run({ name, module: mine.uses ?? '', requires: mine.requires })
80
+ }
81
+
82
+ const went = spawnSync(mine.uses ?? '', passed, { cwd: tree.at, stdio: 'inherit' })
83
+ exit(went.status ?? 1)
84
+ }
85
+
86
+ export async function run(declared: {
87
+ readonly name: string
88
+ readonly module: string
89
+ readonly requires: readonly string[]
90
+ }): Promise<void> {
91
+ const refuse = (because: string): never => {
92
+ stderr.write(`${declared.name}: ${because}\n`)
93
+ exit(1)
94
+ }
95
+
96
+ const method = wordsIn([])[0]
97
+ if (method === undefined) {
98
+ return refuse(
99
+ `takes the method to call, dot-separated for a namespaced one — e.g. \`thread run ${declared.name} customers.list\`.`,
100
+ )
101
+ }
102
+ if (!METHOD.test(method)) {
103
+ return refuse(`\`${method}\` is not a method path — one name, or names joined by dots, e.g. \`customers.list\``)
104
+ }
105
+
106
+ const piped = given()
107
+ only(piped, TAKES, `\`thread run ${declared.name} ${method}\``)
108
+ const { ask, shut } = asking(piped)
109
+ const typed = (
110
+ piped.length > 0
111
+ ? (answer(piped, TAKES[0] ?? 'args') ?? '')
112
+ : await ask('args', 'args, as JSON — an array for positional arguments, anything else for one (blank for none): ')
113
+ ).trim()
114
+ shut()
115
+
116
+ let args: unknown[] = []
117
+ if (typed !== '') {
118
+ let parsed: unknown
119
+ try {
120
+ parsed = JSON.parse(typed)
121
+ } catch (thrown) {
122
+ return refuse(`that is not JSON: ${thrown instanceof Error ? thrown.message : String(thrown)}. Nothing was called.`)
123
+ }
124
+ args = Array.isArray(parsed) ? parsed : [parsed]
125
+ }
126
+
127
+ const env: Env = argv.includes(PRODUCTION) ? 'production' : 'sandbox'
128
+
129
+ const opening = opened(findTree().at, declared.requires, env)
130
+ let resolved: Readonly<Record<string, string>>
131
+ try {
132
+ resolved = await opening(declared.requires)
133
+ } catch (thrown) {
134
+ return refuse(thrown instanceof Error ? thrown.message : String(thrown))
135
+ }
136
+ const credentials = declared.requires.map((one) => resolved[one] ?? '')
137
+
138
+ let mod: Record<string, unknown>
139
+ try {
140
+ mod = (await import(declared.module)) as Record<string, unknown>
141
+ } catch (thrown) {
142
+ return refuse(
143
+ `\`${declared.module}\` could not be imported: ${thrown instanceof Error ? thrown.message : String(thrown)}. It was ` +
144
+ `installed when this was declared — if it is gone, \`npm install ${declared.module}\` puts it back.`,
145
+ )
146
+ }
147
+
148
+ const entry: unknown = mod.default ?? mod
149
+
150
+ const asFunction = entry as (...a: readonly unknown[]) => unknown
151
+ const asConstructor = entry as new (...a: readonly unknown[]) => unknown
152
+
153
+ const REACHES = new Set(['constructor', '__proto__', 'prototype'])
154
+
155
+ const client: unknown =
156
+ typeof entry !== 'function' ? entry : isClass(asFunction) ? Reflect.construct(asConstructor, credentials) : asFunction(...credentials)
157
+
158
+ let owner: unknown = client
159
+ let target: unknown = client
160
+ for (const segment of method.split('.')) {
161
+ if (REACHES.has(segment)) {
162
+ return refuse(
163
+ `\`${segment}\` is not an operation of \`${declared.module}\` — it is a step every JavaScript value has, and ` +
164
+ 'walking to it leaves what was installed. What may run here is what the module exports, and a person fixed that ' +
165
+ 'set when they declared it.',
166
+ )
167
+ }
168
+ owner = target
169
+ target = (target as Record<string, unknown> | null | undefined)?.[segment]
170
+ }
171
+ if (typeof target !== 'function') {
172
+ return refuse(`\`${method}\` on \`${declared.module}\` is ${target === undefined ? 'nothing' : `a ${typeof target}`}, not a method that can be called`)
173
+ }
174
+
175
+ let result: unknown
176
+ try {
177
+ result = await (target as (...a: unknown[]) => unknown).apply(owner, args)
178
+ } catch (thrown) {
179
+ return refuse(`\`${method}\` on \`${declared.module}\` threw: ${thrown instanceof Error ? thrown.message : String(thrown)}`)
180
+ }
181
+
182
+ stdout.write(`${declared.name}: ${method} (${env})\n`)
183
+ try {
184
+ stdout.write(`${JSON.stringify(result, null, 2)}\n`)
185
+ } catch {
186
+ stdout.write(`${String(result)}\n`)
187
+ }
188
+ exit(0)
189
+ }
@@ -0,0 +1,50 @@
1
+ import type { Member } from '../core/thread.ts'
2
+
3
+ export const SECRECY = {
4
+ server: 'sec_',
5
+ public: 'pub_',
6
+ } as const satisfies Readonly<Record<string, string>>
7
+
8
+ export const SAID: Readonly<Record<keyof typeof SECRECY, string>> = {
9
+ server:
10
+ 'A value only a server may ever hold — an API key, a connection string, a signing secret. Everything a browser bundle reaches is in the bundle and belongs to whoever opens the page, so a target requiring one of these is refused a browser entry when it is built rather than when somebody reads it.',
11
+ public:
12
+ 'A value that may reach a browser: a base url, a publishable key, a project id. It is still a secret and still lives in the sealed store, because it still differs between a sandbox and production and nothing else here keeps that difference. What it is not is a thing that has to be withheld.',
13
+ }
14
+
15
+ export interface Secret extends Member {
16
+
17
+ readonly sends?: { readonly header: string; readonly prefix?: string }
18
+ }
19
+
20
+ export function secrecyOf(id: string): keyof typeof SECRECY | undefined {
21
+ return (Object.keys(SECRECY) as (keyof typeof SECRECY)[]).find((one) => id.startsWith(SECRECY[one]))
22
+ }
23
+
24
+ export const isServerOnly = (id: string): boolean => secrecyOf(id) === 'server'
25
+
26
+ export const unclassified = (secrets: readonly Secret[]): readonly string[] =>
27
+ secrets
28
+ .filter((one) => secrecyOf(one.id) === undefined)
29
+ .map(
30
+ (one) =>
31
+ `\`${one.id}\` wears neither \`${SECRECY.server}\` nor \`${SECRECY.public}\`, so nothing can tell whether it may ` +
32
+ 'reach a browser — and that prefix is the only thing standing between a server credential and a bundle',
33
+ )
34
+
35
+ const VALUED = ['value', 'secret', 'key', 'token', 'password', 'env']
36
+
37
+ export const valued = (secrets: readonly Secret[]): readonly string[] => {
38
+ const found: string[] = []
39
+ for (const one of secrets) {
40
+ for (const named of VALUED) {
41
+ if (!(named in one)) continue
42
+ found.push(
43
+ `\`${one.id}\` carries a \`${named}\`. A secret row holds a name and a sentence and never a value — this row is ` +
44
+ 'read by the descent, by every generator and by anything that prints the tree, while a credential is read by ' +
45
+ 'one thing at one moment. Put the value in the sealed store with `thread create secret`',
46
+ )
47
+ }
48
+ }
49
+ return found
50
+ }
@@ -5,7 +5,7 @@ export declare const REACH: {
5
5
  readonly pglite: "pl_";
6
6
  readonly browser: "zu_";
7
7
  readonly https: "ht_";
8
- readonly sdk: "sdk_";
8
+ readonly module: "mod_";
9
9
  readonly cli: "cli_";
10
10
  };
11
11
  export type Reach = keyof typeof REACH;
@@ -17,6 +17,7 @@ export interface Holds {
17
17
  readonly migrations: boolean;
18
18
  readonly references: boolean;
19
19
  readonly sql: boolean;
20
+ readonly driver: string | null;
20
21
  }
21
22
  export declare const HOLDS: Readonly<Record<Reach, Holds>>;
22
23
  export declare const SAID: Readonly<Record<Reach, string>>;
@@ -35,6 +36,7 @@ export declare const CALL_PREFIX = "cl_";
35
36
  export declare const fills: (one: Call) => readonly string[];
36
37
  export declare function reachOf(id: string): Reach | undefined;
37
38
  export declare function holdsOf(id: string): Holds | undefined;
39
+ export declare function driverOf(id: string): string | null;
38
40
  export declare function addressOf(one: Target, secrets: readonly Secret[]): Secret | undefined;
39
41
  export declare function credentialsOf(one: Target, secrets: readonly Secret[]): readonly Secret[];
40
42
  export declare function servesBrowser(one: Target, secrets: readonly Secret[]): true | string;