@objectstack/types 17.2.0 → 17.4.0
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 +1106 -0
- package/dist/index.d.mts +649 -11
- package/dist/index.d.ts +649 -11
- package/dist/index.js +331 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +313 -3
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +12 -3
- package/dist/node.d.ts +12 -3
- package/dist/node.js +199 -2
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +201 -4
- package/dist/node.mjs.map +1 -1
- package/package.json +5 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TenancyPosture } from '@objectstack/spec/security';
|
|
2
2
|
import { ErrorCode, ApiError, FieldErrorCode } from '@objectstack/spec/api';
|
|
3
|
+
import { Logger } from '@objectstack/spec/contracts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Degraded-boot reporting, shared by every subsystem that can be told to boot
|
|
@@ -44,6 +45,47 @@ import { ErrorCode, ApiError, FieldErrorCode } from '@objectstack/spec/api';
|
|
|
44
45
|
*/
|
|
45
46
|
declare function emitDegradedBootBanner(message: string): void;
|
|
46
47
|
|
|
48
|
+
/**
|
|
49
|
+
* [#11343 / #12751] Verified-email predicate over a stored `sys_user` row — a
|
|
50
|
+
* fail-closed ALLOW-LIST over the representations a driver may hand back for
|
|
51
|
+
* the `sys_user.email_verified` boolean column (JS `true`, SQLite `1`, and
|
|
52
|
+
* their stringified forms). Everything else — `false`/`0`, `null`, an ABSENT
|
|
53
|
+
* field on an imported/legacy row, or any representation not listed — reads
|
|
54
|
+
* as UNVERIFIED. Absent-means-unverified is deliberate: treating a missing
|
|
55
|
+
* column as verified would re-open the exact hole this predicate closes for
|
|
56
|
+
* every row that predates the column.
|
|
57
|
+
*
|
|
58
|
+
* ONE resolution, several consumers, by design (#12751) — and since the
|
|
59
|
+
* #11663 platform-admin re-anchor (leg L4) the walled platform-admin
|
|
60
|
+
* ELEVATION GATE this paragraph used to name first is RETIRED: under a
|
|
61
|
+
* walled posture `bootstrapPlatformAdmin` writes no grant row and elevates
|
|
62
|
+
* nobody, it reports. Standing is derived PER REQUEST instead — from a
|
|
63
|
+
* config-anchored verified email, or the legacy unscoped grant row — so the
|
|
64
|
+
* consumer set now includes the authorization derivation itself:
|
|
65
|
+
*
|
|
66
|
+
* - `matchesConfiguredPlatformAdmin` (`@objectstack/core`
|
|
67
|
+
* `security/platform-admin.ts`), read at the one derivation site
|
|
68
|
+
* (`resolve-authz-context.ts` §6b-config), where an UNVERIFIED account
|
|
69
|
+
* holding a declared address confers nothing — and, through it,
|
|
70
|
+
* `plugin-auth`'s last-admin guard, whose administrator enumeration must
|
|
71
|
+
* answer the same question the resolver does;
|
|
72
|
+
* - `resolvePlatformAdminStanding` (`plugin-security`
|
|
73
|
+
* `platform-admin-service.ts`), the read-only standing/audit answer the
|
|
74
|
+
* walled boot reports from, and `isVerifiedPlatformOwnerRow` beside it
|
|
75
|
+
* (`platform-owner-wall-bypass.ts`), the Layer 0 wall bypass;
|
|
76
|
+
* - the walled owner-verification boot diagnostic (`plugin-auth`
|
|
77
|
+
* `walled-owner-verification-path.ts`, where the check decides whether the
|
|
78
|
+
* declared owner's account is already past needing a verification path).
|
|
79
|
+
*
|
|
80
|
+
* They must all answer "is this row verified?" identically — a drift is no
|
|
81
|
+
* longer just a boot warning forecasting a refusal that will not be made, it
|
|
82
|
+
* is a diagnostic, an audit surface or a guard disagreeing with who actually
|
|
83
|
+
* resolves PLATFORM_ADMIN on the next request. `@objectstack/types` is the
|
|
84
|
+
* shared home every one of those packages already resolves
|
|
85
|
+
* `OS_PLATFORM_OWNER_EMAIL` from (`env.ts`).
|
|
86
|
+
*/
|
|
87
|
+
declare function isEmailVerifiedUserRow(row: unknown): boolean;
|
|
88
|
+
|
|
47
89
|
/**
|
|
48
90
|
* Environment-variable helpers shared across `@objectstack/*` packages.
|
|
49
91
|
*
|
|
@@ -142,6 +184,49 @@ declare function resolveMultiOrgEnabled(): boolean;
|
|
|
142
184
|
* enforced is the `tenancy` service's answer (`isolationActive` / `degraded`).
|
|
143
185
|
*/
|
|
144
186
|
declare function resolveTenancyPosture(): TenancyPosture;
|
|
187
|
+
/**
|
|
188
|
+
* The env variable naming the deployment's PLATFORM OWNER account
|
|
189
|
+
* (#11184, the framework leg of cloud#1509).
|
|
190
|
+
*
|
|
191
|
+
* Exported as a constant so every message that names it quotes exactly one
|
|
192
|
+
* spelling: the walled boot guard in plugin-auth, and plugin-security's
|
|
193
|
+
* `bootstrapPlatformAdmin` — its fail-closed backstop for an undeclared or
|
|
194
|
+
* refused config, and the config-derived standing it logs beside it.
|
|
195
|
+
*
|
|
196
|
+
* ⚠️ That second site is no longer an ELEVATION refusal. Since the #11663
|
|
197
|
+
* platform-admin re-anchor (leg L4) the walled `bootstrapPlatformAdmin` writes
|
|
198
|
+
* no grant row and elevates nobody — it reports. Standing is derived PER
|
|
199
|
+
* REQUEST at `resolve-authz-context.ts` §6b-config, from a declared address
|
|
200
|
+
* held on a VERIFIED `sys_user` row.
|
|
201
|
+
*/
|
|
202
|
+
declare const PLATFORM_OWNER_EMAIL_ENV = "OS_PLATFORM_OWNER_EMAIL";
|
|
203
|
+
/**
|
|
204
|
+
* [#11184 / cloud#1509] Resolve the env-declared platform OWNER email —
|
|
205
|
+
* `OS_PLATFORM_OWNER_EMAIL`.
|
|
206
|
+
*
|
|
207
|
+
* Under a WALLED tenancy posture (`group` / `isolated`) the "first registrant
|
|
208
|
+
* becomes owner/platform admin" bootstrap path is REMOVED (maintainer ruling
|
|
209
|
+
* 2026-08-23, verbatim: 「1509 选择 env 指定 owner 邮箱」): on a walled
|
|
210
|
+
* deployment with self-registration reachable, whoever curls the sign-up
|
|
211
|
+
* endpoint first would otherwise receive the cross-tenant `admin_full_access`
|
|
212
|
+
* grant — measured on a real walled SaaS in cloud#1509. Platform admin is
|
|
213
|
+
* granted ONLY to the account whose email matches this variable, and a walled
|
|
214
|
+
* posture with no value declared REFUSES STARTUP (fail-closed, same reasoning
|
|
215
|
+
* as {@link resolveTenancyPosture}'s throw and ADR-0093 D5) rather than
|
|
216
|
+
* silently reverting to first-registrant elevation.
|
|
217
|
+
*
|
|
218
|
+
* The `single` posture never consults this: "first user is owner" is ruled
|
|
219
|
+
* reasonable there and unchanged.
|
|
220
|
+
*
|
|
221
|
+
* Returns the operator's value trimmed, or `undefined` when unset/blank.
|
|
222
|
+
* Comparison against `sys_user.email` is the CONSUMER's job and must be
|
|
223
|
+
* case-insensitive (this resolver echoes what the operator typed so refusal
|
|
224
|
+
* messages can quote it verbatim).
|
|
225
|
+
*
|
|
226
|
+
* Reads `process.env` live on each call, through `globalThis` like the other
|
|
227
|
+
* resolvers here (this package targets non-Node runtimes too).
|
|
228
|
+
*/
|
|
229
|
+
declare function resolvePlatformOwnerEmail(): string | undefined;
|
|
145
230
|
/**
|
|
146
231
|
* Escape hatch for the degraded-tenancy boot guard (ADR-0093 D5).
|
|
147
232
|
*
|
|
@@ -257,6 +342,27 @@ declare function resolveMcpStdioAutoStart(): {
|
|
|
257
342
|
* Deployments that let users self-create orgs SHOULD set a generous cap.
|
|
258
343
|
*/
|
|
259
344
|
declare function resolveOrgLimit(): number | undefined;
|
|
345
|
+
/**
|
|
346
|
+
* Maximum number of MEMBERS a single organization may hold, from
|
|
347
|
+
* `OS_ORG_MEMBERSHIP_LIMIT`. A different question from {@link resolveOrgLimit},
|
|
348
|
+
* which caps how many organizations one user may create.
|
|
349
|
+
*
|
|
350
|
+
* Unset → `undefined`, which the auth plugin forwards as "no cap". That default
|
|
351
|
+
* is a product decision, not an omission: seat entitlements are metered on AI
|
|
352
|
+
* seats, and plain membership is not a billed axis, so nothing about the
|
|
353
|
+
* platform wants a member ceiling.
|
|
354
|
+
*
|
|
355
|
+
* It has to be stated explicitly because better-auth's organization plugin
|
|
356
|
+
* substitutes a vendor default of **100** for an absent `membershipLimit`
|
|
357
|
+
* (`count >= (membershipLimit || 100)`), which reaches the operator as
|
|
358
|
+
* `Organization membership limit reached` — a refusal nobody in this codebase
|
|
359
|
+
* ever chose, on an axis the product does not limit.
|
|
360
|
+
*
|
|
361
|
+
* A deployment that DOES want a ceiling (a pilot, a trial tenant) sets a
|
|
362
|
+
* positive integer here. Non-positive or unparsable values read as unset rather
|
|
363
|
+
* than as zero: a typo must not be the thing that locks an organization.
|
|
364
|
+
*/
|
|
365
|
+
declare function resolveOrgMembershipLimit(): number | undefined;
|
|
260
366
|
/**
|
|
261
367
|
* SINGLE decision point for "is pinyin search recall on?" (#2486).
|
|
262
368
|
*
|
|
@@ -369,8 +475,10 @@ declare function _resetEnvDeprecationWarnings(): void;
|
|
|
369
475
|
* "Do not ship driver internals to clients" is a property of the HTTP
|
|
370
476
|
* boundary, not of one router, so the predicate lives here — the package both
|
|
371
477
|
* `@objectstack/rest` and `@objectstack/runtime` already depend on — and each
|
|
372
|
-
* boundary applies it in its own envelope. One heuristic, one place
|
|
373
|
-
*
|
|
478
|
+
* boundary applies it in its own envelope. One heuristic, one place — and
|
|
479
|
+
* since #16019 a FROZEN one: a phrasing it does not recognise is closed by the
|
|
480
|
+
* producer declaring its fault, never by a new row here (the ruling is
|
|
481
|
+
* recorded on {@link DIALECT_LEAK_PHRASINGS}).
|
|
374
482
|
*
|
|
375
483
|
* Deliberately a *heuristic over the message*, not a driver taxonomy: these
|
|
376
484
|
* errors arrive as plain `Error`s from a half-dozen dialects with no shared
|
|
@@ -667,7 +775,8 @@ declare function sendOk(res: EnvelopeResponse, data: unknown, status?: number):
|
|
|
667
775
|
* ## `extra` is `ApiError`'s own optional fields, not a `Record`
|
|
668
776
|
*
|
|
669
777
|
* Merged into `error`, and typed as exactly what `ApiErrorSchema` declares
|
|
670
|
-
* beside `code` and `message` — `details`, `category`, `requestId`,
|
|
778
|
+
* beside `code` and `message` — `details`, `category`, `requestId`,
|
|
779
|
+
* `httpStatus`, `declaredCode`, `userMessage`.
|
|
671
780
|
* `details` is the slot for structured context: `package-routes` puts a partial
|
|
672
781
|
* delete's per-item failures there, `settings-routes` the whole
|
|
673
782
|
* `SettingsActionResult`.
|
|
@@ -683,8 +792,80 @@ declare function sendOk(res: EnvelopeResponse, data: unknown, status?: number):
|
|
|
683
792
|
* Closing it at the shared builder is the part that lasts: an undeclared sibling
|
|
684
793
|
* is now a compile error in every module at once, rather than a key that quietly
|
|
685
794
|
* evaporates at the schema boundary in whichever module reintroduces it.
|
|
795
|
+
*
|
|
796
|
+
* ## `declaredCode` — declared by the schema, barred by this writer
|
|
797
|
+
*
|
|
798
|
+
* ADR-0112's 2026-08-17 amendment (#9106, extended to the flat `/data` door by
|
|
799
|
+
* #9232) rules the demote at EVERY door: `code` stays the closed vocabulary,
|
|
800
|
+
* and a thrown code that is not a member is demoted to a declared sibling,
|
|
801
|
+
* `ApiError.declaredCode` — the open, author-authored channel that carries a
|
|
802
|
+
* metadata app's OWN `.code` across the QuickJS boundary (#7867) and onto the
|
|
803
|
+
* wire.
|
|
804
|
+
*
|
|
805
|
+
* `ApiErrorSchema` has declared that field since #9106 and the flat door emits
|
|
806
|
+
* it, but it was absent from the `Pick` above — so it was a COMPILE ERROR for
|
|
807
|
+
* any route answering the NESTED envelope to pass one, and every such route
|
|
808
|
+
* dropped the producer's spelling. Nothing invalid shipped (the closed `code`
|
|
809
|
+
* still carried the derived member), which is what made the loss silent and
|
|
810
|
+
* one-directional: the author's spelling gone, and a consumer told by the ADR
|
|
811
|
+
* to read `declaredCode` finding nothing there. Declared-but-unemittable is a
|
|
812
|
+
* `declared = enforced` gap, and admitting the field closes it at the ONE
|
|
813
|
+
* writer rather than in each module that later notices.
|
|
814
|
+
*
|
|
815
|
+
* ⛔ Presence MEANS demotion, and this writer does not re-derive that — the
|
|
816
|
+
* CALLER does, with `demotedDeclaredCode` (`thrown-http-error.ts`, one file
|
|
817
|
+
* over), exactly as the flat door's `thrownCodeFields` already does. That
|
|
818
|
+
* helper answers `undefined` when the producer's spelling IS the vocabulary
|
|
819
|
+
* member already sitting in `code`, which is what stops a registered refusal
|
|
820
|
+
* from carrying two spellings of one fact — `ApiErrorSchema.declaredCode`'s
|
|
821
|
+
* documented invariant. Passing a raw `thrown.declaredCode` re-opens exactly
|
|
822
|
+
* that, and no type here can catch it: vocabulary and position stay two
|
|
823
|
+
* decisions (#9232), so the demotion rule stays with the resolver that owns
|
|
824
|
+
* it rather than being restated in the envelope writer.
|
|
825
|
+
*
|
|
826
|
+
* ## `userMessage` — the second declared channel, and why this `Pick` stays explicit
|
|
827
|
+
*
|
|
828
|
+
* #9934's producer-side opt-in (maintainer ruling 2026-08-19 on objectui#5210,
|
|
829
|
+
* option 1) declares `ApiError.userMessage`: the text a producer marked, AT
|
|
830
|
+
* THROW TIME, as addressed to the END USER. Presence IS the marking — a
|
|
831
|
+
* consumer that sees the field renders it verbatim and keeps its generic
|
|
832
|
+
* substitution (#3821) for everything unmarked.
|
|
833
|
+
*
|
|
834
|
+
* The schema declared it and this writer barred it, with the same
|
|
835
|
+
* one-directional silence `declaredCode` had: the other two doors already emit
|
|
836
|
+
* it — the flat `/data` door through `withDeclaredUserMessage`
|
|
837
|
+
* (`rest/error-response.ts`) and the dispatcher door through
|
|
838
|
+
* `thrown.userMessage` (`runtime/http-dispatcher.ts`) — while a route
|
|
839
|
+
* answering the NESTED envelope could not, so an author's deliberate,
|
|
840
|
+
* localized refusal text was dropped on this door alone. Nothing invalid
|
|
841
|
+
* shipped; the text simply was not there.
|
|
842
|
+
*
|
|
843
|
+
* The channel is live on both ends, which is what makes admitting it a repair
|
|
844
|
+
* rather than a new declared-but-dead surface: a hook sets it at throw time —
|
|
845
|
+
* host-side, or a metadata app's sandboxed body whose `e.userMessage` crosses
|
|
846
|
+
* the QuickJS boundary through `SANDBOX_ERROR_PASSTHROUGH`
|
|
847
|
+
* (`runtime/sandbox/quickjs-runner.ts`) — and `resolveThrownHttpError` already
|
|
848
|
+
* carries it onto `ThrownHttpError` for every caller of the shared resolver.
|
|
849
|
+
*
|
|
850
|
+
* ⛔ Unlike `declaredCode`, this field carries NO invariant for the caller to
|
|
851
|
+
* re-derive. `declaredCode`'s presence MEANS demotion, so its caller passes
|
|
852
|
+
* `demotedDeclaredCode(thrown)` rather than the raw field; `userMessage`'s
|
|
853
|
+
* presence means only that the producer opted in, and `declaredUserMessage`
|
|
854
|
+
* has already decided that (a non-empty string, or nothing at all). The caller
|
|
855
|
+
* passes `thrown.userMessage` straight through, exactly as the dispatcher door
|
|
856
|
+
* does.
|
|
857
|
+
*
|
|
858
|
+
* That difference is why `extra` stays an explicit `Pick` rather than becoming
|
|
859
|
+
* "every optional field of `ApiError`". A derivation would admit each future
|
|
860
|
+
* optional on the day it lands, with nobody asked whether that channel should
|
|
861
|
+
* cross this door or what obligation it hands the caller — and the two fields
|
|
862
|
+
* above needed opposite answers to exactly that question. Recorded for the next
|
|
863
|
+
* reader, because it is the honest cost: with `userMessage` admitted the `Pick`
|
|
864
|
+
* now names ALL SIX of `ApiError`'s optional fields, so this gate has to date
|
|
865
|
+
* rejected none. What it has produced is a different caller obligation per
|
|
866
|
+
* field, which a derivation cannot produce at all.
|
|
686
867
|
*/
|
|
687
|
-
declare function sendError(res: EnvelopeResponse, status: number, code: ErrorCode, message: string, extra?: Pick<ApiError, 'category' | 'httpStatus' | 'details' | 'requestId'>): void;
|
|
868
|
+
declare function sendError(res: EnvelopeResponse, status: number, code: ErrorCode, message: string, extra?: Pick<ApiError, 'category' | 'httpStatus' | 'details' | 'requestId' | 'declaredCode' | 'userMessage'>): void;
|
|
688
869
|
|
|
689
870
|
/**
|
|
690
871
|
* The ONE rule for "what HTTP answer does a THROWN error declare?" (#8016).
|
|
@@ -756,6 +937,14 @@ declare function sendError(res: EnvelopeResponse, status: number, code: ErrorCod
|
|
|
756
937
|
* answers come from ONE function, which is what keeps agreement a construction
|
|
757
938
|
* rather than two suites agreeing about literals.
|
|
758
939
|
*
|
|
940
|
+
* [#12509] And the channel has a SCOPE, ruled 2026-08-27 (option D): on a 5xx
|
|
941
|
+
* the producer did not declare, the demoted spelling came off an undeclared
|
|
942
|
+
* producer and is withheld with the prose, while an author-declared code
|
|
943
|
+
* survives. The discriminator is {@link serverFaultProvenance} — one function,
|
|
944
|
+
* read by {@link demotedDeclaredCode}, which every door already calls, so no
|
|
945
|
+
* registrar carries a variant. Read that function's note for why the status
|
|
946
|
+
* channel is the only honest signal here.
|
|
947
|
+
*
|
|
759
948
|
* ## What this deliberately does NOT decide
|
|
760
949
|
*
|
|
761
950
|
* - **Message disclosure.** A 5xx message may name physical tables or carry a
|
|
@@ -814,6 +1003,11 @@ interface ThrownHttpError {
|
|
|
814
1003
|
* every door (#9106) — but for the wire's `declaredCode` channel when the
|
|
815
1004
|
* spelling is not a vocabulary member ({@link demotedDeclaredCode}). See the
|
|
816
1005
|
* module note on why there are two.
|
|
1006
|
+
*
|
|
1007
|
+
* ⚠️ This field records what the producer WROTE, not what a boundary may
|
|
1008
|
+
* emit: since #12509 a demoted spelling is withheld on an undeclared 5xx.
|
|
1009
|
+
* ⛔ Read {@link demotedDeclaredCode}, never this field, when deciding what
|
|
1010
|
+
* goes on a wire.
|
|
817
1011
|
*/
|
|
818
1012
|
declaredCode?: string;
|
|
819
1013
|
/** The thrown message, UNSANITISED — see the module note on disclosure. */
|
|
@@ -888,21 +1082,206 @@ declare function resolveThrownHttpError(error: unknown, fallbackStatus?: number)
|
|
|
888
1082
|
* consumer keeps its generic substitution (#3821 preserved by construction).
|
|
889
1083
|
*/
|
|
890
1084
|
declare function declaredUserMessage(error: unknown): string | undefined;
|
|
1085
|
+
/**
|
|
1086
|
+
* [#12509] WHO named this 5xx — the producer, or this resolver's fallback.
|
|
1087
|
+
* `undefined` for anything below 500, where nothing is sanitised at all.
|
|
1088
|
+
*
|
|
1089
|
+
* This is the ONE definition of the distinction ADR-0112's 5xx-sanitisation
|
|
1090
|
+
* scope turns on (maintainer ruling 2026-08-27, option D), and it exists as a
|
|
1091
|
+
* named function rather than as an inline conjunction because TWO rules read
|
|
1092
|
+
* it and they read opposite limbs:
|
|
1093
|
+
*
|
|
1094
|
+
* - `'undeclared'` — the throw declared no HTTP answer, so
|
|
1095
|
+
* {@link ThrownHttpError.status} is the caller's `fallbackStatus` and
|
|
1096
|
+
* EVERYTHING this resolver picked up off that throw is the producer's
|
|
1097
|
+
* internals rather than an answer it composed. A driver errno
|
|
1098
|
+
* (`SQLITE_ERROR`, `42P01`) is the measured case, and it is why
|
|
1099
|
+
* {@link demotedDeclaredCode} withholds the code here: the spelling names
|
|
1100
|
+
* the backend, which is one of the two disclosures the 5xx message
|
|
1101
|
+
* withhold exists to prevent (`looksLikeInternalErrorLeak`; the other,
|
|
1102
|
+
* identifiers, is already covered).
|
|
1103
|
+
* - `'declared'` — the producer named a 5xx ITSELF, so its code is authored
|
|
1104
|
+
* and survives. #11718's `{ status: 503, code: 'SERVICE_UNAVAILABLE' }`
|
|
1105
|
+
* relay is this limb, and so is a metadata app's own 5xx refusal spelling
|
|
1106
|
+
* (#7867), which the ADR-0112 amendment wrote `declaredCode` for.
|
|
1107
|
+
*
|
|
1108
|
+
* ⚠️ The DISCRIMINATOR is the status channel, not the code's shape. There is
|
|
1109
|
+
* no other structural signal: a driver errno and an app's own spelling both
|
|
1110
|
+
* arrive on `.code` as a plain string, so anything that told them apart by
|
|
1111
|
+
* LOOKING at the string would be a heuristic over an open channel — the
|
|
1112
|
+
* consumer-side tolerance ADR-0112 exists to forbid, and unfalsifiable besides
|
|
1113
|
+
* (nothing stops an app from spelling `SQLITE_ERROR`). The cost of the
|
|
1114
|
+
* structural answer is stated rather than hidden: a producer that spells a
|
|
1115
|
+
* code but declares NO status loses that code on a 5xx. It keeps it by
|
|
1116
|
+
* declaring the status it means, which is the shape the ADR already asks for.
|
|
1117
|
+
*
|
|
1118
|
+
* ⛔ NOT gated on whether `looksLikeInternalErrorLeak` actually fired on the
|
|
1119
|
+
* message. That predicate is a heuristic over a DIFFERENT channel, and gating
|
|
1120
|
+
* here on it would leak the errno for exactly the dialects whose prose the
|
|
1121
|
+
* heuristic misses — the ceiling `sendThrownError`'s note records. The 5xx
|
|
1122
|
+
* sanitisation REGIME is the condition, not one of its two outcomes.
|
|
1123
|
+
*
|
|
1124
|
+
* ⭐ #12281 — the prose axis of the same 2026-08-27 ruling — is the
|
|
1125
|
+
* `'declared'` limb of this same function: the dispatcher door withholds the
|
|
1126
|
+
* message of EVERY declared 5xx, aligning to `/data`. It is a separate card
|
|
1127
|
+
* with its own measurement-first step, so nothing here applies it; this
|
|
1128
|
+
* function is the shape it will read rather than a second copy it would have
|
|
1129
|
+
* to grow.
|
|
1130
|
+
*/
|
|
1131
|
+
type ServerFaultProvenance = 'declared' | 'undeclared';
|
|
1132
|
+
/** See {@link ServerFaultProvenance}. */
|
|
1133
|
+
declare function serverFaultProvenance(thrown: ThrownHttpError): ServerFaultProvenance | undefined;
|
|
891
1134
|
/**
|
|
892
1135
|
* The producer's spelling a boundary should surface as the wire's
|
|
893
1136
|
* `declaredCode` beside the closed `code` — or `undefined` when there is
|
|
894
1137
|
* nothing to surface (#9106).
|
|
895
1138
|
*
|
|
896
1139
|
* Present exactly when the throw spelled a code that did NOT survive into
|
|
897
|
-
* {@link ThrownHttpError.code} — i.e. the demote happened
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
* "presence means demotion"
|
|
902
|
-
* semantics) has one definition.
|
|
1140
|
+
* {@link ThrownHttpError.code} — i.e. the demote happened — AND the answer is
|
|
1141
|
+
* not an undeclared server fault. A registered code is already in `code`, so
|
|
1142
|
+
* emitting it again would put two spellings of one fact on every refusal; a
|
|
1143
|
+
* throw with no code has nothing to declare. Spelled once here rather than as
|
|
1144
|
+
* three `!==` comparisons at three exits, so "presence means demotion"
|
|
1145
|
+
* (`ApiErrorSchema.declaredCode`'s documented semantics) has one definition.
|
|
1146
|
+
*
|
|
1147
|
+
* [#12509] The withhold limb, ruled 2026-08-27 (option D): on a 5xx the
|
|
1148
|
+
* producer did NOT declare, the spelling this resolver demoted came off an
|
|
1149
|
+
* undeclared producer — a driver errno, measured on the wire at three of this
|
|
1150
|
+
* repo's doors — and it is withheld along with the prose. An AUTHOR-declared
|
|
1151
|
+
* code survives at every status. The judgement lives in
|
|
1152
|
+
* {@link serverFaultProvenance}; it is applied HERE, in the one read every
|
|
1153
|
+
* boundary already makes, so all of them inherit it without a door growing a
|
|
1154
|
+
* rule of its own. ⛔ Do not re-derive the condition at a door: a per-door
|
|
1155
|
+
* variant is the divergence this channel has now been repaired for twice.
|
|
903
1156
|
*/
|
|
904
1157
|
declare function demotedDeclaredCode(thrown: ThrownHttpError): string | undefined;
|
|
905
1158
|
|
|
1159
|
+
/**
|
|
1160
|
+
* [#14310] The one rule for "a 5xx must never be silent", shared by every
|
|
1161
|
+
* transport that turns a fault into an HTTP envelope.
|
|
1162
|
+
*
|
|
1163
|
+
* ## The hole this closes
|
|
1164
|
+
*
|
|
1165
|
+
* A 500 that leaves no server-side line is diagnosed from the browser or not
|
|
1166
|
+
* at all. Measured on `main`: a plain `Error` thrown out of a dispatcher route
|
|
1167
|
+
* answered `500 INTERNAL_ERROR` with **zero** log records at any level — the
|
|
1168
|
+
* only evidence was the client's console and the response body. The failure
|
|
1169
|
+
* that motivated this had been reachable for a week and nobody saw it, which
|
|
1170
|
+
* is AGENTS.md "Route & surface ownership §3 — absence must be loud" inverted.
|
|
1171
|
+
*
|
|
1172
|
+
* The reporting that DID exist was not a substitute, in two independent ways:
|
|
1173
|
+
*
|
|
1174
|
+
* 1. `ErrorReporter.captureException` is an APM channel and defaults to
|
|
1175
|
+
* `NoopErrorReporter`. A dev server — the surface an operator actually
|
|
1176
|
+
* watches — wires no reporter, so the capture was a no-op every time.
|
|
1177
|
+
* 2. It is fed by `res.__obsRecordedError`, which only the THROWN exit sets.
|
|
1178
|
+
* A dispatcher route that catches its own fault and RETURNS a 5xx envelope
|
|
1179
|
+
* (`deps.errorFromThrown`, which is how every `/packages` handler answers)
|
|
1180
|
+
* records nothing, so even a wired reporter never saw those.
|
|
1181
|
+
*
|
|
1182
|
+
* This module is the log half, and it is deliberately not the reporter half:
|
|
1183
|
+
* an APM capture is opt-in telemetry, a log line is the operator's floor.
|
|
1184
|
+
*
|
|
1185
|
+
* ## Why it lives here
|
|
1186
|
+
*
|
|
1187
|
+
* Same argument, and the same package, as `resolveThrownHttpError` one file
|
|
1188
|
+
* over: a rule two doors must agree on cannot live inside one of them.
|
|
1189
|
+
* `@objectstack/runtime` depends on `@objectstack/rest`, so an import between
|
|
1190
|
+
* the two doors could only ever point one way — which is exactly why the
|
|
1191
|
+
* "what status does this throw mean" rule was moved here in #8016. "Is this
|
|
1192
|
+
* answer worth an operator's attention" is the same kind of rule, read by the
|
|
1193
|
+
* same two doors, so it gets the same home rather than a second one.
|
|
1194
|
+
*
|
|
1195
|
+
* Living beside {@link sendError} is what makes the REST side automatic: that
|
|
1196
|
+
* writer is the single exit for every nested-envelope 5xx, so the direct-mount
|
|
1197
|
+
* registrars need no per-door call and cannot forget one. Each transport logs
|
|
1198
|
+
* at its own single exit, so a fault costs one line and never two.
|
|
1199
|
+
*
|
|
1200
|
+
* ## `error` level, and why that clears the default
|
|
1201
|
+
*
|
|
1202
|
+
* The requirement is that the line survives `--log-level`'s DEFAULT. The CLI
|
|
1203
|
+
* default is `warn` (`packages/cli/src/utils/log-level.ts`) and `error` (40)
|
|
1204
|
+
* outranks `warn` (30) in `LEVEL_PRIORITY`, so an `error` record passes the
|
|
1205
|
+
* default threshold without any bypass of the level system. An operator who
|
|
1206
|
+
* asks for `--log-level silent` still gets silence: that is a deliberate
|
|
1207
|
+
* instruction, not the default this issue is about.
|
|
1208
|
+
*
|
|
1209
|
+
* ## 5xx only
|
|
1210
|
+
*
|
|
1211
|
+
* 4xx stays quiet, deliberately and at this one gate rather than at each call
|
|
1212
|
+
* site. A client error is the caller's mistake and the response already
|
|
1213
|
+
* explains it; logging them is how the `/meta` `?state=draft` probe once
|
|
1214
|
+
* printed 45 stack traces in one browsing session. `isServerFault` is the
|
|
1215
|
+
* whole rule: at or above 500.
|
|
1216
|
+
*/
|
|
1217
|
+
|
|
1218
|
+
/** The request coordinates an operator needs to find the failing call. */
|
|
1219
|
+
interface ServerFaultRequest {
|
|
1220
|
+
/** HTTP method, e.g. `GET`. */
|
|
1221
|
+
method?: string;
|
|
1222
|
+
/** Request path as served, e.g. `/api/v1/packages`. */
|
|
1223
|
+
path?: string;
|
|
1224
|
+
/** Correlation id — the `X-Request-Id` echoed on the response. */
|
|
1225
|
+
requestId?: string;
|
|
1226
|
+
}
|
|
1227
|
+
/** One fault, as the emitting door knows it. */
|
|
1228
|
+
interface ServerFaultLogInput {
|
|
1229
|
+
/** The HTTP status about to be written. Below 500 nothing is logged. */
|
|
1230
|
+
status: number;
|
|
1231
|
+
/**
|
|
1232
|
+
* The original thrown value, when the door still holds it. Carries the
|
|
1233
|
+
* stack; the wire body never does, because a 5xx message is withheld.
|
|
1234
|
+
*/
|
|
1235
|
+
error?: unknown;
|
|
1236
|
+
/** The envelope's `code`, when the door resolved one. */
|
|
1237
|
+
code?: string;
|
|
1238
|
+
/**
|
|
1239
|
+
* The message to print when {@link ServerFaultLogInput.error} carries
|
|
1240
|
+
* none — a declared fault built from a string rather than a throw.
|
|
1241
|
+
*/
|
|
1242
|
+
message?: string;
|
|
1243
|
+
/** Where the call came in. */
|
|
1244
|
+
request?: ServerFaultRequest;
|
|
1245
|
+
}
|
|
1246
|
+
/** The prefix every fault line carries, so an operator can grep one token. */
|
|
1247
|
+
declare const SERVER_FAULT_LOG_PREFIX = "[5xx]";
|
|
1248
|
+
/**
|
|
1249
|
+
* THE predicate. A response is a server fault worth a line exactly when its
|
|
1250
|
+
* status is 5xx. Exported so a door can decide without restating `>= 500`.
|
|
1251
|
+
*/
|
|
1252
|
+
declare function isServerFault(status: number): boolean;
|
|
1253
|
+
/**
|
|
1254
|
+
* The human half of the line: `[5xx] 500 GET /api/v1/packages — <message>`.
|
|
1255
|
+
* Split out so both the emitted record and a test can name the same string.
|
|
1256
|
+
*/
|
|
1257
|
+
declare function serverFaultLogMessage(input: ServerFaultLogInput): string;
|
|
1258
|
+
/**
|
|
1259
|
+
* The structured half. `status`/`code`/`requestId` are what a log search keys
|
|
1260
|
+
* on; `method`/`path` repeat the message's coordinates because a JSON sink
|
|
1261
|
+
* indexes fields, not prose.
|
|
1262
|
+
*/
|
|
1263
|
+
declare function serverFaultLogMeta(input: ServerFaultLogInput): Record<string, unknown>;
|
|
1264
|
+
/**
|
|
1265
|
+
* Emit EXACTLY ONE `error`-level record for a 5xx, or nothing at all.
|
|
1266
|
+
*
|
|
1267
|
+
* Returns whether a record was emitted, so a caller that must not double-log
|
|
1268
|
+
* can branch on the answer rather than re-deriving the 5xx test.
|
|
1269
|
+
*
|
|
1270
|
+
* `logger` is optional: a door with no injected logger falls back to
|
|
1271
|
+
* `console.error`, because the point of this function is that the line exists
|
|
1272
|
+
* even on a surface nobody configured. Emission never throws — a logging
|
|
1273
|
+
* failure must not become a second fault on top of the one being reported.
|
|
1274
|
+
*/
|
|
1275
|
+
declare function logServerFault(input: ServerFaultLogInput, logger?: Logger): boolean;
|
|
1276
|
+
/**
|
|
1277
|
+
* Read request coordinates off whatever request object the transport hands
|
|
1278
|
+
* the door. Adapters disagree on the spelling (`path` / `url` /
|
|
1279
|
+
* `originalUrl`), and the request id may be on the object (set by
|
|
1280
|
+
* `instrumentRouteHandler`) or only on the incoming header — so both are
|
|
1281
|
+
* read here, once, instead of at each call site.
|
|
1282
|
+
*/
|
|
1283
|
+
declare function describeFaultRequest(req: unknown): ServerFaultRequest;
|
|
1284
|
+
|
|
906
1285
|
/** The HTTP status a validation failure maps to when the error names none. */
|
|
907
1286
|
declare const VALIDATION_FAILED_STATUS = 400;
|
|
908
1287
|
interface ValidationFailureDetails {
|
|
@@ -963,6 +1342,118 @@ declare function fieldsFromZodIssues(issues: Array<{
|
|
|
963
1342
|
message: string;
|
|
964
1343
|
}>;
|
|
965
1344
|
|
|
1345
|
+
/**
|
|
1346
|
+
* The machine-readable half of a `RESUME_FAILED` — a decision that is durably
|
|
1347
|
+
* recorded whose flow run could not be resumed (#13807).
|
|
1348
|
+
*
|
|
1349
|
+
* ## The condition
|
|
1350
|
+
*
|
|
1351
|
+
* An approval decision finalises: the `sys_approval_request` row flips to its
|
|
1352
|
+
* terminal status, the audit action is written, the record's mirrored status
|
|
1353
|
+
* field advances — and only THEN is the owning flow run resumed. When that
|
|
1354
|
+
* resume fails the writes are already durable, so the outcome stands and the
|
|
1355
|
+
* run is stranded. `@objectstack/plugin-approvals` throws rather than
|
|
1356
|
+
* answering `resumed: false`, deliberately: a recorded decision whose flow
|
|
1357
|
+
* never advances is #4420's zombie half-state, and the contract
|
|
1358
|
+
* (`ApprovalDecisionResult`) declares the throw intentional.
|
|
1359
|
+
*
|
|
1360
|
+
* ⛔ This module does NOT change that posture. The maintainer ruled on
|
|
1361
|
+
* 2026-09-04 (decision batch #37, option B) that the door **keeps its status
|
|
1362
|
+
* code** — the 500-class `RESUME_FAILED` — because the effect landing while
|
|
1363
|
+
* the run strands is still a failure. What the ruling changed is that the
|
|
1364
|
+
* throw must be *truthful*: the facts a caller needs were being discarded.
|
|
1365
|
+
*
|
|
1366
|
+
* ## What was being discarded, measured
|
|
1367
|
+
*
|
|
1368
|
+
* Three states coexist after such a call: the caller reads 500, the request
|
|
1369
|
+
* IS in its terminal status, and the run is stranded. A caller — human,
|
|
1370
|
+
* script, or agent — reads 500 as "the rejection did not happen" and retries
|
|
1371
|
+
* or escalates. It did happen. Before this module the only carrier of that
|
|
1372
|
+
* fact was English prose in `error`, so an operator had to regex the run id
|
|
1373
|
+
* out of a sentence, and nothing said whether the run was repairable at all.
|
|
1374
|
+
*
|
|
1375
|
+
* Meanwhile the engine already knew. `AutomationResult.status: 'stranded'`
|
|
1376
|
+
* (`@objectstack/spec`, `automation-service.ts`) is stamped on exactly the
|
|
1377
|
+
* exit that journals a repair snapshot — the shape-4 name from #13937 — and
|
|
1378
|
+
* it is distinct from `'failed'` on purpose: `'failed'` says the run ran and
|
|
1379
|
+
* was rejected, `'stranded'` says a recorded continuation stopped mid-flight
|
|
1380
|
+
* and an operator has something to repair. It had a producer and, until this
|
|
1381
|
+
* module, **zero consumers**: the approvals door read only
|
|
1382
|
+
* `success` / `code` / `error` off the resume result and dropped it one line
|
|
1383
|
+
* before the envelope was built.
|
|
1384
|
+
*
|
|
1385
|
+
* ## Why it lives in `@objectstack/types`
|
|
1386
|
+
*
|
|
1387
|
+
* Same Home rule as {@link ValidationFailureDetails} one file over: the
|
|
1388
|
+
* PRODUCER is `@objectstack/plugin-approvals` and the CONSUMER is the REST
|
|
1389
|
+
* door in `@objectstack/rest`, and rest cannot import a plugin. Both already
|
|
1390
|
+
* depend on this package, so the shared declaration adds no dependency edge —
|
|
1391
|
+
* and keeping the constructor and the reader in ONE module is what stops the
|
|
1392
|
+
* two sides from drifting into a stringly-typed agreement about a property
|
|
1393
|
+
* name.
|
|
1394
|
+
*
|
|
1395
|
+
* ⛔ Deliberately NOT a tolerant reader. There is no alias chain and no prose
|
|
1396
|
+
* parsing: a body either carries the four facts the producer attached, or the
|
|
1397
|
+
* response is exactly what it was before. A `RESUME_FAILED` raised by
|
|
1398
|
+
* something that never had a decision to report (a test double, a future
|
|
1399
|
+
* caller) must not be dressed up as one.
|
|
1400
|
+
*/
|
|
1401
|
+
/**
|
|
1402
|
+
* The four facts a stranded decision publishes alongside its `code` and
|
|
1403
|
+
* `error`. Every field is present or the whole envelope is absent — a partial
|
|
1404
|
+
* one would let a consumer branch on `finalized === undefined` and read it as
|
|
1405
|
+
* "the decision did not stand", which is the exact misreading this exists to
|
|
1406
|
+
* end.
|
|
1407
|
+
*/
|
|
1408
|
+
interface StrandedDecisionDetails {
|
|
1409
|
+
/**
|
|
1410
|
+
* Always `true`. The decision reached a terminal state and is durable; the
|
|
1411
|
+
* 5xx is about the run, never about the decision. Spelled as a literal
|
|
1412
|
+
* rather than omitted so a consumer reads a fact instead of an absence.
|
|
1413
|
+
*/
|
|
1414
|
+
finalized: true;
|
|
1415
|
+
/**
|
|
1416
|
+
* Which outcome was recorded — `'approve'` / `'reject'` for a decision, and
|
|
1417
|
+
* the sibling doors on the same path for the rest (`'revise'` on a
|
|
1418
|
+
* send-back, `'resubmit'`). Free-form by design: the vocabulary belongs to
|
|
1419
|
+
* the producing service, not to this recogniser.
|
|
1420
|
+
*/
|
|
1421
|
+
decision: string;
|
|
1422
|
+
/** The stranded run. The one identifier an operator needs to act. */
|
|
1423
|
+
runId: string;
|
|
1424
|
+
/**
|
|
1425
|
+
* Whether the engine says this run can still be repaired — derived from the
|
|
1426
|
+
* engine's own discriminator (`AutomationResult.status === 'stranded'`),
|
|
1427
|
+
* never from the message text and never assumed.
|
|
1428
|
+
*
|
|
1429
|
+
* `false` is the honest answer for every other exit, including the ones
|
|
1430
|
+
* that report no status at all (a lost run, an engine that predates the
|
|
1431
|
+
* discriminator). ⛔ Absence of the signal is not repairability: promising a
|
|
1432
|
+
* repair verb that will refuse is worse than promising nothing.
|
|
1433
|
+
*/
|
|
1434
|
+
repairable: boolean;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Structured details for a thrown stranded-decision failure, or `undefined`
|
|
1438
|
+
* when `err` is not one.
|
|
1439
|
+
*
|
|
1440
|
+
* Callers use the `undefined` result as the predicate and the returned object
|
|
1441
|
+
* as the payload, so the two can never disagree — the same contract
|
|
1442
|
+
* `validationFailureDetails` keeps one module over. Every field is validated:
|
|
1443
|
+
* a malformed carrier answers `undefined` rather than putting a half-envelope
|
|
1444
|
+
* on the wire.
|
|
1445
|
+
*/
|
|
1446
|
+
declare function strandedDecisionDetails(err: unknown): StrandedDecisionDetails | undefined;
|
|
1447
|
+
/**
|
|
1448
|
+
* The CONSTRUCTOR for the shape {@link strandedDecisionDetails} recognises —
|
|
1449
|
+
* kept in the same module so the two can never drift.
|
|
1450
|
+
*
|
|
1451
|
+
* The message stays the producer's own, unchanged: the prose is what a human
|
|
1452
|
+
* reads in a log, the details are what a machine reads on the wire, and this
|
|
1453
|
+
* ruling added the second without touching the first.
|
|
1454
|
+
*/
|
|
1455
|
+
declare function strandedDecisionFailure(message: string, details: StrandedDecisionDetails): Error;
|
|
1456
|
+
|
|
966
1457
|
/**
|
|
967
1458
|
* The one home for Postgres' `«sub-object» "x" of relation "y" …` phrasing
|
|
968
1459
|
* (#6615).
|
|
@@ -1154,6 +1645,153 @@ declare function isUniqueViolationError(error: unknown): boolean;
|
|
|
1154
1645
|
*/
|
|
1155
1646
|
declare function uniqueViolationColumn(error: unknown): string | undefined;
|
|
1156
1647
|
|
|
1648
|
+
/**
|
|
1649
|
+
* [#13438] The physical table a driver's statement TARGETED, declared on the
|
|
1650
|
+
* error envelope by the producer that knows it.
|
|
1651
|
+
*
|
|
1652
|
+
* `readObject` closed the #13324 hole for callers that can name what they read
|
|
1653
|
+
* — and left a residual one layer down. A caller names its OBJECT (the API
|
|
1654
|
+
* name); a driver compiles the statement against the PHYSICAL table, and for a
|
|
1655
|
+
* federated object (ADR-0015, `external.remoteName`) those are two different
|
|
1656
|
+
* names. `driver-sql` reads `crm_order` from `legacy_orders`, so when that
|
|
1657
|
+
* remote is genuinely absent the dialect phrase names `legacy_orders`, the
|
|
1658
|
+
* caller names `crm_order`, and the comparison called a real missing table
|
|
1659
|
+
* "about something else" — loud, for the one case the licence was built for.
|
|
1660
|
+
*
|
|
1661
|
+
* Nothing at a call site can fold that away: the mapping lives on the driver
|
|
1662
|
+
* instance, and asking every caller to consult it is the guessing this channel
|
|
1663
|
+
* exists to remove (maintainer ruling 2026-09-01, option 2 on the card). So the
|
|
1664
|
+
* fact is declared where it is known — the driver that composed the envelope
|
|
1665
|
+
* stamps the table its statement targeted onto it — and the predicate PREFERS
|
|
1666
|
+
* a declared table over the caller-supplied `readObject`. The caller never
|
|
1667
|
+
* needs to know a federated object's remote name, and a driver that declares
|
|
1668
|
+
* nothing gets exactly the #13324 behaviour.
|
|
1669
|
+
*
|
|
1670
|
+
* A symbol key from the global registry, held non-enumerable: the carrier
|
|
1671
|
+
* discipline `driver-sql` already applies to its withheld-diagnostic symbols
|
|
1672
|
+
* and to the envelope's own `cause`. Readable by code; invisible to
|
|
1673
|
+
* `JSON.stringify`, `{ ...err }`, `Object.keys`, `for…in` and the
|
|
1674
|
+
* structured-clone boundary — so the physical table name, the very thing the
|
|
1675
|
+
* envelope's composed message withholds, can never ride back onto a wire that
|
|
1676
|
+
* serialises the error. `Symbol.for` so a duplicated copy of this package
|
|
1677
|
+
* resolves the same key.
|
|
1678
|
+
*
|
|
1679
|
+
* ⚠️ A declaration is EVIDENCE, so it also narrows the one-argument form: an
|
|
1680
|
+
* envelope declaring `legacy_orders` whose dialect phrase names some other
|
|
1681
|
+
* relation reads not-benign even with no `readObject` — the driver supplied
|
|
1682
|
+
* the fact the caller could not. That is the #13324 verdict reached without
|
|
1683
|
+
* the caller's help, in the direction the module docblock calls cheap.
|
|
1684
|
+
*/
|
|
1685
|
+
declare const DRIVER_TARGETED_TABLE: symbol;
|
|
1686
|
+
/**
|
|
1687
|
+
* Declare, on `error`, the physical table the statement that raised it targeted.
|
|
1688
|
+
*
|
|
1689
|
+
* The producer's half of {@link DRIVER_TARGETED_TABLE} — for a driver composing
|
|
1690
|
+
* an error envelope over a dialect failure. `table` is the name the statement
|
|
1691
|
+
* was compiled against (a federated object's `external.remoteName`, otherwise
|
|
1692
|
+
* the object's own table), bare: the comparison folds away schema and database
|
|
1693
|
+
* qualifiers on both sides, so none is needed here.
|
|
1694
|
+
*
|
|
1695
|
+
* Non-enumerable and non-writable, and the FIRST declaration wins: the actor
|
|
1696
|
+
* that compiled the statement is the one that knows its target, and a later,
|
|
1697
|
+
* more distant wrapper re-declaring it would be re-introducing the guess. (The
|
|
1698
|
+
* predicate applies the same rule across a `cause` chain: the declaration
|
|
1699
|
+
* NEAREST the dialect phrase is the one compared.) An empty or non-string
|
|
1700
|
+
* `table` declares nothing — silently, because this runs on an error path
|
|
1701
|
+
* where a thrown `TypeError` would replace the envelope it was meant to
|
|
1702
|
+
* annotate; the predicate then falls back to `readObject` exactly as if no
|
|
1703
|
+
* driver had spoken.
|
|
1704
|
+
*
|
|
1705
|
+
* @returns `error`, for chaining.
|
|
1706
|
+
*/
|
|
1707
|
+
declare function declareTargetedTable<E extends object>(error: E, table: string): E;
|
|
1708
|
+
/**
|
|
1709
|
+
* The table `error` declares its statement targeted, or `null` when it declares
|
|
1710
|
+
* none — the reading half of {@link declareTargetedTable}. Tolerant of bare
|
|
1711
|
+
* input: any non-object, and any object without a non-empty string under the
|
|
1712
|
+
* key, is "no declaration".
|
|
1713
|
+
*/
|
|
1714
|
+
declare function targetedTableOf(error: unknown): string | null;
|
|
1715
|
+
/**
|
|
1716
|
+
* Is this DDL error the benign "already provisioned" case?
|
|
1717
|
+
*
|
|
1718
|
+
* @param error - The value thrown by `syncSchema()` (or any DDL call).
|
|
1719
|
+
* @param depth - Internal `cause`-chain recursion counter; callers pass nothing.
|
|
1720
|
+
* @returns `true` only when the error positively identifies as
|
|
1721
|
+
* table/column/index-already-exists. Anything else — including an
|
|
1722
|
+
* unrecognised error, `undefined`, or a permission/connection failure —
|
|
1723
|
+
* returns `false` and MUST be reported loudly by the caller.
|
|
1724
|
+
*/
|
|
1725
|
+
declare function isSchemaAlreadyExistsError(error: unknown, depth?: number): boolean;
|
|
1726
|
+
/**
|
|
1727
|
+
* Is this READ error the benign "table has not been provisioned yet" case?
|
|
1728
|
+
*
|
|
1729
|
+
* The only failure that licenses a caller to treat an empty table as the truth
|
|
1730
|
+
* — there are no rows, so there is nothing to be inconsistent with. A
|
|
1731
|
+
* connection drop, a timeout, a permission denial or a query error all mean the
|
|
1732
|
+
* rows may well exist and simply were not seen; those return `false` and the
|
|
1733
|
+
* caller must report the consequence and give up rather than compute an answer
|
|
1734
|
+
* from data it never read (#4825).
|
|
1735
|
+
*
|
|
1736
|
+
* A failure about a **column** of a relation is never this case, in either of
|
|
1737
|
+
* Postgres' two phrasings — the relation is right there in the message because
|
|
1738
|
+
* it exists (#6347). See {@link MISSING_TABLE}'s `excludes`.
|
|
1739
|
+
*
|
|
1740
|
+
* [#13324] Neither is a failure that names a **different relation**, and that
|
|
1741
|
+
* one cannot be seen without `readObject`. The message test asks what the
|
|
1742
|
+
* phrase LOOKS like and never which table it names, so a read of a view whose
|
|
1743
|
+
* base table has been dropped — `no such table: main.<base>`, measured on
|
|
1744
|
+
* libsql for a view that itself exists — answered benign for a relation that is
|
|
1745
|
+
* present and may be backed by rows. Naming the read closes it: the phrase must
|
|
1746
|
+
* be about the table the caller asked for, or it is not evidence about it.
|
|
1747
|
+
*
|
|
1748
|
+
* Pass `readObject` from every in-repo call site. It is **optional** so that
|
|
1749
|
+
* omitting it is exactly the pre-#13324 behaviour rather than a new loud
|
|
1750
|
+
* failure — this is a published export (`@objectstack/types`, and still
|
|
1751
|
+
* `@objectstack/metadata/errors` by re-export), and a required parameter would
|
|
1752
|
+
* be a breaking change to it. The cost of the choice
|
|
1753
|
+
* is that the narrowing is opt-in per call site: a new caller that forgets it
|
|
1754
|
+
* silently gets the old, wider verdict.
|
|
1755
|
+
*
|
|
1756
|
+
* [#13440] That last sentence is no longer only a warning. In-repo callers are
|
|
1757
|
+
* held to it by `driver-error-classification.callers.test.ts`, which walks every
|
|
1758
|
+
* TypeScript source under `packages/` and fails any call of this function that
|
|
1759
|
+
* omits `readObject` or passes it as `undefined`/`null`. The exemption is this
|
|
1760
|
+
* module's own contract tests, which exercise the one-argument PUBLISHED form on
|
|
1761
|
+
* purpose; read that file's header before adding to the exemption, because
|
|
1762
|
+
* widening it is how the enforcement becomes prose again. External consumers are
|
|
1763
|
+
* untouched: the signature below is unchanged, and the gate binds only callers
|
|
1764
|
+
* inside this repository.
|
|
1765
|
+
*
|
|
1766
|
+
* [#13438] `readObject` is the caller's name for what it read, and for a
|
|
1767
|
+
* federated object (ADR-0015) that is not the name the driver put in the
|
|
1768
|
+
* statement — `crm_order` reads `external.remoteName: 'legacy_orders'`, so a
|
|
1769
|
+
* genuinely absent remote raised a phrase naming `legacy_orders` against a
|
|
1770
|
+
* caller naming `crm_order`, and the #13324 comparison read it loud. A driver
|
|
1771
|
+
* that knows the table it targeted now DECLARES it on the envelope
|
|
1772
|
+
* ({@link declareTargetedTable}), and a declared table is preferred over
|
|
1773
|
+
* `readObject` outright: the phrase is compared against the declared name, and
|
|
1774
|
+
* the caller-supplied one is not consulted at that node or below it. Absent a
|
|
1775
|
+
* declaration the comparison is the #13324 one, unchanged. Two consequences,
|
|
1776
|
+
* both pinned: a genuinely absent federated remote reads benign again without
|
|
1777
|
+
* the caller learning the mapping; and — because a declaration is evidence the
|
|
1778
|
+
* caller did not have — an envelope whose phrase names a relation other than
|
|
1779
|
+
* its declared table reads NOT benign even through the one-argument form.
|
|
1780
|
+
*
|
|
1781
|
+
* @param error - The value thrown by a driver/engine read (`find`, `findOne`, …).
|
|
1782
|
+
* @param readObject - The object/table whose emptiness the caller is about to
|
|
1783
|
+
* treat as the truth — its own API name is fine, the comparison folds
|
|
1784
|
+
* away schema qualifiers, the legacy `ns__short` prefix and case.
|
|
1785
|
+
* Omitted (or not a string) means "cannot say", never "be loud".
|
|
1786
|
+
* Superseded, at any node of the `cause` chain that declares the
|
|
1787
|
+
* table its statement targeted, by that declaration (#13438).
|
|
1788
|
+
* @param depth - Internal `cause`-chain recursion counter; callers pass nothing.
|
|
1789
|
+
* @returns `true` only when the error positively identifies as
|
|
1790
|
+
* table/relation-does-not-exist **for the table that was read** —
|
|
1791
|
+
* the declared target where a driver supplied one, else `readObject`.
|
|
1792
|
+
*/
|
|
1793
|
+
declare function isMissingTableError(error: unknown, readObject?: string, depth?: number): boolean;
|
|
1794
|
+
|
|
1157
1795
|
/**
|
|
1158
1796
|
* Whether a thrown driver error says the `ON CONFLICT` target it was given is
|
|
1159
1797
|
* backed by no PRIMARY KEY or UNIQUE index.
|
|
@@ -1407,4 +2045,4 @@ interface RuntimePlugin {
|
|
|
1407
2045
|
onStart?: (ctx: RuntimeContext) => void | Promise<void>;
|
|
1408
2046
|
}
|
|
1409
2047
|
|
|
1410
|
-
export { type EnvelopeResponse, GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION, type GlobalUniqueAttestation, type GlobalUniqueFinding, type IKernel, INTERNAL_ERROR_MESSAGE, type KeysetPageQuery, type KeysetWalk, type KeysetWalkOptions, type RuntimeContext, type RuntimePlugin, type ThrownHttpError, VALIDATION_FAILED_STATUS, type ValidationFailureDetails, _resetEnvDeprecationWarnings, buildGlobalUniqueStopMessage, collectConfiguredLocales, collectGlobalUniques, declaredIndexUniqueIsGlobal, declaredUserMessage, declaresServerFault, demotedDeclaredCode, describeGlobalUniqueFinding, emitDegradedBootBanner, fieldUniqueIsGlobal, fieldsFromZodIssues, globalUniqueFindingId, isMcpServerEnabled, isModuleNotFoundError, isPlatformOwnedObject, isRelationSubObjectPhrase, isUnbackedConflictTargetError, isUniqueViolationError, keysetWalk, looksLikeInternalErrorLeak, matchMissingColumnOfRelation, postureGatesGlobalUniques, readEnvWithDeprecation, recordGlobalUniqueAttestation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, resolveThrownHttpError, sendError, sendOk, stampSearchPinyinEnabled, unconfirmedGlobalUniques, uniqueViolationColumn, validationFailure, validationFailureDetails };
|
|
2048
|
+
export { DRIVER_TARGETED_TABLE, type EnvelopeResponse, GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION, type GlobalUniqueAttestation, type GlobalUniqueFinding, type IKernel, INTERNAL_ERROR_MESSAGE, type KeysetPageQuery, type KeysetWalk, type KeysetWalkOptions, PLATFORM_OWNER_EMAIL_ENV, type RuntimeContext, type RuntimePlugin, SERVER_FAULT_LOG_PREFIX, type ServerFaultLogInput, type ServerFaultProvenance, type ServerFaultRequest, type StrandedDecisionDetails, type ThrownHttpError, VALIDATION_FAILED_STATUS, type ValidationFailureDetails, _resetEnvDeprecationWarnings, buildGlobalUniqueStopMessage, collectConfiguredLocales, collectGlobalUniques, declareTargetedTable, declaredIndexUniqueIsGlobal, declaredUserMessage, declaresServerFault, demotedDeclaredCode, describeFaultRequest, describeGlobalUniqueFinding, emitDegradedBootBanner, fieldUniqueIsGlobal, fieldsFromZodIssues, globalUniqueFindingId, isEmailVerifiedUserRow, isMcpServerEnabled, isMissingTableError, isModuleNotFoundError, isPlatformOwnedObject, isRelationSubObjectPhrase, isSchemaAlreadyExistsError, isServerFault, isUnbackedConflictTargetError, isUniqueViolationError, keysetWalk, logServerFault, looksLikeInternalErrorLeak, matchMissingColumnOfRelation, postureGatesGlobalUniques, readEnvWithDeprecation, recordGlobalUniqueAttestation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveOrgMembershipLimit, resolvePlatformOwnerEmail, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, resolveThrownHttpError, sendError, sendOk, serverFaultLogMessage, serverFaultLogMeta, serverFaultProvenance, stampSearchPinyinEnabled, strandedDecisionDetails, strandedDecisionFailure, targetedTableOf, unconfirmedGlobalUniques, uniqueViolationColumn, validationFailure, validationFailureDetails };
|