@objectstack/types 17.0.0-rc.3 → 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/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
- * Resolve whether the deployment runs in multi-org (a.k.a. multi-tenant) mode.
93
- *
94
- * Single source of truth for the `OS_MULTI_ORG_ENABLED` flag. Resolution: the
95
- * canonical `OS_MULTI_ORG_ENABLED`; else `false`. Any value other than a
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 when multi-org is enabled ({@link resolveMultiOrgEnabled}).
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