@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.
- package/dist/core/actor.ts +20 -0
- package/dist/core/asking.d.ts +0 -1
- package/dist/core/asking.js +0 -1
- package/dist/core/asking.ts +1342 -0
- package/dist/core/attach.js +2 -2
- package/dist/core/attach.ts +585 -0
- package/dist/core/chain.js +8 -1
- package/dist/core/chain.ts +148 -0
- package/dist/core/cli.js +97 -18
- package/dist/core/cli.ts +2004 -0
- package/dist/core/condition.ts +227 -0
- package/dist/core/descend.d.ts +1 -0
- package/dist/core/descend.js +36 -0
- package/dist/core/descend.ts +1001 -0
- package/dist/core/finding.ts +38 -0
- package/dist/core/generators/meta.js +6 -0
- package/dist/core/generators/meta.ts +1006 -0
- package/dist/core/init.ts +170 -0
- package/dist/core/invoked.ts +47 -0
- package/dist/core/round.ts +57 -0
- package/dist/core/rows.ts +369 -0
- package/dist/core/scope.ts +18 -0
- package/dist/core/thread.ts +29 -0
- package/dist/core/tree.ts +67 -0
- package/dist/core/types.ts +350 -0
- package/dist/derivation/attach.js +1 -1
- package/dist/derivation/attach.ts +68 -0
- package/dist/derivation/generators/build.ts +232 -0
- package/dist/derivation/generators/check.ts +118 -0
- package/dist/derivation/generators/derivation.ts +347 -0
- package/dist/derivation/generators/determinism.ts +145 -0
- package/dist/derivation/generators/release.js +19 -9
- package/dist/derivation/generators/release.ts +213 -0
- package/dist/derivation/makers/release.ts +135 -0
- package/dist/derivation/release.ts +23 -0
- package/dist/integrations/attach.js +2 -2
- package/dist/integrations/attach.ts +207 -0
- package/dist/integrations/generators/read.ts +127 -0
- package/dist/integrations/generators/secret.ts +289 -0
- package/dist/integrations/generators/target.js +16 -6
- package/dist/integrations/generators/target.ts +944 -0
- package/dist/integrations/generators/vault.js +46 -14
- package/dist/integrations/generators/vault.ts +212 -0
- package/dist/integrations/{sdk.js → module.js} +4 -4
- package/dist/integrations/module.ts +189 -0
- package/dist/integrations/secret.ts +50 -0
- package/dist/integrations/target.d.ts +3 -1
- package/dist/integrations/target.js +14 -11
- package/dist/integrations/target.ts +340 -0
- package/dist/integrations/vault.d.ts +2 -0
- package/dist/integrations/vault.js +4 -2
- package/dist/integrations/vault.ts +136 -0
- package/dist/integrity/attach.ts +234 -0
- package/dist/integrity/check-threads.js +2 -26
- package/dist/integrity/check-threads.ts +166 -0
- package/dist/integrity/constraint.ts +1202 -0
- package/dist/integrity/generators/compiler.ts +161 -0
- package/dist/integrity/generators/gate.ts +201 -0
- package/dist/integrity/generators/issues.ts +160 -0
- package/dist/integrity/generators/publish.js +14 -5
- package/dist/integrity/generators/publish.ts +247 -0
- package/dist/integrity/generators/rule.ts +271 -0
- package/dist/integrity/generators/seal.ts +117 -0
- package/dist/integrity/generators/trial.ts +320 -0
- package/dist/integrity/lint.ts +227 -0
- package/dist/integrity/lsp/bin.ts +42 -0
- package/dist/integrity/lsp/diagnostics.ts +92 -0
- package/dist/integrity/lsp/findings.ts +63 -0
- package/dist/integrity/lsp/lint.ts +140 -0
- package/dist/integrity/lsp/protocol.ts +78 -0
- package/dist/integrity/lsp/server.ts +224 -0
- package/dist/integrity/sealed.js +2 -2
- package/dist/integrity/sealed.ts +56 -0
- package/dist/integrity/slopnator.ts +350 -0
- package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
- package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
- package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
- package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
- package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
- package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
- package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
- package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
- package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
- package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
- package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
- package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
- package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
- package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
- package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
- package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
- package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
- package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
- package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
- package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
- package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
- package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
- package/dist/integrity/sweep.ts +307 -0
- package/dist/knowledge/attach.ts +350 -0
- package/dist/knowledge/generators/docsgen.ts +174 -0
- package/dist/knowledge/generators/glossary.ts +73 -0
- package/dist/knowledge/generators/skills.ts +263 -0
- package/dist/knowledge/glossary.ts +6 -0
- package/dist/operations/access.ts +88 -0
- package/dist/operations/attach.ts +475 -0
- package/dist/operations/capabilities.ts +65 -0
- package/dist/operations/cost.ts +58 -0
- package/dist/operations/dispatch.ts +348 -0
- package/dist/operations/error-results.ts +31 -0
- package/dist/operations/errors/catalog.ts +168 -0
- package/dist/operations/errors/classes.ts +51 -0
- package/dist/operations/errors/errors.ts +314 -0
- package/dist/operations/generators/capability.js +29 -19
- package/dist/operations/generators/capability.ts +704 -0
- package/dist/operations/generators/compile.js +20 -1
- package/dist/operations/generators/compile.ts +643 -0
- package/dist/operations/generators/grant.ts +336 -0
- package/dist/operations/generators/operation.ts +972 -0
- package/dist/operations/generators/permits.ts +223 -0
- package/dist/operations/generators/seed.ts +347 -0
- package/dist/operations/generators/trial.js +4 -1
- package/dist/operations/generators/trial.ts +529 -0
- package/dist/operations/http.ts +164 -0
- package/dist/operations/makers/capability.ts +166 -0
- package/dist/operations/makers/trial.ts +520 -0
- package/dist/operations/memory.ts +470 -0
- package/dist/operations/metering.ts +328 -0
- package/dist/operations/nodes.ts +54 -0
- package/dist/operations/operations.ts +32 -0
- package/dist/operations/queue-policy.ts +10 -0
- package/dist/operations/result.ts +28 -0
- package/dist/operations/seams.d.ts +4 -4
- package/dist/operations/seams.ts +364 -0
- package/dist/operations/store.ts +29 -0
- package/package.json +8 -1
- package/dist/integrations/generators/sdk.d.ts +0 -1
- package/dist/integrations/generators/sdk.js +0 -148
- /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { ANONYMOUS, ENVIRONMENT } from '../core/actor.ts'
|
|
2
|
+
import { errorDefForResult } from './error-results.ts'
|
|
3
|
+
import { DbConflict } from './errors/errors.ts'
|
|
4
|
+
import { quoted, runtime } from './generators/seed.ts'
|
|
5
|
+
import type { ResultCode } from './result.ts'
|
|
6
|
+
import { SUPPLY, type AuditOutcome } from './seams.ts'
|
|
7
|
+
|
|
8
|
+
export const DISPATCH_AT = 'operations/_dispatch.ts'
|
|
9
|
+
|
|
10
|
+
const identity = (code: ResultCode): { readonly code: string; readonly class: string } => {
|
|
11
|
+
const def = errorDefForResult(code)
|
|
12
|
+
return { code: def.code, class: def.class }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CODES: readonly ResultCode[] = ['denied', 'invalid', 'rate_limited', 'feature_denied', 'quota_exceeded', 'error']
|
|
16
|
+
|
|
17
|
+
const REFUSAL: Readonly<Record<ResultCode, { readonly said: string; readonly outcome: AuditOutcome }>> = {
|
|
18
|
+
denied: { said: 'not allowed', outcome: 'denied' },
|
|
19
|
+
invalid: { said: 'invalid input', outcome: 'failed' },
|
|
20
|
+
rate_limited: { said: 'rate limited', outcome: 'rate_limited' },
|
|
21
|
+
feature_denied: { said: 'not in plan', outcome: 'feature_denied' },
|
|
22
|
+
quota_exceeded: { said: 'quota exceeded', outcome: 'quota_exceeded' },
|
|
23
|
+
error: { said: 'failed', outcome: 'failed' },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const printed = (value: unknown): string => JSON.stringify(value, null, 2)
|
|
27
|
+
|
|
28
|
+
const DEFS = Object.fromEntries(CODES.map((code) => [code, identity(code)]))
|
|
29
|
+
|
|
30
|
+
export const dispatched = (
|
|
31
|
+
reached: Readonly<
|
|
32
|
+
Record<
|
|
33
|
+
string,
|
|
34
|
+
{
|
|
35
|
+
readonly reach: string
|
|
36
|
+
readonly address: string | null
|
|
37
|
+
readonly sends: readonly { readonly secret: string; readonly header: string; readonly prefix: string }[]
|
|
38
|
+
readonly calls: Readonly<Record<string, { readonly method: string; readonly path: string }>>
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
>,
|
|
42
|
+
): string => [
|
|
43
|
+
`import type { Actor } from ${quoted(runtime(DISPATCH_AT, '../../core/actor.ts'))}`,
|
|
44
|
+
`import type { Operation } from ${quoted(runtime(DISPATCH_AT, '../operations.ts'))}`,
|
|
45
|
+
`import type { FailureIdentity, Result, ResultCode } from ${quoted(runtime(DISPATCH_AT, '../result.ts'))}`,
|
|
46
|
+
`import type { AuditOutcome, Handed, LogLevel, QuotaHold, Seams } from ${quoted(runtime(DISPATCH_AT, '../seams.ts'))}`,
|
|
47
|
+
'',
|
|
48
|
+
`const ANONYMOUS: Actor = ${printed(ANONYMOUS)}`,
|
|
49
|
+
'',
|
|
50
|
+
`const ENVIRONMENT: readonly string[] = ${printed(ENVIRONMENT.map(([fact]) => fact))}`,
|
|
51
|
+
'',
|
|
52
|
+
`const REFUSAL: Readonly<Record<ResultCode, { readonly said: string; readonly outcome: AuditOutcome }>> = ${printed(REFUSAL)}`,
|
|
53
|
+
'',
|
|
54
|
+
`const DEFS = ${printed(DEFS)} as Readonly<Record<ResultCode, FailureIdentity>>`,
|
|
55
|
+
'',
|
|
56
|
+
`const CONFLICT = ${printed({ code: DbConflict.def.code, class: DbConflict.def.class })} as FailureIdentity`,
|
|
57
|
+
'',
|
|
58
|
+
`const SUPPLY: Readonly<Record<string, string>> = ${printed(SUPPLY)}`,
|
|
59
|
+
'',
|
|
60
|
+
'type Reached = Readonly<Record<string, {',
|
|
61
|
+
' readonly reach: string',
|
|
62
|
+
' readonly address: string | null',
|
|
63
|
+
' readonly sends: readonly { readonly secret: string; readonly header: string; readonly prefix: string }[]',
|
|
64
|
+
' readonly calls: Readonly<Record<string, { readonly method: string; readonly path: string }>>',
|
|
65
|
+
'}>>',
|
|
66
|
+
'',
|
|
67
|
+
`const REACHED: Reached = ${printed(reached)}`,
|
|
68
|
+
'',
|
|
69
|
+
"const STOPPED = Symbol.for('thread.operation.stopped')",
|
|
70
|
+
'',
|
|
71
|
+
'const ok = <O>(value: O): Result<O> => ({ ok: true, value })',
|
|
72
|
+
'',
|
|
73
|
+
'const fail = (code: ResultCode, error: string, because?: string, identity?: FailureIdentity): Result<never> => ({',
|
|
74
|
+
' ok: false,',
|
|
75
|
+
' code,',
|
|
76
|
+
' error,',
|
|
77
|
+
' because,',
|
|
78
|
+
' ...(identity === undefined ? {} : { errorCode: identity.code, errorClass: identity.class }),',
|
|
79
|
+
'}) as Result<never>',
|
|
80
|
+
'',
|
|
81
|
+
"const strings = (value: unknown): value is string[] => Array.isArray(value) && value.every((one) => typeof one === 'string')",
|
|
82
|
+
'',
|
|
83
|
+
'const person = (value: unknown): Actor | undefined => {',
|
|
84
|
+
" if (typeof value !== 'object' || value === null) return undefined",
|
|
85
|
+
' const one = value as Record<string, unknown>',
|
|
86
|
+
" const id = one['id']",
|
|
87
|
+
" if (!(id === null || typeof id === 'string')) return undefined",
|
|
88
|
+
" const roles = one['roles']",
|
|
89
|
+
" const entitlements = one['entitlements']",
|
|
90
|
+
' if (!strings(roles) || !strings(entitlements)) return undefined',
|
|
91
|
+
' const carried: Record<string, string> = {}',
|
|
92
|
+
' for (const fact of ENVIRONMENT) {',
|
|
93
|
+
' const held = one[fact]',
|
|
94
|
+
' if (held === undefined) continue',
|
|
95
|
+
" if (typeof held !== 'string') return undefined",
|
|
96
|
+
' carried[fact] = held',
|
|
97
|
+
' }',
|
|
98
|
+
' return { ...carried, id, roles, entitlements }',
|
|
99
|
+
'}',
|
|
100
|
+
'',
|
|
101
|
+
'const named = (thrown: unknown, what: string): boolean => {',
|
|
102
|
+
" if (typeof thrown !== 'object' || thrown === null) return false",
|
|
103
|
+
' const one = thrown as { readonly name?: unknown; readonly constructor?: { readonly name?: unknown } }',
|
|
104
|
+
' return one.name === what || one.constructor?.name === what',
|
|
105
|
+
'}',
|
|
106
|
+
'',
|
|
107
|
+
'const stopped = (thrown: unknown): { readonly value: unknown } | undefined => {',
|
|
108
|
+
" if (typeof thrown !== 'object' || thrown === null) return undefined",
|
|
109
|
+
' const one = thrown as Record<PropertyKey, unknown>',
|
|
110
|
+
" return one[STOPPED] === true ? { value: one['value'] } : undefined",
|
|
111
|
+
'}',
|
|
112
|
+
'',
|
|
113
|
+
'const marked = (thrown: unknown): string | undefined => {',
|
|
114
|
+
" if (typeof thrown !== 'object' || thrown === null) return undefined",
|
|
115
|
+
' const said = (thrown as { readonly node?: unknown }).node',
|
|
116
|
+
" return typeof said === 'string' ? said : undefined",
|
|
117
|
+
'}',
|
|
118
|
+
'',
|
|
119
|
+
'const platform = (thrown: unknown): string | undefined => {',
|
|
120
|
+
" if (typeof thrown !== 'object' || thrown === null) return undefined",
|
|
121
|
+
' const said = (thrown as { readonly because?: unknown }).because',
|
|
122
|
+
" return typeof said === 'string' ? said : undefined",
|
|
123
|
+
'}',
|
|
124
|
+
'',
|
|
125
|
+
'const filled = (path: string, fills: Readonly<Record<string, string | number>> | undefined): string => {',
|
|
126
|
+
' let held = path',
|
|
127
|
+
' for (const [name, value] of Object.entries(fills ?? {})) held = held.split(`{${name}}`).join(encodeURIComponent(String(value)))',
|
|
128
|
+
' const left = /\\{([a-z][a-z0-9_]*)\\}/.exec(held)',
|
|
129
|
+
' if (left !== null) throw new Error(`the path ${path} leaves ${left[0]} to be filled and nothing filled it`)',
|
|
130
|
+
' return held',
|
|
131
|
+
'}',
|
|
132
|
+
'',
|
|
133
|
+
'const bound = (word: string, wired: Reached[string], seams: Seams): Record<string, unknown> | string => {',
|
|
134
|
+
' const names = Object.keys(wired.calls)',
|
|
135
|
+
' if (names.length === 0) {',
|
|
136
|
+
' return `\\`${word}\\` is required and takes no call, so there is nothing to hand a node. Declare one with \\`thread create target call\\``',
|
|
137
|
+
' }',
|
|
138
|
+
' if (seams.http === undefined) {',
|
|
139
|
+
' return `\\`${word}\\` is reached over ${wired.reach} and the seams this dispatch was handed hold no \\`http\\` — wire one, or take \\`${word}\\` off that operation\'s row`',
|
|
140
|
+
' }',
|
|
141
|
+
' const wanted = [...(wired.address === null ? [] : [wired.address]), ...wired.sends.map((one) => one.secret)]',
|
|
142
|
+
' if (wanted.length > 0 && seams.vault === undefined) {',
|
|
143
|
+
' return `\\`${word}\\` needs ${wanted.join(\', \')} and the seams this dispatch was handed hold no \\`vault\\` to open them. A target is reached with what opens it or not at all`',
|
|
144
|
+
' }',
|
|
145
|
+
' const out: Record<string, unknown> = {}',
|
|
146
|
+
' for (const name of names) {',
|
|
147
|
+
' const call = wired.calls[name]',
|
|
148
|
+
' if (call === undefined) continue',
|
|
149
|
+
' out[name] = async (given?: { fills?: Readonly<Record<string, string | number>>; json?: unknown; body?: Uint8Array; contentType?: string }) => {',
|
|
150
|
+
' const values = wanted.length === 0 ? {} : await seams.vault!.resolve(wanted)',
|
|
151
|
+
' const headers: Record<string, string> = {}',
|
|
152
|
+
' for (const one of wired.sends) headers[one.header] = `${one.prefix}${values[one.secret] ?? \'\'}`',
|
|
153
|
+
' const body = given?.json === undefined ? given?.body : new TextEncoder().encode(JSON.stringify(given.json))',
|
|
154
|
+
' return await seams.http!.call({',
|
|
155
|
+
' target: word,',
|
|
156
|
+
' address: wired.address === null ? \'\' : (values[wired.address] ?? \'\'),',
|
|
157
|
+
" method: call.method as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',",
|
|
158
|
+
' path: filled(call.path, given?.fills),',
|
|
159
|
+
' headers,',
|
|
160
|
+
' ...(body === undefined ? {} : { body, contentType: given?.contentType ?? \'application/json\' }),',
|
|
161
|
+
' })',
|
|
162
|
+
' }',
|
|
163
|
+
' }',
|
|
164
|
+
' return out',
|
|
165
|
+
'}',
|
|
166
|
+
'',
|
|
167
|
+
|
|
168
|
+
'const given = (operation: Operation<unknown, string>, actor: Actor, seams: Seams): Record<string, unknown> | string => {',
|
|
169
|
+
' const held: Record<string, unknown> = { actor }',
|
|
170
|
+
' for (const word of operation.requires) {',
|
|
171
|
+
' const key = SUPPLY[word]',
|
|
172
|
+
' if (key === undefined) {',
|
|
173
|
+
' const wired = REACHED[word]',
|
|
174
|
+
' if (wired === undefined) {',
|
|
175
|
+
' return `\\`${operation.id}\\` requires \\`${word}\\`, and nothing here supplies it — what may be required is ${[...Object.keys(SUPPLY), ...Object.keys(REACHED)].join(\', \')}`',
|
|
176
|
+
' }',
|
|
177
|
+
' const reached = bound(word, wired, seams)',
|
|
178
|
+
" if (typeof reached === 'string') return reached",
|
|
179
|
+
' held[word] = reached',
|
|
180
|
+
' continue',
|
|
181
|
+
' }',
|
|
182
|
+
' const seam = (seams as unknown as Record<string, unknown>)[key]',
|
|
183
|
+
' if (seam === undefined) {',
|
|
184
|
+
' return `\\`${operation.id}\\` requires \\`${word}\\`, and the seams this dispatch was handed hold no \\`${key}\\` — wire one, or take \\`${word}\\` off that operation\'s row. An operation does not run short of what it declared`',
|
|
185
|
+
' }',
|
|
186
|
+
' held[key] = seam',
|
|
187
|
+
' }',
|
|
188
|
+
' return held',
|
|
189
|
+
'}',
|
|
190
|
+
'',
|
|
191
|
+
'export async function dispatch<I, R extends string = string>(',
|
|
192
|
+
' operation: Operation<I, R>,',
|
|
193
|
+
' rawInput: unknown,',
|
|
194
|
+
' asking: Actor,',
|
|
195
|
+
' handed: Seams,',
|
|
196
|
+
'): Promise<Result<unknown>> {',
|
|
197
|
+
' const seams = running(handed)',
|
|
198
|
+
' const id = operation.id',
|
|
199
|
+
' const who = person(asking)',
|
|
200
|
+
' const run = who?.run ?? seams.random.uuid()',
|
|
201
|
+
' const actor: Actor = { ...(who ?? ANONYMOUS), run }',
|
|
202
|
+
' const span = seams.telemetry?.start(id, run, { ...actor })',
|
|
203
|
+
'',
|
|
204
|
+
' const gates = { rate: seams.rateLimiter !== undefined, replay: seams.idempotency !== undefined, quota: seams.quotas !== undefined, telemetry: seams.telemetry !== undefined }',
|
|
205
|
+
'',
|
|
206
|
+
' let hold: QuotaHold | null = null',
|
|
207
|
+
' let granted: string | undefined',
|
|
208
|
+
'',
|
|
209
|
+
' const sank = (what: string) => (thrown: unknown): void => {',
|
|
210
|
+
" seams.log?.write('error', id, actor, {",
|
|
211
|
+
' [`${what}_failed`]: thrown instanceof Error ? thrown.message : String(thrown),',
|
|
212
|
+
' })',
|
|
213
|
+
' }',
|
|
214
|
+
'',
|
|
215
|
+
' const settled = async (held: Result<unknown>, because?: string): Promise<Result<unknown>> => {',
|
|
216
|
+
" const outcome: AuditOutcome = held.ok ? 'ok' : REFUSAL[held.code].outcome",
|
|
217
|
+
' const detail = because === undefined ? {} : { because }',
|
|
218
|
+
'',
|
|
219
|
+
' const reserved = hold === null ? null : operation.estimate',
|
|
220
|
+
' let consumed: number | null = null',
|
|
221
|
+
'',
|
|
222
|
+
' if (hold !== null) {',
|
|
223
|
+
' const settling = held.ok ? hold.commit(operation.estimate) : hold.release()',
|
|
224
|
+
' consumed = held.ok ? operation.estimate : 0',
|
|
225
|
+
' hold = null',
|
|
226
|
+
" await settling.catch(sank('quota'))",
|
|
227
|
+
' }',
|
|
228
|
+
'',
|
|
229
|
+
" seams.log.write(held.ok ? 'info' : 'warn', id, actor, {",
|
|
230
|
+
' outcome,',
|
|
231
|
+
' ...detail,',
|
|
232
|
+
" access: { roles: actor.roles, entitlements: actor.entitlements, granted: granted ?? 'not reached' },",
|
|
233
|
+
' quota: { meter: id, reserved, consumed },',
|
|
234
|
+
' gates,',
|
|
235
|
+
' })',
|
|
236
|
+
' if (held.ok) span?.ok()',
|
|
237
|
+
' else span?.error(outcome)',
|
|
238
|
+
'',
|
|
239
|
+
' await seams.audit',
|
|
240
|
+
" .append(actor, { action: id, outcome, subject: { type: 'operation', id }, ...detail })",
|
|
241
|
+
" .catch(sank('audit'))",
|
|
242
|
+
'',
|
|
243
|
+
' if (!held.ok) {',
|
|
244
|
+
' const identity: FailureIdentity =',
|
|
245
|
+
' held.errorCode === undefined || held.errorClass === undefined',
|
|
246
|
+
' ? DEFS[held.code]',
|
|
247
|
+
' : { code: held.errorCode, class: held.errorClass }',
|
|
248
|
+
'',
|
|
249
|
+
' await seams.errors',
|
|
250
|
+
' .record(actor, { code: identity.code, class: identity.class, action: id, ...detail })',
|
|
251
|
+
" .catch(sank('errors'))",
|
|
252
|
+
' }',
|
|
253
|
+
'',
|
|
254
|
+
' return held',
|
|
255
|
+
' }',
|
|
256
|
+
'',
|
|
257
|
+
' const refused = (code: ResultCode, because?: string): Promise<Result<unknown>> =>',
|
|
258
|
+
' settled(fail(code, REFUSAL[code].said, because), because)',
|
|
259
|
+
'',
|
|
260
|
+
' const recorded = (began: number, ok: boolean, at: string | undefined, because?: string): void => {',
|
|
261
|
+
' const took = seams.clock.now() - began',
|
|
262
|
+
' const sections = {',
|
|
263
|
+
' node: { at: at ?? null },',
|
|
264
|
+
' audit: { actor, when: began },',
|
|
265
|
+
' observability: { ms: took, ok, ...(because === undefined ? {} : { because }) },',
|
|
266
|
+
' }',
|
|
267
|
+
'',
|
|
268
|
+
" seams.log.write(ok ? 'info' : 'warn', id, actor, sections)",
|
|
269
|
+
' seams.telemetry?.record?.({',
|
|
270
|
+
' key: id,',
|
|
271
|
+
' traceId: run,',
|
|
272
|
+
' spanId: run,',
|
|
273
|
+
' durationMs: took,',
|
|
274
|
+
' ok,',
|
|
275
|
+
' attrs: sections,',
|
|
276
|
+
' actor,',
|
|
277
|
+
' ...(at === undefined ? {} : { nodeId: at }),',
|
|
278
|
+
' workflowId: id,',
|
|
279
|
+
' ...(because === undefined ? {} : { error: because }),',
|
|
280
|
+
' })',
|
|
281
|
+
' }',
|
|
282
|
+
'',
|
|
283
|
+
' try {',
|
|
284
|
+
" if (who === undefined) return await refused('denied', 'identity_invalid')",
|
|
285
|
+
'',
|
|
286
|
+
" if (seams.rateLimiter !== undefined && !(await seams.rateLimiter.check(actor, id))) return await refused('rate_limited')",
|
|
287
|
+
'',
|
|
288
|
+
' const replayed = seams.idempotency === undefined ? null : await seams.idempotency.check(actor.replay, id)',
|
|
289
|
+
" if (replayed !== null && replayed !== undefined) return await settled(ok(replayed), 'replayed')",
|
|
290
|
+
'',
|
|
291
|
+
' const parsed = operation.input.safeParse(rawInput)',
|
|
292
|
+
' if (!parsed.success) {',
|
|
293
|
+
" return await refused('invalid', parsed.error.issues.map((one) => one.message).join('; '))",
|
|
294
|
+
' }',
|
|
295
|
+
'',
|
|
296
|
+
' if (seams.quotas) {',
|
|
297
|
+
' const reserved = await seams.quotas.reserve(actor, id, operation.estimate)',
|
|
298
|
+
" if (!reserved.ok) return await refused('quota_exceeded', reserved.behavior ?? 'block')",
|
|
299
|
+
' hold = reserved',
|
|
300
|
+
' }',
|
|
301
|
+
'',
|
|
302
|
+
' if (seams.permits === undefined) {',
|
|
303
|
+
" return await refused('denied', 'no permits check is wired, so nothing can permit this operation')",
|
|
304
|
+
' }',
|
|
305
|
+
'',
|
|
306
|
+
' const ruled = await seams.permits.check(actor, id, operation.resource(parsed.data))',
|
|
307
|
+
" if (!ruled.allow) return await refused('denied', ruled.because)",
|
|
308
|
+
' granted = ruled.because',
|
|
309
|
+
'',
|
|
310
|
+
' const hands = given(operation as Operation<unknown, string>, actor, seams)',
|
|
311
|
+
" if (typeof hands === 'string') return await refused('error', hands)",
|
|
312
|
+
'',
|
|
313
|
+
' const held: { readonly input: I } & Record<string, unknown> = { input: parsed.data }',
|
|
314
|
+
' const began = seams.clock.now()',
|
|
315
|
+
' let value: unknown',
|
|
316
|
+
' try {',
|
|
317
|
+
' value = await operation.run(hands as Handed<R>, held)',
|
|
318
|
+
' } catch (thrown) {',
|
|
319
|
+
' recorded(began, stopped(thrown) !== undefined, marked(thrown), thrown instanceof Error ? thrown.message : String(thrown))',
|
|
320
|
+
' throw thrown',
|
|
321
|
+
' }',
|
|
322
|
+
' recorded(began, true, undefined)',
|
|
323
|
+
'',
|
|
324
|
+
" if (seams.idempotency !== undefined) await seams.idempotency.store(actor.replay, id, value).catch(sank('idempotency'))",
|
|
325
|
+
'',
|
|
326
|
+
' return await settled(ok(value))',
|
|
327
|
+
' } catch (thrown) {',
|
|
328
|
+
' const early = stopped(thrown)',
|
|
329
|
+
' if (early !== undefined) {',
|
|
330
|
+
" if (seams.idempotency !== undefined) await seams.idempotency.store(actor.replay, id, early.value).catch(sank('idempotency'))",
|
|
331
|
+
" return await settled(ok(early.value), 'stopped')",
|
|
332
|
+
' }',
|
|
333
|
+
'',
|
|
334
|
+
' const message = thrown instanceof Error ? thrown.message : String(thrown)',
|
|
335
|
+
'',
|
|
336
|
+
" if (named(thrown, 'VersionConflictError')) {",
|
|
337
|
+
" return await settled(fail('invalid', message, 'version_conflict', CONFLICT), 'version_conflict')",
|
|
338
|
+
' }',
|
|
339
|
+
'',
|
|
340
|
+
" const code: ResultCode = named(thrown, 'ValidationError') ? 'invalid' : 'error'",
|
|
341
|
+
' const because = platform(thrown) ?? message',
|
|
342
|
+
' return await settled(fail(code, message, because), because)',
|
|
343
|
+
' } finally {',
|
|
344
|
+
' span?.end()',
|
|
345
|
+
' }',
|
|
346
|
+
'}',
|
|
347
|
+
'',
|
|
348
|
+
].join('\n')
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PlatformError, catalog, type ErrorDef } from './errors/catalog.ts'
|
|
2
|
+
import { AuthDenied, FeatureNotInPlan, InternalInvalidArgument, InternalUnexpected, QuotaExceeded, RateLimitExceeded } from './errors/errors.ts'
|
|
3
|
+
|
|
4
|
+
import type { Result, ResultCode } from './result.ts'
|
|
5
|
+
|
|
6
|
+
const RESULT_ERROR_DEFS: Readonly<Record<ResultCode, ErrorDef>> = {
|
|
7
|
+
invalid: InternalInvalidArgument.def,
|
|
8
|
+
denied: AuthDenied.def,
|
|
9
|
+
rate_limited: RateLimitExceeded.def,
|
|
10
|
+
quota_exceeded: QuotaExceeded.def,
|
|
11
|
+
feature_denied: FeatureNotInPlan.def,
|
|
12
|
+
error: InternalUnexpected.def,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function errorDefForResult(code: ResultCode): ErrorDef {
|
|
16
|
+
return RESULT_ERROR_DEFS[code]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function platformErrorForResult(result: Result<unknown>, run: string): PlatformError {
|
|
20
|
+
if (result.ok) throw new TypeError('cannot render a successful Result as a problem')
|
|
21
|
+
|
|
22
|
+
const def = result.errorCode === undefined ? errorDefForResult(result.code) : catalog.get(result.errorCode)
|
|
23
|
+
const resolved = def ?? InternalUnexpected.def
|
|
24
|
+
const detail = result.errorCode === undefined && resolved.code === InternalUnexpected.def.code ? undefined : result.error
|
|
25
|
+
|
|
26
|
+
return new PlatformError(resolved, {
|
|
27
|
+
...(detail === undefined ? {} : { detail }),
|
|
28
|
+
...(result.because === undefined ? {} : { because: result.because }),
|
|
29
|
+
traceId: run,
|
|
30
|
+
})
|
|
31
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { classDef, type ErrorClass } from './classes.ts'
|
|
4
|
+
|
|
5
|
+
export const NAMESPACES = [
|
|
6
|
+
'auth',
|
|
7
|
+
'quota',
|
|
8
|
+
'rate_limit',
|
|
9
|
+
'feature',
|
|
10
|
+
'db',
|
|
11
|
+
'workflow',
|
|
12
|
+
'queue',
|
|
13
|
+
'ai',
|
|
14
|
+
'search',
|
|
15
|
+
'comms',
|
|
16
|
+
'config',
|
|
17
|
+
'net',
|
|
18
|
+
'internal',
|
|
19
|
+
] as const
|
|
20
|
+
|
|
21
|
+
export type Namespace = (typeof NAMESPACES)[number]
|
|
22
|
+
|
|
23
|
+
export type ErrorCode = `${Namespace}.${string}`
|
|
24
|
+
|
|
25
|
+
export interface ErrorDef<C extends ErrorCode = ErrorCode> {
|
|
26
|
+
readonly code: C
|
|
27
|
+
readonly class: ErrorClass
|
|
28
|
+
readonly status: number
|
|
29
|
+
|
|
30
|
+
readonly titleKey: string
|
|
31
|
+
readonly retryable: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DefineErrorInput<C extends ErrorCode = ErrorCode> {
|
|
35
|
+
readonly code: C
|
|
36
|
+
readonly class: ErrorClass
|
|
37
|
+
|
|
38
|
+
readonly status?: number
|
|
39
|
+
|
|
40
|
+
readonly retryable?: boolean
|
|
41
|
+
|
|
42
|
+
readonly titleKey?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PlatformErrorInit {
|
|
46
|
+
|
|
47
|
+
readonly detail?: string
|
|
48
|
+
|
|
49
|
+
readonly instance?: string
|
|
50
|
+
|
|
51
|
+
readonly because?: string
|
|
52
|
+
readonly traceId?: string
|
|
53
|
+
|
|
54
|
+
readonly retryAfter?: number
|
|
55
|
+
readonly cause?: unknown
|
|
56
|
+
|
|
57
|
+
readonly extensions?: Readonly<Record<string, unknown>>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class PlatformError<C extends ErrorCode = ErrorCode> extends Error {
|
|
61
|
+
readonly def: ErrorDef<C>
|
|
62
|
+
readonly detail?: string
|
|
63
|
+
readonly instance?: string
|
|
64
|
+
readonly because?: string
|
|
65
|
+
readonly traceId?: string
|
|
66
|
+
readonly retryAfter?: number
|
|
67
|
+
readonly extensions: Readonly<Record<string, unknown>>
|
|
68
|
+
|
|
69
|
+
constructor(def: ErrorDef<C>, init: PlatformErrorInit = {}) {
|
|
70
|
+
|
|
71
|
+
super(def.code, init.cause === undefined ? undefined : { cause: init.cause })
|
|
72
|
+
this.name = 'PlatformError'
|
|
73
|
+
this.def = def
|
|
74
|
+
this.detail = init.detail
|
|
75
|
+
this.instance = init.instance
|
|
76
|
+
this.because = init.because
|
|
77
|
+
this.traceId = init.traceId
|
|
78
|
+
this.retryAfter = init.retryAfter
|
|
79
|
+
this.extensions = init.extensions ?? {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get code(): C {
|
|
83
|
+
return this.def.code
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get status(): number {
|
|
87
|
+
return this.def.status
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get retryable(): boolean {
|
|
91
|
+
return this.def.retryable
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isPlatformError(value: unknown): value is PlatformError {
|
|
96
|
+
return value instanceof PlatformError
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type ErrorFactory<C extends ErrorCode = ErrorCode> = ((
|
|
100
|
+
init?: PlatformErrorInit,
|
|
101
|
+
) => PlatformError<C>) & {
|
|
102
|
+
readonly def: ErrorDef<C>
|
|
103
|
+
|
|
104
|
+
readonly is: (value: unknown) => value is PlatformError<C>
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export class DuplicateErrorCodeError extends Error {
|
|
108
|
+
constructor(code: string) {
|
|
109
|
+
super(`duplicate error code: ${code}`)
|
|
110
|
+
this.name = 'DuplicateErrorCodeError'
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class ErrorCatalog {
|
|
115
|
+
readonly #byCode = new Map<ErrorCode, ErrorDef>()
|
|
116
|
+
|
|
117
|
+
define<const C extends ErrorCode>(input: DefineErrorInput<C>): ErrorFactory<C> {
|
|
118
|
+
if (this.#byCode.has(input.code)) {
|
|
119
|
+
|
|
120
|
+
throw new DuplicateErrorCodeError(input.code)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const cls = classDef(input.class)
|
|
124
|
+
const def: ErrorDef<C> = {
|
|
125
|
+
code: input.code,
|
|
126
|
+
class: input.class,
|
|
127
|
+
status: input.status ?? cls.status,
|
|
128
|
+
retryable: input.retryable ?? cls.retryable,
|
|
129
|
+
titleKey: input.titleKey ?? `errors.${input.code}.title`,
|
|
130
|
+
}
|
|
131
|
+
this.#byCode.set(def.code, def)
|
|
132
|
+
|
|
133
|
+
const factory = ((init?: PlatformErrorInit) => new PlatformError(def, init)) as {
|
|
134
|
+
(init?: PlatformErrorInit): PlatformError<C>
|
|
135
|
+
def: ErrorDef<C>
|
|
136
|
+
is: (value: unknown) => value is PlatformError<C>
|
|
137
|
+
}
|
|
138
|
+
factory.def = def
|
|
139
|
+
factory.is = (value: unknown): value is PlatformError<C> =>
|
|
140
|
+
isPlatformError(value) && value.def.code === def.code
|
|
141
|
+
|
|
142
|
+
return factory as ErrorFactory<C>
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get(code: string): ErrorDef | undefined {
|
|
146
|
+
return this.#byCode.get(code as ErrorCode)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
has(code: string): boolean {
|
|
150
|
+
return this.#byCode.has(code as ErrorCode)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
all(): readonly ErrorDef[] {
|
|
154
|
+
return [...this.#byCode.values()].sort((a, b) => a.code.localeCompare(b.code))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
get size(): number {
|
|
158
|
+
return this.#byCode.size
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const catalog = new ErrorCatalog()
|
|
163
|
+
|
|
164
|
+
export function defineError<const C extends ErrorCode>(
|
|
165
|
+
input: DefineErrorInput<C>,
|
|
166
|
+
): ErrorFactory<C> {
|
|
167
|
+
return catalog.define(input)
|
|
168
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
export interface ErrorClassDef {
|
|
4
|
+
|
|
5
|
+
readonly status: number
|
|
6
|
+
|
|
7
|
+
readonly retryable: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ERROR_CLASSES = {
|
|
11
|
+
|
|
12
|
+
OK: { status: 200, retryable: false },
|
|
13
|
+
|
|
14
|
+
CANCELLED: { status: 499, retryable: false },
|
|
15
|
+
|
|
16
|
+
UNKNOWN: { status: 500, retryable: false },
|
|
17
|
+
|
|
18
|
+
INVALID_ARGUMENT: { status: 400, retryable: false },
|
|
19
|
+
|
|
20
|
+
DEADLINE_EXCEEDED: { status: 504, retryable: true },
|
|
21
|
+
|
|
22
|
+
NOT_FOUND: { status: 404, retryable: false },
|
|
23
|
+
|
|
24
|
+
ALREADY_EXISTS: { status: 409, retryable: false },
|
|
25
|
+
|
|
26
|
+
PERMISSION_DENIED: { status: 403, retryable: false },
|
|
27
|
+
|
|
28
|
+
RESOURCE_EXHAUSTED: { status: 429, retryable: true },
|
|
29
|
+
|
|
30
|
+
FAILED_PRECONDITION: { status: 400, retryable: false },
|
|
31
|
+
|
|
32
|
+
ABORTED: { status: 409, retryable: true },
|
|
33
|
+
|
|
34
|
+
OUT_OF_RANGE: { status: 400, retryable: false },
|
|
35
|
+
|
|
36
|
+
UNIMPLEMENTED: { status: 501, retryable: false },
|
|
37
|
+
|
|
38
|
+
INTERNAL: { status: 500, retryable: false },
|
|
39
|
+
|
|
40
|
+
UNAVAILABLE: { status: 503, retryable: true },
|
|
41
|
+
|
|
42
|
+
UNAUTHENTICATED: { status: 401, retryable: false },
|
|
43
|
+
} as const satisfies Record<string, ErrorClassDef>
|
|
44
|
+
|
|
45
|
+
export type ErrorClass = keyof typeof ERROR_CLASSES
|
|
46
|
+
|
|
47
|
+
export const ERROR_CLASS_NAMES = Object.keys(ERROR_CLASSES) as readonly ErrorClass[]
|
|
48
|
+
|
|
49
|
+
export function classDef(cls: ErrorClass): ErrorClassDef {
|
|
50
|
+
return ERROR_CLASSES[cls]
|
|
51
|
+
}
|