@objectstack/types 17.0.0-rc.2 → 17.0.0-rc.4
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 +369 -0
- package/dist/index.d.mts +289 -14
- package/dist/index.d.ts +289 -14
- package/dist/index.js +131 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +117 -1
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +120 -6
- package/dist/node.d.ts +120 -6
- package/dist/node.js +132 -8
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +125 -7
- package/dist/node.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -89,21 +89,33 @@ declare function readEnvWithDeprecation(preferred: string, legacy: string | read
|
|
|
89
89
|
silent?: boolean;
|
|
90
90
|
}): string | undefined;
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
92
|
+
* Read the LEGACY `OS_MULTI_ORG_ENABLED` boolean.
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ **[ADR-0105 D1] DEMOTED — not the knob to gate on.** `OS_TENANCY_POSTURE`
|
|
95
|
+
* superseded this flag and is the authoritative one;
|
|
96
|
+
* {@link resolveTenancyPosture} is where the two are reconciled (posture when
|
|
97
|
+
* set, else this boolean). This function only reports the legacy input, so a
|
|
98
|
+
* deployment that sets ONLY the canonical `OS_TENANCY_POSTURE` reads `false`
|
|
99
|
+
* here while genuinely running a walled multi-organization posture.
|
|
100
|
+
*
|
|
101
|
+
* **Answering "is this deployment multi-org?" with this function is a bug.**
|
|
102
|
+
* Ask the posture instead — `postureEnforcesWall(resolveTenancyPosture())`
|
|
103
|
+
* (`@objectstack/spec/security`) — or, inside a running kernel, the `tenancy`
|
|
104
|
+
* service, which additionally knows whether the requested wall is actually
|
|
105
|
+
* ENFORCED (ADR-0093 D4/D5). Two shipped defects came from gating on this
|
|
106
|
+
* boolean after the demotion: cloud#1020 (the EE licence gate) and #5233
|
|
107
|
+
* (`organization/create` 403'd on a posture-only deployment whose organization
|
|
108
|
+
* wall was fully mounted — the guided "create your workspace" path dead-ended).
|
|
109
|
+
* The sentence this paragraph replaced actively instructed both.
|
|
110
|
+
*
|
|
111
|
+
* Legitimate remaining callers are the ones that specifically mean *the legacy
|
|
112
|
+
* input*: {@link resolveTenancyPosture}'s own back-compat fallback, and
|
|
113
|
+
* back-compat/reporting surfaces that must echo what the operator typed.
|
|
114
|
+
*
|
|
115
|
+
* Resolution: `OS_MULTI_ORG_ENABLED`; else `false`. Any value other than a
|
|
96
116
|
* case-insensitive `'false'` enables it. (The legacy `OS_MULTI_TENANT` alias was
|
|
97
117
|
* removed in 11.0.)
|
|
98
118
|
*
|
|
99
|
-
* Every site that needs to know "is this multi-org?" — the SQL driver's
|
|
100
|
-
* tenant-audit gate, the auth manager's `/auth/config` feature flag and
|
|
101
|
-
* org-create guard, the CLI / dev / runtime org-scoping plugin wiring — MUST
|
|
102
|
-
* call this instead of re-reading the env, so the driver, the security layer,
|
|
103
|
-
* and the UI can never disagree about the mode. Previously each site inlined
|
|
104
|
-
* its own `String(... ?? 'false').toLowerCase() !== 'false'` (and the SQL
|
|
105
|
-
* driver read `process.env` directly, skipping the deprecation warning).
|
|
106
|
-
*
|
|
107
119
|
* Reads `process.env` live on each call; memoise at the call site if the
|
|
108
120
|
* result must be stable for the process lifetime.
|
|
109
121
|
*/
|
|
@@ -237,7 +249,9 @@ declare function resolveMcpStdioAutoStart(): {
|
|
|
237
249
|
* self-created orgs (each of which can auto-provision a free environment on the
|
|
238
250
|
* cloud control plane) without penalising a user invited into many orgs.
|
|
239
251
|
*
|
|
240
|
-
* Only meaningful
|
|
252
|
+
* Only meaningful under a posture that enforces an organization wall, i.e.
|
|
253
|
+
* `postureEnforcesWall({@link resolveTenancyPosture}())` — NOT the demoted
|
|
254
|
+
* `resolveMultiOrgEnabled()` boolean (ADR-0105 D1, #5233).
|
|
241
255
|
* Returns `undefined` when unset or non-positive → no limit (better-auth treats
|
|
242
256
|
* an absent `organizationLimit` as unlimited), preserving self-host behaviour.
|
|
243
257
|
* Deployments that let users self-create orgs SHOULD set a generous cap.
|
|
@@ -364,6 +378,10 @@ declare function _resetEnvDeprecationWarnings(): void;
|
|
|
364
378
|
* positive costs a caller nothing but detail on a response that was a server
|
|
365
379
|
* fault anyway — while the full text still reaches server logs and the
|
|
366
380
|
* error reporter.
|
|
381
|
+
*
|
|
382
|
+
* [#5811] {@link declaresServerFault} joins it here for the same reason and
|
|
383
|
+
* answers the other half of the question: the heuristic asks whether a message
|
|
384
|
+
* *sounds* internal, the declaration asks whether the producer *said so*.
|
|
367
385
|
*/
|
|
368
386
|
/** Generic replacement text for a message that trips {@link looksLikeInternalErrorLeak}. */
|
|
369
387
|
declare const INTERNAL_ERROR_MESSAGE = "Internal server error";
|
|
@@ -381,6 +399,53 @@ declare const INTERNAL_ERROR_MESSAGE = "Internal server error";
|
|
|
381
399
|
* *mention* "update" without being one.
|
|
382
400
|
*/
|
|
383
401
|
declare function looksLikeInternalErrorLeak(message: string | undefined | null): boolean;
|
|
402
|
+
/**
|
|
403
|
+
* Whether the thrown error **declares a server fault** in the ADR-0112 envelope:
|
|
404
|
+
* `status >= 500` *and* a non-empty `code`.
|
|
405
|
+
*
|
|
406
|
+
* The counterpart to {@link looksLikeInternalErrorLeak}, and deliberately not a
|
|
407
|
+
* message test at all. Some server faults are dangerous to echo while saying
|
|
408
|
+
* nothing a phrasing heuristic can recognise — the motivating family is
|
|
409
|
+
* `service-analytics`' `read-scope-sql.ts`, whose ten fail-closed RLS lowering
|
|
410
|
+
* refusals name the FIELD NAMES AND COMPARANDS OF THE RLS POLICY:
|
|
411
|
+
*
|
|
412
|
+
* ```
|
|
413
|
+
* [read-scope-sql] unsafe field identifier "secret_policy_field" — refusing to
|
|
414
|
+
* build read scope (fail-closed).
|
|
415
|
+
* ```
|
|
416
|
+
*
|
|
417
|
+
* That text comes from an administrator's sharing rule compiled by the security
|
|
418
|
+
* service; the tenant who receives it never wrote it and must not be able to read
|
|
419
|
+
* it out of an error body. Measured, all eleven of its message shapes return
|
|
420
|
+
* FALSE from `looksLikeInternalErrorLeak` — they look nothing like a driver dump —
|
|
421
|
+
* so a boundary that only ran the heuristic echoed every one of them verbatim
|
|
422
|
+
* (#5811 measured 11/11 through `errorResponseBase`). Teaching the heuristic to
|
|
423
|
+
* recognise `[read-scope-sql]` would have been *more* message sniffing, which is
|
|
424
|
+
* the mechanism #5352/#5367 exist to remove. So the withhold keys on the
|
|
425
|
+
* DECLARATION instead: a producer that says `status >= 500` with a `code` has
|
|
426
|
+
* declared that this is the server's fault, and a server fault's detail belongs in
|
|
427
|
+
* the operator's log, not in the caller's body.
|
|
428
|
+
*
|
|
429
|
+
* **Both halves are required, and it is deliberately NOT "any 5xx".** #5667 kept
|
|
430
|
+
* UNDECLARED 5xx errors legible on purpose — a bare `Error` from our own code
|
|
431
|
+
* ("no strategy can handle query …") is the operator's own bug report, carries
|
|
432
|
+
* nothing tenant-sensitive, and still falls to `looksLikeInternalErrorLeak`.
|
|
433
|
+
* Widening this to every 500 would delete that decision.
|
|
434
|
+
*
|
|
435
|
+
* **Reads `status`, not `statusCode`.** `status` is the channel ADR-0112 declares;
|
|
436
|
+
* `statusCode` is an alternate spelling some boundaries tolerate when *deriving*
|
|
437
|
+
* an HTTP status. Accepting it here would make the disclosure rule depend on which
|
|
438
|
+
* spelling a producer happened to use — consumer-side leniency of exactly the kind
|
|
439
|
+
* Prime Directive #12 removes. A producer that wants its detail withheld declares
|
|
440
|
+
* the envelope.
|
|
441
|
+
*
|
|
442
|
+
* Costs no diagnostics: every boundary that applies this still logs the untouched
|
|
443
|
+
* error and hands it to the error reporter.
|
|
444
|
+
*
|
|
445
|
+
* @param err - the thrown value, of any shape (a non-object is simply not a
|
|
446
|
+
* declaration).
|
|
447
|
+
*/
|
|
448
|
+
declare function declaresServerFault(err: unknown): boolean;
|
|
384
449
|
|
|
385
450
|
/**
|
|
386
451
|
* Seek-based (keyset) pagination for the batch walks that read a whole object.
|
|
@@ -615,6 +680,216 @@ declare function sendOk(res: EnvelopeResponse, data: unknown, status?: number):
|
|
|
615
680
|
*/
|
|
616
681
|
declare function sendError(res: EnvelopeResponse, status: number, code: ErrorCode, message: string, extra?: Pick<ApiError, 'category' | 'httpStatus' | 'details' | 'requestId'>): void;
|
|
617
682
|
|
|
683
|
+
/**
|
|
684
|
+
* [ADR-0120 D5e] The `isolated`-posture install gate for `'global'` uniqueness.
|
|
685
|
+
*
|
|
686
|
+
* ## Why a gate exists at all
|
|
687
|
+
*
|
|
688
|
+
* ADR-0120's scope vocabulary is deliberately **posture-invariant**: the author
|
|
689
|
+
* states a business boundary (`'organization'` = one holder per organization,
|
|
690
|
+
* `'global'` = one holder across the whole installation) and the same app
|
|
691
|
+
* package runs unmodified under every tenancy posture (ADR-0105 D1
|
|
692
|
+
* `single | group | isolated`). No index shape reads the posture — a posture
|
|
693
|
+
* flip has zero automatic schema consequences, which is exactly what makes one
|
|
694
|
+
* app package serve all three.
|
|
695
|
+
*
|
|
696
|
+
* One residual survives that invariance, and only in one direction
|
|
697
|
+
* (ADR-0120 §Posture portability, Resolved question #4):
|
|
698
|
+
*
|
|
699
|
+
* - Under `single` / `group`, `'global'` means "the installation" — which for a
|
|
700
|
+
* `group` deployment IS the customer company (集团). An app business rule
|
|
701
|
+
* spelled `'global'` is correct there.
|
|
702
|
+
* - Under `isolated`, organizations are **separate customers**. The identical
|
|
703
|
+
* declaration now crosses customers: it over-constrains (customer B cannot
|
|
704
|
+
* reuse customer A's material code) and it becomes a cross-tenant existence
|
|
705
|
+
* oracle — the very leak #3696 closed for field-level uniques (S10).
|
|
706
|
+
*
|
|
707
|
+
* `'global'` is therefore physically posture-invariant but not *safety*-invariant,
|
|
708
|
+
* and the ADR's S14 row records the honest cost: "unique across the whole
|
|
709
|
+
* company" is not expressible in metadata alone, because it means the
|
|
710
|
+
* installation under `group` and one organization under `isolated`. A third,
|
|
711
|
+
* posture-resolved word (`'company'`) was designed and **rejected** — it is the
|
|
712
|
+
* one token that cannot be used without first understanding the posture
|
|
713
|
+
* spectrum, exactly the cognitive load an AI-authored vocabulary must not carry.
|
|
714
|
+
* The scenario is handled **here**, at the deployment seam, instead.
|
|
715
|
+
*
|
|
716
|
+
* ## Why a HARD stop and not an advisory
|
|
717
|
+
*
|
|
718
|
+
* Maintainer decision, 2026-08-04 (ADR-0120 Resolved #4). An advisory that
|
|
719
|
+
* nobody reads leaves a cross-customer constraint enforced in production — the
|
|
720
|
+
* ADR-0049/0078 class this whole ADR exists to close. So installing an app that
|
|
721
|
+
* carries `'global'` uniques on non-`sys` objects into an `isolated` environment
|
|
722
|
+
* **stops**, lists each index, and asks the installer (typically an AI agent) to
|
|
723
|
+
* either confirm it as genuinely platform-wide or rewrite it to
|
|
724
|
+
* `'organization'`. The confirmation is recorded in the install manifest
|
|
725
|
+
* (ADR-0104 attestation style) so it is **never re-asked**.
|
|
726
|
+
*
|
|
727
|
+
* ⛔ **Never a boot-time warning** (#4884 discipline). A deployment whose apps
|
|
728
|
+
* were installed before this gate existed, or whose posture changed after
|
|
729
|
+
* install, is reached by the ADVISORY form in `os doctor` / `os migrate plan` —
|
|
730
|
+
* the two cases a gate at the install seam structurally cannot see. Turning
|
|
731
|
+
* this into a startup diagnostic would fire on every boot of every deployment
|
|
732
|
+
* forever, which is the false-alarm class #4884 retired.
|
|
733
|
+
*
|
|
734
|
+
* ## What counts as a finding
|
|
735
|
+
*
|
|
736
|
+
* | Declaration | Finding? | Why |
|
|
737
|
+
* |:---|:---|:---|
|
|
738
|
+
* | field `unique: 'global'` | ✅ | one holder across the installation — crosses customers under `isolated` |
|
|
739
|
+
* | declared index `unique: 'global'` | ✅ | same boundary, spelled on the index |
|
|
740
|
+
* | declared index `unique: true` | ✅ | ADR-0120 D1: bare `true` **is** the deprecated positional spelling of `'global'`; identical physical shape, identical hazard. Excluding it would leave the gate bypassable by spelling for the whole of 17.x |
|
|
741
|
+
* | field `unique: true` / `'organization'` | ❌ | per-organization — correct under every posture |
|
|
742
|
+
* | declared index `unique: 'organization'` | ❌ | per-organization (D3 NULL-safe key part) |
|
|
743
|
+
* | anything on a `sys_*` object | ❌ | engine idempotency / dedup keys (the ADR's S5 inventory) are platform-wide **by construction**; asking about them on every install is the false-alarm class again |
|
|
744
|
+
*
|
|
745
|
+
* The enumeration is a pure projection of declared metadata — no tenancy
|
|
746
|
+
* inference, no database access — which is what lets the identical function
|
|
747
|
+
* serve the hard gate, `os doctor` and `os migrate plan`.
|
|
748
|
+
*/
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Is this object platform-owned (the ADR's "`sys` objects")?
|
|
752
|
+
*
|
|
753
|
+
* The ADR scopes the gate to **non-`sys`** objects because the platform's own
|
|
754
|
+
* `'global'` uniques are the S5 inventory — `sys_job.name`,
|
|
755
|
+
* `sys_notification.dedup_key`, `http_delivery (source, dedup_key)` and the rest
|
|
756
|
+
* — engine idempotency keys that are platform-wide on purpose and identical
|
|
757
|
+
* under every posture. Re-confirming them on every app install would be the
|
|
758
|
+
* #4884 false-alarm class with extra steps.
|
|
759
|
+
*
|
|
760
|
+
* `base_` is included alongside `sys_`: it is the platform's other reserved
|
|
761
|
+
* object prefix, carrying the same "owned by the framework, not the app"
|
|
762
|
+
* meaning. An app object can never legitimately claim either.
|
|
763
|
+
*/
|
|
764
|
+
declare function isPlatformOwnedObject(objectName: unknown): boolean;
|
|
765
|
+
/**
|
|
766
|
+
* Does a FIELD-level `unique` value ask for the installation-wide boundary?
|
|
767
|
+
*
|
|
768
|
+
* Only the explicit `'global'` does. Bare `true` at field level is the
|
|
769
|
+
* documented, unambiguous synonym of `'organization'` (ADR-0120 D1 —
|
|
770
|
+
* "field-level bare `true` stays valid indefinitely", Resolved #2), so it is
|
|
771
|
+
* never a finding.
|
|
772
|
+
*/
|
|
773
|
+
declare function fieldUniqueIsGlobal(unique: unknown): boolean;
|
|
774
|
+
/**
|
|
775
|
+
* Does a DECLARED-INDEX `unique` value ask for the installation-wide boundary?
|
|
776
|
+
*
|
|
777
|
+
* `'global'` and bare `true` both do. Per ADR-0120 D1 the bare spelling **is**
|
|
778
|
+
* `'global'` — "today's verbatim semantics, materialized over exactly the listed
|
|
779
|
+
* columns" — deprecated (lint `unique/unscoped-declared-index` warns in 17.x,
|
|
780
|
+
* protocol 18 rejects it, #5082) but physically identical while it lasts. A gate
|
|
781
|
+
* that judged only the explicit word would be bypassable by writing the
|
|
782
|
+
* deprecated one, which is the #4986 trap wearing the gate's own uniform.
|
|
783
|
+
*/
|
|
784
|
+
declare function declaredIndexUniqueIsGlobal(unique: unknown): boolean;
|
|
785
|
+
/** One installation-wide unique declaration found on an app (non-`sys`) object. */
|
|
786
|
+
interface GlobalUniqueFinding {
|
|
787
|
+
/** Stable identity for the attestation record — see {@link globalUniqueFindingId}. */
|
|
788
|
+
readonly id: string;
|
|
789
|
+
/** Object (and therefore table) the declaration sits on. */
|
|
790
|
+
readonly object: string;
|
|
791
|
+
/** Which spelling carried it. */
|
|
792
|
+
readonly kind: 'field' | 'index';
|
|
793
|
+
/** Field name for `kind: 'field'`; the index's declared name (when it has one) otherwise. */
|
|
794
|
+
readonly name?: string;
|
|
795
|
+
/** The columns the constraint spans, in declaration order. */
|
|
796
|
+
readonly columns: readonly string[];
|
|
797
|
+
/** The exact authored value (`true` | `'global'`) — quoted back in the stop message. */
|
|
798
|
+
readonly spelling: true | 'global';
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Stable id for one finding, used as the attestation key.
|
|
802
|
+
*
|
|
803
|
+
* Keyed by object + kind + **columns**, deliberately NOT by the index's optional
|
|
804
|
+
* `name`: a declared index may be anonymous, and renaming an index does not
|
|
805
|
+
* change which constraint the installer confirmed. Two indexes on the same
|
|
806
|
+
* object spanning the same columns are the same constraint by any physical
|
|
807
|
+
* reading, so collapsing them is correct rather than lossy.
|
|
808
|
+
*/
|
|
809
|
+
declare function globalUniqueFindingId(objectName: string, kind: 'field' | 'index', columns: readonly string[]): string;
|
|
810
|
+
/**
|
|
811
|
+
* Enumerate every installation-wide unique declared on an app's non-`sys`
|
|
812
|
+
* objects (ADR-0120 D5e).
|
|
813
|
+
*
|
|
814
|
+
* Pure and posture-agnostic on purpose: the CALLER decides whether the posture
|
|
815
|
+
* makes these findings a hard stop (`isolated`, at install) or an advisory
|
|
816
|
+
* (`os doctor` / `os migrate plan`). Deterministic order — objects as supplied,
|
|
817
|
+
* fields before indexes within an object — so the stop message and the
|
|
818
|
+
* attestation record are reproducible across runs.
|
|
819
|
+
*/
|
|
820
|
+
declare function collectGlobalUniques(objects: unknown): GlobalUniqueFinding[];
|
|
821
|
+
/**
|
|
822
|
+
* The attestation recorded in the install manifest once an installer has
|
|
823
|
+
* confirmed a set of findings as genuinely platform-wide (ADR-0104 style).
|
|
824
|
+
*
|
|
825
|
+
* Shape follows the ADR-0104 precedent rather than inventing one: the FACT
|
|
826
|
+
* observed (which constraint ids a human/agent affirmed), WHO affirmed it, WHEN,
|
|
827
|
+
* and under WHICH posture the question was asked. That last field is what keeps
|
|
828
|
+
* the record honest — an attestation given under `isolated` is evidence about
|
|
829
|
+
* `isolated`, and nothing else.
|
|
830
|
+
*
|
|
831
|
+
* Never rewritten in place: confirmations ACCUMULATE. A later install of a newer
|
|
832
|
+
* version that adds a new `'global'` index asks about the new one only — the
|
|
833
|
+
* earlier answers stand, which is the "之后不复问" half of the decision.
|
|
834
|
+
*/
|
|
835
|
+
interface GlobalUniqueAttestation {
|
|
836
|
+
/** Posture the confirmation was given under. */
|
|
837
|
+
readonly posture: TenancyPosture;
|
|
838
|
+
/** Finding ids affirmed as genuinely platform-wide. */
|
|
839
|
+
readonly confirmed: readonly string[];
|
|
840
|
+
/** ISO timestamp of the most recent confirmation. */
|
|
841
|
+
readonly attestedAt: string;
|
|
842
|
+
/** Identity of the confirming installer, when the seam knows one. */
|
|
843
|
+
readonly attestedBy?: string | null;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Which findings still need an answer, given an existing attestation.
|
|
847
|
+
*
|
|
848
|
+
* Returns the findings NOT covered by `attestation.confirmed`. An empty result
|
|
849
|
+
* means the install proceeds silently — this is the mechanism behind "never
|
|
850
|
+
* re-asked".
|
|
851
|
+
*
|
|
852
|
+
* An attestation recorded under a DIFFERENT posture does not carry over: the
|
|
853
|
+
* question "is this genuinely platform-wide, knowing organizations here are
|
|
854
|
+
* separate customers?" was never asked. Confirmations made under `isolated` are
|
|
855
|
+
* the only ones that answer it, so a `single`-posture record is treated as
|
|
856
|
+
* absent rather than as consent — the conservative direction, and the only one
|
|
857
|
+
* that cannot silently admit a cross-customer constraint.
|
|
858
|
+
*/
|
|
859
|
+
declare function unconfirmedGlobalUniques(findings: readonly GlobalUniqueFinding[], attestation: GlobalUniqueAttestation | undefined | null, posture: TenancyPosture): GlobalUniqueFinding[];
|
|
860
|
+
/**
|
|
861
|
+
* Merge a new set of confirmations into an existing attestation.
|
|
862
|
+
*
|
|
863
|
+
* Additive by construction — see {@link GlobalUniqueAttestation}. A record from
|
|
864
|
+
* another posture is replaced rather than merged: its `confirmed` ids answered a
|
|
865
|
+
* different question.
|
|
866
|
+
*/
|
|
867
|
+
declare function recordGlobalUniqueAttestation(previous: GlobalUniqueAttestation | undefined | null, confirmedIds: readonly string[], posture: TenancyPosture, attestedBy?: string | null, now?: string): GlobalUniqueAttestation;
|
|
868
|
+
/** Render one finding the way both the hard stop and the advisory quote it. */
|
|
869
|
+
declare function describeGlobalUniqueFinding(finding: GlobalUniqueFinding): string;
|
|
870
|
+
/**
|
|
871
|
+
* The prescription every surface repeats verbatim, so the hard stop and the two
|
|
872
|
+
* advisories cannot drift into three different pieces of advice.
|
|
873
|
+
*/
|
|
874
|
+
declare const GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION: string;
|
|
875
|
+
/**
|
|
876
|
+
* The full hard-stop message for an install into an `isolated` environment.
|
|
877
|
+
*
|
|
878
|
+
* Built here rather than at the install seam so the CLI, the HTTP surface and
|
|
879
|
+
* the tests all quote one text.
|
|
880
|
+
*/
|
|
881
|
+
declare function buildGlobalUniqueStopMessage(appLabel: string, findings: readonly GlobalUniqueFinding[]): string;
|
|
882
|
+
/** Error code the install seam returns when the gate stops an install. */
|
|
883
|
+
declare const GLOBAL_UNIQUE_CONFIRMATION_REQUIRED = "UNIQUE_SCOPE_CONFIRMATION_REQUIRED";
|
|
884
|
+
/**
|
|
885
|
+
* Does this posture make `'global'` uniques a decision point at all?
|
|
886
|
+
*
|
|
887
|
+
* `isolated` only. Under `single` there is one customer; under `group` the
|
|
888
|
+
* installation IS the customer company, which is what `'global'` means there —
|
|
889
|
+
* both are the benign direction the ADR leaves to the app's install notes.
|
|
890
|
+
*/
|
|
891
|
+
declare function postureGatesGlobalUniques(posture: unknown): boolean;
|
|
892
|
+
|
|
618
893
|
interface IKernel {
|
|
619
894
|
ql?: any;
|
|
620
895
|
start(): Promise<void>;
|
|
@@ -629,4 +904,4 @@ interface RuntimePlugin {
|
|
|
629
904
|
onStart?: (ctx: RuntimeContext) => void | Promise<void>;
|
|
630
905
|
}
|
|
631
906
|
|
|
632
|
-
export { type EnvelopeResponse, type IKernel, INTERNAL_ERROR_MESSAGE, type KeysetPageQuery, type KeysetWalk, type KeysetWalkOptions, type RuntimeContext, type RuntimePlugin, _resetEnvDeprecationWarnings, collectConfiguredLocales, emitDegradedBootBanner, isMcpServerEnabled, isModuleNotFoundError, keysetWalk, looksLikeInternalErrorLeak, readEnvWithDeprecation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, sendError, sendOk, stampSearchPinyinEnabled };
|
|
907
|
+
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, _resetEnvDeprecationWarnings, buildGlobalUniqueStopMessage, collectConfiguredLocales, collectGlobalUniques, declaredIndexUniqueIsGlobal, declaresServerFault, describeGlobalUniqueFinding, emitDegradedBootBanner, fieldUniqueIsGlobal, globalUniqueFindingId, isMcpServerEnabled, isModuleNotFoundError, isPlatformOwnedObject, keysetWalk, looksLikeInternalErrorLeak, postureGatesGlobalUniques, readEnvWithDeprecation, recordGlobalUniqueAttestation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, sendError, sendOk, stampSearchPinyinEnabled, unconfirmedGlobalUniques };
|
package/dist/index.js
CHANGED
|
@@ -20,15 +20,27 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
GLOBAL_UNIQUE_CONFIRMATION_REQUIRED: () => GLOBAL_UNIQUE_CONFIRMATION_REQUIRED,
|
|
24
|
+
GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION: () => GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION,
|
|
23
25
|
INTERNAL_ERROR_MESSAGE: () => INTERNAL_ERROR_MESSAGE,
|
|
24
26
|
_resetEnvDeprecationWarnings: () => _resetEnvDeprecationWarnings,
|
|
27
|
+
buildGlobalUniqueStopMessage: () => buildGlobalUniqueStopMessage,
|
|
25
28
|
collectConfiguredLocales: () => collectConfiguredLocales,
|
|
29
|
+
collectGlobalUniques: () => collectGlobalUniques,
|
|
30
|
+
declaredIndexUniqueIsGlobal: () => declaredIndexUniqueIsGlobal,
|
|
31
|
+
declaresServerFault: () => declaresServerFault,
|
|
32
|
+
describeGlobalUniqueFinding: () => describeGlobalUniqueFinding,
|
|
26
33
|
emitDegradedBootBanner: () => emitDegradedBootBanner,
|
|
34
|
+
fieldUniqueIsGlobal: () => fieldUniqueIsGlobal,
|
|
35
|
+
globalUniqueFindingId: () => globalUniqueFindingId,
|
|
27
36
|
isMcpServerEnabled: () => isMcpServerEnabled,
|
|
28
37
|
isModuleNotFoundError: () => isModuleNotFoundError,
|
|
38
|
+
isPlatformOwnedObject: () => isPlatformOwnedObject,
|
|
29
39
|
keysetWalk: () => keysetWalk,
|
|
30
40
|
looksLikeInternalErrorLeak: () => looksLikeInternalErrorLeak,
|
|
41
|
+
postureGatesGlobalUniques: () => postureGatesGlobalUniques,
|
|
31
42
|
readEnvWithDeprecation: () => readEnvWithDeprecation,
|
|
43
|
+
recordGlobalUniqueAttestation: () => recordGlobalUniqueAttestation,
|
|
32
44
|
resolveAllowDegradedTenancy: () => resolveAllowDegradedTenancy,
|
|
33
45
|
resolveAllowDevPlugin: () => resolveAllowDevPlugin,
|
|
34
46
|
resolveAllowDriverConnectFailure: () => resolveAllowDriverConnectFailure,
|
|
@@ -40,7 +52,8 @@ __export(index_exports, {
|
|
|
40
52
|
resolveTenancyPosture: () => resolveTenancyPosture,
|
|
41
53
|
sendError: () => sendError,
|
|
42
54
|
sendOk: () => sendOk,
|
|
43
|
-
stampSearchPinyinEnabled: () => stampSearchPinyinEnabled
|
|
55
|
+
stampSearchPinyinEnabled: () => stampSearchPinyinEnabled,
|
|
56
|
+
unconfirmedGlobalUniques: () => unconfirmedGlobalUniques
|
|
44
57
|
});
|
|
45
58
|
module.exports = __toCommonJS(index_exports);
|
|
46
59
|
|
|
@@ -184,6 +197,11 @@ function looksLikeInternalErrorLeak(message) {
|
|
|
184
197
|
const lower = String(message).toLowerCase();
|
|
185
198
|
return lower.includes("sqlite_") || lower.includes("sqlstate") || lower.startsWith("insert into ") || lower.startsWith("update ") || lower.startsWith("select ") || lower.startsWith("delete from ") || lower.includes("constraint failed") || lower.includes("unique constraint") || lower.includes("foreign key");
|
|
186
199
|
}
|
|
200
|
+
function declaresServerFault(err) {
|
|
201
|
+
if (typeof err !== "object" || err === null) return false;
|
|
202
|
+
const { status, code } = err;
|
|
203
|
+
return typeof status === "number" && status >= 500 && typeof code === "string" && code.length > 0;
|
|
204
|
+
}
|
|
187
205
|
|
|
188
206
|
// src/keyset-walk.ts
|
|
189
207
|
function withCursor(where, key, cursor) {
|
|
@@ -261,17 +279,127 @@ function sendOk(res, data, status = 200) {
|
|
|
261
279
|
function sendError(res, status, code, message, extra) {
|
|
262
280
|
res.status(status).json({ success: false, error: { code, message, ...extra } });
|
|
263
281
|
}
|
|
282
|
+
|
|
283
|
+
// src/unique-scope-install-gate.ts
|
|
284
|
+
var import_security2 = require("@objectstack/spec/security");
|
|
285
|
+
var SYS_OBJECT_PREFIXES = ["sys_", "base_"];
|
|
286
|
+
function isPlatformOwnedObject(objectName) {
|
|
287
|
+
const name = typeof objectName === "string" ? objectName.trim().toLowerCase() : "";
|
|
288
|
+
if (!name) return false;
|
|
289
|
+
return SYS_OBJECT_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
290
|
+
}
|
|
291
|
+
function fieldUniqueIsGlobal(unique) {
|
|
292
|
+
return unique === "global";
|
|
293
|
+
}
|
|
294
|
+
function declaredIndexUniqueIsGlobal(unique) {
|
|
295
|
+
return unique === "global" || unique === true;
|
|
296
|
+
}
|
|
297
|
+
function globalUniqueFindingId(objectName, kind, columns) {
|
|
298
|
+
return `${objectName}:${kind}:${columns.join("+")}`;
|
|
299
|
+
}
|
|
300
|
+
function fieldEntriesOf(fields) {
|
|
301
|
+
if (!fields) return [];
|
|
302
|
+
if (Array.isArray(fields)) {
|
|
303
|
+
return fields.filter((f) => f && f.name != null).map((f) => ({ name: String(f.name), def: f }));
|
|
304
|
+
}
|
|
305
|
+
if (typeof fields !== "object") return [];
|
|
306
|
+
return Object.entries(fields).map(([name, def]) => ({ name, def }));
|
|
307
|
+
}
|
|
308
|
+
function collectGlobalUniques(objects) {
|
|
309
|
+
if (!Array.isArray(objects)) return [];
|
|
310
|
+
const findings = [];
|
|
311
|
+
for (const obj of objects) {
|
|
312
|
+
const objectName = typeof obj?.name === "string" ? obj.name.trim() : "";
|
|
313
|
+
if (!objectName) continue;
|
|
314
|
+
if (isPlatformOwnedObject(objectName)) continue;
|
|
315
|
+
for (const { name, def } of fieldEntriesOf(obj?.fields)) {
|
|
316
|
+
if (!fieldUniqueIsGlobal(def?.unique)) continue;
|
|
317
|
+
findings.push({
|
|
318
|
+
id: globalUniqueFindingId(objectName, "field", [name]),
|
|
319
|
+
object: objectName,
|
|
320
|
+
kind: "field",
|
|
321
|
+
name,
|
|
322
|
+
columns: [name],
|
|
323
|
+
spelling: "global"
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const declaredIndexes = Array.isArray(obj?.indexes) ? obj.indexes : [];
|
|
327
|
+
for (const idx of declaredIndexes) {
|
|
328
|
+
if (!declaredIndexUniqueIsGlobal(idx?.unique)) continue;
|
|
329
|
+
const columns = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").map((f) => f) : [];
|
|
330
|
+
if (columns.length === 0) continue;
|
|
331
|
+
const indexName = typeof idx?.name === "string" && idx.name.trim() ? idx.name.trim() : void 0;
|
|
332
|
+
findings.push({
|
|
333
|
+
id: globalUniqueFindingId(objectName, "index", columns),
|
|
334
|
+
object: objectName,
|
|
335
|
+
kind: "index",
|
|
336
|
+
...indexName ? { name: indexName } : {},
|
|
337
|
+
columns,
|
|
338
|
+
spelling: idx.unique === true ? true : "global"
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return findings;
|
|
343
|
+
}
|
|
344
|
+
function unconfirmedGlobalUniques(findings, attestation, posture) {
|
|
345
|
+
if (!attestation || attestation.posture !== posture) return [...findings];
|
|
346
|
+
const confirmed = new Set(attestation.confirmed ?? []);
|
|
347
|
+
return findings.filter((f) => !confirmed.has(f.id));
|
|
348
|
+
}
|
|
349
|
+
function recordGlobalUniqueAttestation(previous, confirmedIds, posture, attestedBy, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
350
|
+
const carried = previous && previous.posture === posture ? previous.confirmed ?? [] : [];
|
|
351
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...carried, ...confirmedIds])).sort();
|
|
352
|
+
return {
|
|
353
|
+
posture,
|
|
354
|
+
confirmed: merged,
|
|
355
|
+
attestedAt: now,
|
|
356
|
+
...attestedBy !== void 0 ? { attestedBy } : {}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function describeGlobalUniqueFinding(finding) {
|
|
360
|
+
const spelling = finding.spelling === true ? "`unique: true`" : "`unique: 'global'`";
|
|
361
|
+
const deprecated = finding.spelling === true ? " [deprecated bare spelling of 'global']" : "";
|
|
362
|
+
if (finding.kind === "field") {
|
|
363
|
+
return `${finding.object}.${finding.name} \u2014 field-level ${spelling}`;
|
|
364
|
+
}
|
|
365
|
+
const label = finding.name ? ` '${finding.name}'` : "";
|
|
366
|
+
return `${finding.object} \u2014 declared index${label} [${finding.columns.join(", ")}] ${spelling}${deprecated}`;
|
|
367
|
+
}
|
|
368
|
+
var GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION = "Under the 'isolated' posture organizations are separate CUSTOMERS, so an installation-wide unique constrains across customers and can reveal that another customer already holds a value (ADR-0120 S10/S14). For each index above, either (a) confirm it is genuinely platform-wide \u2014 an infrastructure/dedup key, a DNS hostname, an external provider id \u2014 or (b) rewrite it to `unique: 'organization'` so it is one holder per organization. See ADR-0120 \xA7Posture portability.";
|
|
369
|
+
function buildGlobalUniqueStopMessage(appLabel, findings) {
|
|
370
|
+
const lines = findings.map((f) => ` \u2022 ${describeGlobalUniqueFinding(f)}`);
|
|
371
|
+
return `'${appLabel}' declares ${findings.length} installation-wide unique constraint(s) on its own objects, and this environment runs the 'isolated' tenancy posture (ADR-0120 D5e):
|
|
372
|
+
${lines.join("\n")}
|
|
373
|
+
${GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION}
|
|
374
|
+
Re-run the install with the confirmation to record it in the install manifest \u2014 it is asked once, never again for the same constraints.`;
|
|
375
|
+
}
|
|
376
|
+
var GLOBAL_UNIQUE_CONFIRMATION_REQUIRED = "UNIQUE_SCOPE_CONFIRMATION_REQUIRED";
|
|
377
|
+
function postureGatesGlobalUniques(posture) {
|
|
378
|
+
return (0, import_security2.normalizeTenancyPosture)(posture) === "isolated";
|
|
379
|
+
}
|
|
264
380
|
// Annotate the CommonJS export names for ESM import in node:
|
|
265
381
|
0 && (module.exports = {
|
|
382
|
+
GLOBAL_UNIQUE_CONFIRMATION_REQUIRED,
|
|
383
|
+
GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION,
|
|
266
384
|
INTERNAL_ERROR_MESSAGE,
|
|
267
385
|
_resetEnvDeprecationWarnings,
|
|
386
|
+
buildGlobalUniqueStopMessage,
|
|
268
387
|
collectConfiguredLocales,
|
|
388
|
+
collectGlobalUniques,
|
|
389
|
+
declaredIndexUniqueIsGlobal,
|
|
390
|
+
declaresServerFault,
|
|
391
|
+
describeGlobalUniqueFinding,
|
|
269
392
|
emitDegradedBootBanner,
|
|
393
|
+
fieldUniqueIsGlobal,
|
|
394
|
+
globalUniqueFindingId,
|
|
270
395
|
isMcpServerEnabled,
|
|
271
396
|
isModuleNotFoundError,
|
|
397
|
+
isPlatformOwnedObject,
|
|
272
398
|
keysetWalk,
|
|
273
399
|
looksLikeInternalErrorLeak,
|
|
400
|
+
postureGatesGlobalUniques,
|
|
274
401
|
readEnvWithDeprecation,
|
|
402
|
+
recordGlobalUniqueAttestation,
|
|
275
403
|
resolveAllowDegradedTenancy,
|
|
276
404
|
resolveAllowDevPlugin,
|
|
277
405
|
resolveAllowDriverConnectFailure,
|
|
@@ -283,6 +411,7 @@ function sendError(res, status, code, message, extra) {
|
|
|
283
411
|
resolveTenancyPosture,
|
|
284
412
|
sendError,
|
|
285
413
|
sendOk,
|
|
286
|
-
stampSearchPinyinEnabled
|
|
414
|
+
stampSearchPinyinEnabled,
|
|
415
|
+
unconfirmedGlobalUniques
|
|
287
416
|
});
|
|
288
417
|
//# sourceMappingURL=index.js.map
|