You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 3da3da5
Browse filesBrowse the repository at this point in the historyBrowse files
feat(metadata-protocol)!: cross-tenant uninstall must be declared — deletePackage refuses a call naming neither an organization nor allTenants (#7780) (#7995)
* feat(metadata-protocol)!: cross-tenant uninstall must be declared explicitly (#7780)
`protocol.deletePackage` selected rows with `{ package_id }` and added an
organization predicate only when the caller supplied one, so a call with no
`organizationId` matched EVERY organization's rows — measured during #7705 at
5 of 5 deleted, including a foreign org's.
The two doors of `DELETE /api/v1/packages/:id` disagreed about which semantic
they invoked: the direct-mount REST registrar passes no org (cross-tenant), the
dispatcher twin resolves one (org-scoped). Neither chose it; it fell out of a
missing argument. `resolveActiveOrganizationId` (#4127) is entirely
catch-wrapped, so an accidental org-less call and a deliberate env-wide one are
byte-identical at the call site.
Maintainer ruling (2026-08-12):
跨租户卸载必须显式声明,缺省缺参永远不等于「全部租户」.
`deletePackage` gains `allTenants?: boolean`, the explicit carrier. The tenant
scope must be readable off the request, and there are two ways to violate that,
both answering `TENANT_SCOPE_REQUIRED` (400) and deleting nothing:
organizationId | allTenants | result
---------------|----------------|--------------------------------
set | absent / false | org-scoped delete (unchanged)
absent | true | cross-tenant delete
absent | absent / false | 400
set | true | 400
An explicit `allTenants: false` is treated as undeclared — it is not an
affirmative request for cross-tenant semantics, so it cannot authorise them.
Both-supplied is refused rather than resolved: the two are contradictory, and
resolving narrow-first makes the flag silently inert while resolving
explicit-first ignores a named org and deletes every tenant's rows — the
original defect wearing a flag. Refusing is also the only reading that stays
correct when a request is composed from two places, which is exactly what the
catch-wrapped resolver makes real.
The REST door declares `allTenants: true`: `packages/rest` has no org to
resolve, so of the two remedies the ruling allows only declaring is available.
The no-org branch is still NOT narrowed to `organization_id IS NULL` — #7705
proved that orphans every org-scoped row. The remedy is explicitness.
#7705 case 4 pinned the old no-org-clears-everything behaviour deliberately. It
is rewritten, not deleted, into the halves of the ruled contract, and the reason
it existed is carried by the case that goes red if anyone narrows the branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VcwwwtZHpwrjWZihjpUvS
* chore(spec): register the uninstall tenant-scope break on the ADR-0087 chain and regenerate docs (#7780)
Two mechanical CI fixes on top of the accepted change.
1. `Check Changeset` — the changeset declares a breaking change and carried no
ADR-0087 disposition. Registered rather than exempted, and the exemption was not
available anyway: `no-migration-prescription` is refused when the changeset body
carries a consumer-rewrite prescription, and this one does ("Callers that must be
updated"). `unpublished` is false (all three bumped packages publish) and
`already-registered` is false (the id is new here).
The substantive reason is the same one the gate asks for: a consumer really does
have to act — an uninstall that succeeded yesterday now answers 400 until it
states its tenant scope — and which scope a given caller meant is an intent no
transform can recover, because the catch-wrapped resolver makes an accidental
org-less call byte-identical to a deliberate environment-wide one. That is the
disposition `rest-requireauth-default-flip` (step 12) took for its own default
flip, and `semantic/` already carries runtime-contract entries of this shape
(`api-runtime-create-withdrawn`), so this is not a metadata-shaped entry forced
into a metadata chain.
New entry `semantic/17.package-uninstall-explicit-all-tenants.ts`, folded in with
`gen:migration-registry`; `spec-changes.json` and `docs/protocol-upgrade-guide.md`
regenerated as its projections.
2. `check:docs` — `TENANT_SCOPE_REQUIRED` was added to the error-code ledger and
the generated reference docs were never regenerated. Ran `gen:schema` then
`gen:docs`. Eleven pages move rather than the two CI named: the shared error
envelope renders the code-enum arity, so `… +261 more` becomes `… +262 more`
wherever it appears. All generated output, hand-edited nowhere. No
`content/docs/releases/**` page is touched.
Merged `origin/main` (clean) before regenerating, so the artifacts are generated
against current main rather than the `59ac0c6` base.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VcwwwtZHpwrjWZihjpUvS
---------
Co-authored-by: Claude <noreply@anthropic.com>
feat(metadata-protocol)!: cross-tenant uninstall must be declared — `deletePackage` refuses a call that names neither an organization nor `allTenants` (#7780)
8
+
9
+
**This changes the contract of a destructive operation, and a caller that omits
10
+
the organization today starts getting a 400. That is the point of the change,
11
+
not a side effect of it.**
12
+
13
+
`protocol.deletePackage` selected its rows with `{ package_id }` and added an
14
+
organization predicate only when the caller supplied one. With no
15
+
`organizationId` the predicate matched **every organization's rows** — measured
16
+
during #7705 at 5 of 5 deleted, including a foreign organization's.
17
+
18
+
Nobody chose that. It fell out of a missing argument, and the two doors of
19
+
`DELETE /api/v1/packages/:id` disagreed about which semantic they were invoking:
20
+
21
+
- the direct-mount REST registrar (`packages/rest/src/package-routes.ts`) passes
22
+
no organization and got the cross-tenant reading;
23
+
- the dispatcher twin (`packages/runtime/src/domains/packages.ts`) resolves one
24
+
and got the org-scoped reading.
25
+
26
+
Worse, the two are indistinguishable at the call site. `resolveActiveOrganizationId`
27
+
(#4127) is entirely `catch`-wrapped, so any throw on the auth seam returns
28
+
`undefined` — an accidental org-less call and a deliberate environment-wide one
29
+
are byte-identical, and the accident silently selected the widest possible
30
+
reading of a destructive operation.
31
+
32
+
Maintainer ruling (2026-08-12), quoted unchanged:
33
+
34
+
> 跨租户卸载必须显式声明,缺省缺参永远不等于「全部租户」.
35
+
36
+
**What changes**
37
+
38
+
-`deletePackage` gains `allTenants?: boolean`, the explicit carrier for
39
+
cross-tenant semantics.
40
+
- A call with neither `organizationId` nor `allTenants: true` is refused with
41
+
`TENANT_SCOPE_REQUIRED` (HTTP 400) and deletes nothing. An explicit
42
+
`allTenants: false` is treated as undeclared: it is not an affirmative request
43
+
for cross-tenant semantics, so it cannot authorise them.
44
+
- A call supplying **both**`organizationId` and `allTenants: true` is refused
45
+
with the same code and status. The two are contradictory, not redundant — one
46
+
scopes to a tenant, the other clears every tenant — and both silent
47
+
resolutions are worse than a refusal: resolving narrow-first makes
48
+
`allTenants: true` silently inert, and resolving explicit-first ignores a named
49
+
organization and deletes every tenant's rows, which is the original defect
50
+
wearing a flag. Rejecting is also the only reading that stays correct when a
51
+
request is composed from two places (a resolver supplying the org, config
52
+
supplying the flag). The message names both offending parameters.
53
+
- The REST direct-mount door now declares `allTenants: true`. It has no
54
+
organization to resolve (`packages/rest` carries no org plumbing at all), so of
55
+
the two remedies the ruling allows, only declaring the intent is available
56
+
there. Its observable behaviour is unchanged; what changed is that the width is
57
+
now stated at the call site instead of inferred from an absent argument.
58
+
59
+
**What deliberately does NOT change**
60
+
61
+
The no-organization branch is still **not** narrowed to `organization_id IS
62
+
NULL`. #7705 proved that narrowing orphans every org-scoped row — the same
63
+
defect pointed the other way. The remedy here is explicitness, not narrowing.
64
+
65
+
**Callers that must be updated**
66
+
67
+
Any caller of `deletePackage` that omits `organizationId` and intends an
68
+
environment-wide uninstall must now pass `allTenants: true`. The refusal message
69
+
names both remedies.
70
+
71
+
Registered on the ADR-0087 migration chain (step 17,
72
+
`package-uninstall-explicit-all-tenants`) rather than exempted: a consumer really
73
+
does have to act — an uninstall that succeeded yesterday now answers 400 until it
74
+
states its tenant scope — and which scope it meant is an intent no transform can
75
+
recover, which is the same disposition `rest-requireauth-default-flip` took for
|**data**|`{ name: string; title?: string; measures: object[]; dimensions: object[] }[]`| ✅ | Available cubes, each as the `CubeMeta` discovery projection — the cube name, its title, and the measures/dimensions a client may name in a query. A bare array: there is no `cubes` wrapper object, and no cube `sql` is published. |
50
50
@@ -79,7 +79,7 @@ const result = AnalyticsEndpoint.parse(data);
79
79
| Property | Type | Required | Description |
80
80
| :--- | :--- | :--- | :--- |
81
81
|**success**|`boolean`| ✅ | Operation success status |
0 commit comments