Skip to content

Commit 62f959d

Browse files
fix(runtime)!: POST /packages refuses an overwrite over a read-only package with 422 WRITABLE_PACKAGE_REQUIRED (#24055)
Fixes #24030 Clause-②: no (narrowing) ## What changed `POST /api/v1/packages` now asks `requireWritablePackage` (that is, `isWritablePackage`, the ADR-0070 D2 read-only rule) for the manifest's id. It asks after the `409 … already exists` check and before either install limb writes. An overwrite over a package the deployment ships is refused `422 WRITABLE_PACKAGE_REQUIRED`: booted by code, installed from the marketplace, or a stored row whose manifest `scope` is `system` / `cloud`. Disable, publish, revert and delete already give that answer. The rule is not re-derived at this door. - `packages/runtime/src/domains/packages.ts`: the guard, one call plus its comment, on the `POST /packages` install path only. - `packages/runtime/src/domains/packages-install-overwrite-readonly.test.ts`: the pins, 16 cases over both limbs (the `protocol` service's `installPackage`, and the no-protocol fallback). - `.changeset/24030-install-overwrite-readonly-package.md`: `@objectstack/runtime` `minor` under the launch-window convention for a breaking narrowing, with FROM / TO for an integrator, the `Clause-②` line, and the ADR-0087 disposition `not-required (no-migration-prescription)`. A `CONTRACT_REVIEW_TIER` review is owed before the queue (narrowing); the seat runs it. ## Answers, path by path | Request | Before | After | |---|---|---| | Overwrite over a read-only id (`overwrite: true` in the body, or `?overwrite=true`) | `201`, registry row replaced (and `sys_packages` written on the protocol limb) | `422 WRITABLE_PACKAGE_REQUIRED`, nothing written | | Install over an existing id without `overwrite` (read-only or not) | `409` | `409`, unchanged: the guard sits after it | | Install of a new id, with or without `overwrite` | `201` | `201` | | Overwrite of an authored package | `201` | `201` | ## Measured **Reproduction at base `b74434c58`.** A throwaway probe on the dispatcher harness (real `ObjectQL` with `registerApp`, real `ObjectStackProtocolImplementation`), deleted afterwards: ```text PROBE protocol code: status=201 rowReplaced=true version=2.0.0 source=authored manifests.has=true sys_packages.publish=1 PROBE protocol marketplace: status=201 rowReplaced=true version=2.0.0 source=authored manifests.has=true sys_packages.publish=1 PROBE fallback code: status=201 rowReplaced=true version=2.0.0 source=authored manifests.has=true sys_packages.publish=0 PROBE fallback marketplace: status=201 rowReplaced=true version=2.0.0 source=authored manifests.has=true sys_packages.publish=0 ``` The new pin file at base: `8 failed | 8 passed`. Every refusal row is red with `expected 201 to be 422`; every CONTROL row and the 409 row are green. **At head `8dd02793f`:** `16 passed (16)`. **Ablation**, on the committed fix. `scripts/ablation-replace.mjs` replaced the guard call with `null`: anchor 1 to 0, marker 0 to 1, blob `b0bb67c44296` to `4e5996f62b61`. The test reaches `packages.ts` by relative import, so no rebuild was needed. Result: `8 failed | 8 passed`. The 8 refusal rows went red; the 6 CONTROL rows and the two 409 rows stayed green. Restore: blob equals HEAD `b0bb67c44296`, and `git diff HEAD` is empty. ## The dispatch's mechanism assumptions 1. **Reproduction:** reproduced on both limbs (above). The protocol limb also wrote the `sys_packages` row: the `package` service's `publish` was called once. 2. **Unknown id:** `isWritablePackage` answers writable for an id nothing installed. An install of a new id with `overwrite: true` answers `201`, pinned on both limbs. So the guard runs on every path that reaches a write without refusing a fresh install. 3. **409 ordering:** the guard sits after the 409. A non-overwrite install over a read-only id keeps its `409` (pinned on both limbs); an overwrite gets the `422`. 4. **Who sends `overwrite`:** no first-party caller does. - `packages/client` exposes `packages.install(m, { overwrite })`; only its own tests send it. - The CLI has no caller of this door. - objectui at the `.objectui-sha` pin `f8267df93` sends no `overwrite`: `git grep` for `overwrite: true` / `overwrite=true` finds 0 hits. Its package create posts `{ manifest }` and expects the 409 on a duplicate. - No legitimate use was found that this narrowing breaks. ## Tests and gates (local, at `8dd02793f`) - `turbo run build --filter='@objectstack/runtime...' --concurrency=2`: 31 of 31 tasks successful, verify-lock `VERDICT command-exit 0`. - `pnpm --filter @objectstack/runtime typecheck`: exit 0, with `check:test-typecheck: OK`. The new test file is in the `tsconfig.test.json` program (`--listFiles` count 1) and has no tsc error. - `pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2`: `405 passed (405)` files, `6157 passed | 19 skipped` tests, exit 0. - `pnpm exec eslint --no-inline-config` on the two changed `.ts` files: exit 0. - `node scripts/pm/dispatch-gates.mjs --commands` (64 families, from merge base `b74434c58`): 62 exit 0. The other 2 are NOT MEASURED, each exit 3 (`PREREQUISITE NOT MET`): - `check:dual-build-cjs-loads` needs every package's `dist/`, and only the runtime closure was built locally. - `check:type-check-debt`: its closure build never acquired the shared verify lock (queue-timeout). - `--ran` reconciliation: 64 accounted, 0 UNRUN. - `Lint & Repo Gates` (the repo-wide eslint run), `TypeScript Type Check`, `Test Core` and `Build Core`: CI on this PR. `test:repo` was not run locally; CI owns it. ## Acceptance notes - **Finding, class (a):** an authored package can declare a deployment-shipped scope and so lock itself. - `POST /api/v1/packages` with `manifest.scope: 'cloud'` (or `'system'`) answers `201` and stores an `authored` row. `isWritablePackage` judges that row read-only, so `DELETE` and `PATCH …/disable` then answer `422 WRITABLE_PACKAGE_REQUIRED`. - Before this PR, an overwrite with `scope: 'project'` was the one API exit. After it, that overwrite is refused too, as the card's Done-when requires for the scope arm. - Measured at head with a throwaway probe, since deleted: `create=201 source=authored storedScope=cloud delete=422 disable=422 overwriteToProject=422`. - Handed to the seat for filing and not fixed here. This door does not refuse a key that the declaration (`ManifestSchema.scope`) admits, so the remedy is a contract decision. - The spec's `overwrite` description ("Overwrite an already-installed package id instead of answering 409 Conflict") does not mention the read-only refusal. This is a note on the doc surface only. - `origin/main` was not merged before opening. Main has gained two commits since the base (`c4111c676`, `d731b89a6`); they touch no file in `packages/runtime`, `packages/metadata-protocol` or `packages/objectql`. CI runs on the merge ref. --- _Generated by [Claude Code](https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent a33f3d5 commit 62f959d

3 files changed

Lines changed: 296 additions & 0 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/runtime': minor
3+
---
4+
5+
fix(runtime)!: `POST /api/v1/packages` with `overwrite` refuses a read-only package with `422 WRITABLE_PACKAGE_REQUIRED`, as disable, publish, revert and delete already do
6+
7+
ADR-0070 D2 makes a package the deployment ships read-only: one booted by code, one installed from the marketplace, and one whose manifest `scope` is `system` or `cloud`. `isWritablePackage` is that rule, and the four lifecycle doors that change a package (`PATCH /packages/:id/disable`, `POST /packages/:id/publish`, `POST /packages/:id/revert`, `DELETE /packages/:id`) refuse such a package with `422 WRITABLE_PACKAGE_REQUIRED`. The install door's `overwrite` option only bypassed the `409 … already exists` answer and never asked the rule. So an overwrite over a read-only package answered `201` and replaced its registry row with an authored one, and with a `protocol` service composed it also wrote that package's `sys_packages` row, while the server went on treating the package as read-only. The install door now asks the same rule, for the manifest's id, before either install path writes anything.
8+
9+
Clause-②: no (narrowing)
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is the answer one runtime REST door gives a caller who asks it to overwrite a package the deployment ships; the remedy is to install under an id the caller owns or to customise beside the shipped package, never a rewrite of a package source or a stored row. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
12+
13+
**BREAKING** (a REST door's accept set narrows), shipped as `minor` under the launch-window convention for breaking changes.
14+
15+
- **FROM:** `POST /api/v1/packages` with `overwrite: true` in the wrapped body, or `?overwrite=true`, and a manifest whose `id` names a read-only package, answered `201` and replaced that package's registry row.
16+
- **TO:** the same request answers `422` with `error.code: 'WRITABLE_PACKAGE_REQUIRED'` and `error.details.packageId`, and writes nothing: no registry row, no `sys_packages` row. It is the refusal the four lifecycle doors give.
17+
- **Unchanged:** an install of a new id, with or without `overwrite`, and an overwrite of a package an organization authored answer `201` as before. An install over an existing id without `overwrite` still answers `409`.
18+
- **The fix for an integrator:** a package the deployment ships is managed by the deployment (its code release, or a new marketplace version), not over this API. Install the manifest under an id you own, or customise what the shipped package provides beside it as environment-level state (ADR-0131 D6).
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* `POST /packages` with `overwrite` — the ADR-0070 D2 read-only rule at the
5+
* install door (#24030).
6+
*
7+
* ## The defect
8+
*
9+
* `overwrite` only bypassed the `409 Package '…' already exists`; nothing
10+
* asked whether the id was a read-only package. An overwrite naming an id that
11+
* `registerApp` booted (code, or a marketplace install) answered `201` on
12+
* both limbs and REPLACED the registry row, stamped `source: 'authored'`,
13+
* while `engine.manifests` still held the id, so the served row read
14+
* `writable: false`. Disable, publish, revert and delete already refused that
15+
* same id `422 WRITABLE_PACKAGE_REQUIRED`.
16+
*
17+
* ## What is asserted, and why it is not the status alone
18+
*
19+
* The harm is the replaced row, so every refusal reads the observables: the
20+
* registry row is the SAME object it was (the registry's `installPackage`
21+
* replaces the row object), `engine.manifests` holds the same manifest, and no
22+
* write was attempted: neither `registry.installPackage` nor, on the protocol
23+
* limb, `protocol.installPackage` and the `package` service's `publish` (the
24+
* `sys_packages` write). The engine is a REAL `ObjectQL`: the read-only ids are
25+
* booted through its own `registerApp`, so `engine.manifests` and each row's
26+
* `source` are what production stamps. The scope-only id is installed the way
27+
* `sys_packages` hydration installs a stored row (`source: 'authored'`, no
28+
* booted manifest), so its refusal can only come from the scope signal.
29+
*
30+
* ## Both limbs
31+
*
32+
* The door installs through the `protocol` service's `installPackage` when one
33+
* resolves, and falls back to the bare registry write when none does. Every
34+
* case runs on both.
35+
*
36+
* ## The gate is not an outage
37+
*
38+
* CONTROL: an install of a new id (with and without `overwrite`) and an
39+
* overwrite of an authored package still answer `201` and really write, on the
40+
* same spies, which is what makes the refusal rows' "nothing was written" a
41+
* measurement rather than a vacuous truth.
42+
*/
43+
44+
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
45+
import { mkdtempSync, rmSync } from 'node:fs';
46+
import { tmpdir } from 'node:os';
47+
import { join } from 'node:path';
48+
import { ObjectQL } from '@objectstack/objectql';
49+
import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol';
50+
import { HttpDispatcher } from '../http-dispatcher.js';
51+
52+
/** Booted by the deployment's code: `registerApp`, default source `'code'`. */
53+
const CODE_BOOTED = 'com.acme.codepkg';
54+
/** Installed from the marketplace: `registerApp(…, 'marketplace')`. */
55+
const MARKETPLACE = 'com.acme.marketpkg';
56+
/** A stored row whose manifest declares `scope: 'cloud'`; nothing booted it. */
57+
const CLOUD_SCOPED = 'com.acme.cloudpkg';
58+
/** An org's own base, installed through this door. */
59+
const AUTHORED = 'com.acme.authoredpkg';
60+
/** An id nothing has installed. */
61+
const FRESH = 'com.acme.freshpkg';
62+
63+
const manifest = (id: string, version: string, extra: Record<string, unknown> = {}) => ({
64+
id,
65+
name: id,
66+
version,
67+
type: 'app',
68+
...extra,
69+
});
70+
71+
const silentLogger: any = {
72+
debug() {}, info() {}, warn() {}, error() {}, trace() {}, fatal() {},
73+
child() { return silentLogger; },
74+
};
75+
76+
type Limb = 'protocol' | 'fallback';
77+
const LIMBS: Limb[] = ['protocol', 'fallback'];
78+
79+
/**
80+
* The composed door over a real engine. `protocol` resolves the real protocol
81+
* primitive with a `package` service double standing in for `sys_packages`;
82+
* `fallback` resolves no protocol service, so the door takes its bare
83+
* registry write.
84+
*/
85+
function door(limb: Limb) {
86+
const engine = new ObjectQL({ logger: silentLogger });
87+
(engine.registry as unknown as { logLevel: string }).logLevel = 'silent';
88+
engine.registerApp(manifest(CODE_BOOTED, '1.0.0'));
89+
engine.registerApp(manifest(MARKETPLACE, '1.0.0'), undefined, 'marketplace');
90+
engine.registry.installPackage(manifest(CLOUD_SCOPED, '1.0.0', { scope: 'cloud' }) as any, 'authored');
91+
92+
const packageService = { publish: vi.fn(async () => ({ success: true })) };
93+
const services = new Map<string, unknown>([['package', packageService]]);
94+
const protocol = limb === 'protocol'
95+
? new ObjectStackProtocolImplementation(engine as any, () => services)
96+
: null;
97+
const kernel: any = {
98+
getService: (name: string) => {
99+
if (name === 'protocol') return protocol ? Promise.resolve(protocol) : null;
100+
if (name === 'objectql') return Promise.resolve(engine);
101+
return null;
102+
},
103+
context: { getService: (name: string) => (name === 'objectql' ? engine : null) },
104+
};
105+
const registryInstall = vi.spyOn(engine.registry, 'installPackage');
106+
const protocolInstall = protocol ? vi.spyOn(protocol, 'installPackage') : undefined;
107+
const manifests = (engine as unknown as { manifests: Map<string, unknown> }).manifests;
108+
return {
109+
dispatcher: new HttpDispatcher(kernel),
110+
engine,
111+
manifests,
112+
registryInstall,
113+
protocolInstall,
114+
publish: packageService.publish,
115+
};
116+
}
117+
118+
const ADMIN = (): any => ({
119+
request: {},
120+
environmentId: 'pkg-install-overwrite-readonly-test',
121+
executionContext: {
122+
userId: 'u_pkg_admin',
123+
systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'],
124+
},
125+
});
126+
127+
type Door = ReturnType<typeof door>;
128+
const install = (d: Door, body: unknown, query: Record<string, unknown> = {}) =>
129+
d.dispatcher.handlePackages('', 'POST', body, query, ADMIN());
130+
131+
// The allow path persists enable state under OS_HOME; keep it out of the
132+
// developer's own home.
133+
let home: string;
134+
let priorHome: string | undefined;
135+
beforeAll(() => {
136+
priorHome = process.env.OS_HOME;
137+
home = mkdtempSync(join(tmpdir(), 'os-pkg-overwrite-readonly-'));
138+
process.env.OS_HOME = home;
139+
});
140+
afterAll(() => {
141+
if (priorHome === undefined) delete process.env.OS_HOME;
142+
else process.env.OS_HOME = priorHome;
143+
rmSync(home, { recursive: true, force: true });
144+
});
145+
146+
/** The refusal's envelope, and that nothing on either limb was written. */
147+
function expectRefusedUnwritten(d: Door, r: any, id: string) {
148+
expect(r.response?.status).toBe(422);
149+
expect(r.response?.body?.success).toBe(false);
150+
expect(r.response?.body?.error?.code).toBe('WRITABLE_PACKAGE_REQUIRED');
151+
expect(r.response?.body?.error?.httpStatus).toBe(422);
152+
expect(r.response?.body?.error?.details?.packageId).toBe(id);
153+
expect(d.registryInstall).not.toHaveBeenCalled();
154+
if (d.protocolInstall) expect(d.protocolInstall).not.toHaveBeenCalled();
155+
expect(d.publish).not.toHaveBeenCalled();
156+
}
157+
158+
// ══════════════════════════════════════════════════════════════════════════════
159+
// 1. REFUSED: an overwrite over a read-only id, on both limbs
160+
// ══════════════════════════════════════════════════════════════════════════════
161+
162+
const READ_ONLY: Array<{ label: string; id: string; source: string; booted: boolean }> = [
163+
{ label: 'code-booted (registerApp)', id: CODE_BOOTED, source: 'code', booted: true },
164+
{ label: 'marketplace-installed (registerApp, marketplace)', id: MARKETPLACE, source: 'marketplace', booted: true },
165+
{ label: 'cloud-scoped stored row (scope signal only)', id: CLOUD_SCOPED, source: 'authored', booted: false },
166+
];
167+
168+
for (const limb of LIMBS) {
169+
describe(`POST /packages overwrite over a read-only id is refused (${limb} limb)`, () => {
170+
for (const pkg of READ_ONLY) {
171+
it(`422s the overwrite of the ${pkg.label} package and leaves its row and manifest as they were`, async () => {
172+
const d = door(limb);
173+
const rowBefore = d.engine.registry.getPackage(pkg.id);
174+
const manifestBefore = d.manifests.get(pkg.id);
175+
expect(rowBefore?.manifest?.version, 'precondition: the booted row').toBe('1.0.0');
176+
expect(d.manifests.has(pkg.id), 'precondition: the boot signal').toBe(pkg.booted);
177+
178+
const r = await install(d, { manifest: manifest(pkg.id, '2.0.0'), overwrite: true });
179+
180+
expectRefusedUnwritten(d, r, pkg.id);
181+
expect(d.engine.registry.getPackage(pkg.id)).toBe(rowBefore);
182+
expect(d.engine.registry.getPackage(pkg.id)?.manifest?.version).toBe('1.0.0');
183+
expect(d.engine.registry.getPackage(pkg.id)?.source).toBe(pkg.source);
184+
expect(d.manifests.get(pkg.id)).toBe(manifestBefore);
185+
expect(d.manifests.has(pkg.id)).toBe(pkg.booted);
186+
});
187+
}
188+
189+
it('refuses the `?overwrite=true` query spelling the same way', async () => {
190+
const d = door(limb);
191+
const rowBefore = d.engine.registry.getPackage(CODE_BOOTED);
192+
193+
const r = await install(d, { manifest: manifest(CODE_BOOTED, '2.0.0') }, { overwrite: 'true' });
194+
195+
expectRefusedUnwritten(d, r, CODE_BOOTED);
196+
expect(d.engine.registry.getPackage(CODE_BOOTED)).toBe(rowBefore);
197+
expect(d.manifests.has(CODE_BOOTED)).toBe(true);
198+
});
199+
200+
it('keeps the 409 for an install over a read-only id that does not ask to overwrite', async () => {
201+
const d = door(limb);
202+
const rowBefore = d.engine.registry.getPackage(CODE_BOOTED);
203+
204+
const r = await install(d, { manifest: manifest(CODE_BOOTED, '2.0.0') });
205+
206+
expect(r.response?.status).toBe(409);
207+
expect(d.registryInstall).not.toHaveBeenCalled();
208+
expect(d.publish).not.toHaveBeenCalled();
209+
expect(d.engine.registry.getPackage(CODE_BOOTED)).toBe(rowBefore);
210+
});
211+
});
212+
}
213+
214+
// ══════════════════════════════════════════════════════════════════════════════
215+
// 2. CONTROL: the gate is not an outage
216+
// ══════════════════════════════════════════════════════════════════════════════
217+
218+
for (const limb of LIMBS) {
219+
describe(`POST /packages still installs and overwrites what an org owns (${limb} limb)`, () => {
220+
it('installs a new id: 201, and the row is written', async () => {
221+
const d = door(limb);
222+
223+
const r = await install(d, { manifest: manifest(FRESH, '1.0.0') });
224+
225+
expect(r.response?.status).toBe(201);
226+
expect(d.registryInstall).toHaveBeenCalledTimes(1);
227+
expect(d.engine.registry.getPackage(FRESH)?.source).toBe('authored');
228+
if (limb === 'protocol') expect(d.publish).toHaveBeenCalledTimes(1);
229+
});
230+
231+
it('installs a new id with `overwrite: true` set: nothing to overwrite, still 201', async () => {
232+
const d = door(limb);
233+
234+
const r = await install(d, { manifest: manifest(FRESH, '1.0.0'), overwrite: true });
235+
236+
expect(r.response?.status).toBe(201);
237+
expect(d.registryInstall).toHaveBeenCalledTimes(1);
238+
expect(d.engine.registry.getPackage(FRESH)?.manifest?.version).toBe('1.0.0');
239+
if (limb === 'protocol') expect(d.publish).toHaveBeenCalledTimes(1);
240+
});
241+
242+
it('overwrites an authored base: 201, and the row is replaced', async () => {
243+
const d = door(limb);
244+
const first = await install(d, { manifest: manifest(AUTHORED, '1.0.0') });
245+
expect(first.response?.status, 'precondition: the base is installed').toBe(201);
246+
const rowBefore = d.engine.registry.getPackage(AUTHORED);
247+
248+
const r = await install(d, { manifest: manifest(AUTHORED, '2.0.0'), overwrite: true });
249+
250+
expect(r.response?.status).toBe(201);
251+
expect(d.registryInstall).toHaveBeenCalledTimes(2);
252+
expect(d.engine.registry.getPackage(AUTHORED)).not.toBe(rowBefore);
253+
expect(d.engine.registry.getPackage(AUTHORED)?.manifest?.version).toBe('2.0.0');
254+
expect(d.engine.registry.getPackage(AUTHORED)?.source).toBe('authored');
255+
if (limb === 'protocol') {
256+
expect(d.protocolInstall).toHaveBeenCalledTimes(2);
257+
expect(d.publish).toHaveBeenCalledTimes(2);
258+
}
259+
});
260+
});
261+
}

‎packages/runtime/src/domains/packages.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,23 @@ export async function handlePackagesRequest(deps: DomainHandlerDeps, path: strin
13811381
response: deps.error(`Package '${pkgId}' already exists`, 409),
13821382
};
13831383
}
1384+
// [#24030] ADR-0070 D2 — the READ-ONLY gate, asked for the manifest's
1385+
// id before either install limb below writes. `overwrite` only ever
1386+
// bypassed the 409 above; nothing asked whether the id is a package
1387+
// the deployment ships, so an overwrite over a code-booted or
1388+
// marketplace-installed id answered 201, replaced its registry row
1389+
// with an authored one (and, on the protocol limb, wrote its
1390+
// `sys_packages` row) while `engine.manifests` kept judging it
1391+
// read-only. Disable, publish, revert and delete refuse that id with
1392+
// this same answer; ⛔ the rule is not re-derived here — it is
1393+
// {@link requireWritablePackage}, i.e. {@link isWritablePackage}.
1394+
//
1395+
// Placed AFTER the 409, so an install that does not ask to overwrite
1396+
// keeps its 409 answer for any existing id. An id nothing installed
1397+
// is writable by the predicate's own rule, so a fresh install —
1398+
// `overwrite` set or not — answers as before.
1399+
const readOnly = requireWritablePackage(deps, qlService, pkgId, overwrite ? 'overwrite' : 'install');
1400+
if (readOnly) return readOnly;
13841401
// ⚠️ The MANIFEST handed on is the one the caller sent, ⛔ not the
13851402
// parsed copy. `ManifestSchema` applies defaults at parse time
13861403
// (`scope`, `defaultDatasource`), and this door has always stored

0 commit comments

Comments
 (0)