diff --git a/.changeset/permission-metadata-door-lock.md b/.changeset/permission-metadata-door-lock.md new file mode 100644 index 0000000000..cc9e983ebd --- /dev/null +++ b/.changeset/permission-metadata-door-lock.md @@ -0,0 +1,9 @@ +--- +'@objectstack/plugin-security': minor +--- + +The packaged-permission-set lock now guards the metadata door as well as the data door. The pre-persistence authoring-gate seam gains a `permission` registration (`registerPackagedPermissionSetLockGate`, new export) that consults the same `classifyPackagedPermissionSet` classifier and throws the same `PackagedPermissionSetLockedError` the `sys_permission_set` write door already uses — one spelling of "package-declared", two doors, one refusal. + +What stops working, and for whom: an operator using the `OS_METADATA_WRITABLE=permission` escape hatch to save a permission set that an installed package declares now gets `403 NOT_OVERRIDABLE` (the refusal names the sanctioned clone path) instead of silently minting a `sys_metadata` overlay whose grants win at read over the package's declaration. This applies hatch open or closed, draft and publish saves alike, and also means stored-overlay maintenance passes (e.g. stored-item migration) report a per-row refusal for such grandfathered forks rather than rewriting them. + +What keeps working unchanged: hatch writes to any name no installed package declares — the hatch's documented per-org/env override capability — land exactly as before, and package-door authoring of workspace-owned sets (definitions living in `sys_metadata`; ADR-0070, ADR-0094 D5-R) stays editable. Package publishes travel the `package-author` channel, which this seam exempts by contract. diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 01cc9e0ed4..480c0d8187 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -2,7 +2,7 @@ "name": "@objectstack/plugin-security", "version": "17.2.0", "license": "Apache-2.0", - "description": "Security Plugin for ObjectStack — RBAC, RLS, and Field-Level Security Runtime", + "description": "Security Plugin for ObjectStack \u2014 RBAC, RLS, and Field-Level Security Runtime", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { @@ -27,6 +27,7 @@ }, "devDependencies": { "@objectstack/driver-sql": "workspace:*", + "@objectstack/metadata-protocol": "workspace:*", "@objectstack/objectql": "workspace:*", "@objectstack/plugin-sharing": "workspace:*", "@objectstack/service-i18n": "workspace:*", diff --git a/packages/plugins/plugin-security/src/index.ts b/packages/plugins/plugin-security/src/index.ts index 7c95e00bdf..5297d93d7f 100644 --- a/packages/plugins/plugin-security/src/index.ts +++ b/packages/plugins/plugin-security/src/index.ts @@ -132,6 +132,11 @@ export type { PackagedSetVerdict, LayeredProbe, } from './packaged-permission-set-lock.js'; +// [#11843 — maintainer ruling 2026-08-25, option B] The lock's METADATA-door +// registration: the pre-persistence authoring-gate seam consults the SAME +// classifier and throws the SAME error classes as the data door above. +export { registerPackagedPermissionSetLockGate } from './packaged-permission-set-lock-gate.js'; +export type { PermissionSetLockGateContext } from './packaged-permission-set-lock-gate.js'; export { OVERLAY_PAGE_LIMIT, detectPackagedPermissionSetOverlays, diff --git a/packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.test.ts b/packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.test.ts new file mode 100644 index 0000000000..c8ab939baf --- /dev/null +++ b/packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.test.ts @@ -0,0 +1,286 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #11843 — the packaged-permission-set lock answers at the METADATA door. + * + * The lock (`packaged-permission-set-lock.ts`) used to have exactly one + * enforcement point: the `sys_permission_set` DATA door. The pre-persistence + * authoring-gate seam carried an `'object'` registration only, so a + * metadata-door save targeting a package-declared permission set reached + * persistence whenever the ADR-0005 tier gate was open for the type + * (`OS_METADATA_WRITABLE=permission`) — and the resulting overlay won at + * read. The maintainer ruling of 2026-08-25 (「11843 同意」 — option B) closed + * that door by REGISTERING the same lock on the seam rather than authoring a + * second refusal; this file pins the registered behaviour end to end. + * + * ## The harness, and what makes each case answer for the layer it names + * + * A REAL `ObjectStackProtocolImplementation` (aliased to the producer's + * source — see `vitest.config.ts`) over a minimal fake engine, on the + * host-config topology (`environmentId` undefined — the flagship showcase's + * own assembly, the one whose `saveMetaItem` runs the authoring gate ahead of + * every persistence path). The refusal cases assert the lock's ERROR CLASS + * IDENTITY (`instanceof PackagedPermissionSetLockedError`), not only the + * `code`/`status` envelope: `NOT_OVERRIDABLE`/403 is shared with the ADR-0005 + * tier gate by design, so the class is the only fingerprint that proves WHICH + * layer answered. And every refusal case asserts the ROW COUNT — the defect + * this card measured was a write that landed, so "threw" alone is half a pin. + * + * ## What is deliberately NOT re-pinned here (Prime Directive #8) + * + * - The data door staying locked is `packaged-permission-set-lock.test.ts`. + * - The hatch's documented behaviour for non-packaged names on the UNGATED + * protocol is `sys-metadata-repository.package-writability.test.ts` in + * `@objectstack/metadata-protocol` (39 pins, authoritative per the ruling); + * this file adds only the with-gate half of that preservation. + * - The classifier's own verdict table is `packaged-permission-set-lock.test.ts`. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +// The producer's OWN write-verb dispatch decisions, so this fake engine cannot +// accept a call ObjectQL refuses (`check:engine-double-contract`; imported +// from `@objectstack/metadata-core` exactly as the sibling pins do). +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; +import { + ObjectStackProtocolImplementation, + resetEnvWritableMetadataTypes, +} from '@objectstack/metadata-protocol'; +import { registerPackagedPermissionSetLockGate } from './packaged-permission-set-lock-gate.js'; +import { + PackagedPermissionSetLockedError, + PackagedPermissionSetProvenanceUnknownError, +} from './packaged-permission-set-lock.js'; + +interface Row { [k: string]: unknown } + +/** The installed code package whose declaration locks its set. */ +const PKG = 'com.example.crm'; +/** The set the package declares — artifact-shipped, so the lock owns it. */ +const PACKAGED_SET = 'crm_support_agent'; +/** An ordinary env-authored name no package declares. */ +const ORG_SET = 'org_reporting'; +/** + * A set whose definition lives only in `sys_metadata` (ADR-0070 package-door + * authoring): hydrated into the registry as a runtime shadow, stamped with + * the `'sys_metadata'` sentinel `_packageId`. NOT package-declared to the + * lock — ADR-0094 D5-R keeps this tier editable. + */ +const SHADOW_SET = 'workspace_drafted'; + +/** Spec-valid permission-set body — must pass the Zod gate, which runs BEFORE the seam under test. */ +const body = (name: string) => ({ name, label: 'Pinned', objects: {} }); + +/** + * Minimal engine fake: enough storage for `saveMetaItem`'s repository path + * (the accept cases read their row back) and a SchemaRegistry surface for the + * classifier (`listItems`) and the protocol (`getArtifactItem` — what makes + * the packaged name artifact-backed, mirroring an installed package). + */ +function makeFakeEngine(opts?: { listItemsThrows?: boolean }) { + const rows = new Map(); + const historyRows: Row[] = []; + const keyOf = (w: Record, table = 'sys_metadata') => + `${table}|${String(w.type)}|${String(w.name)}|${String(w.organization_id ?? 'null')}|${String(w.state ?? 'active')}`; + const findRow = (where: Record) => { + if (where.id !== undefined) { + for (const [k, r] of rows) if (r.id === where.id) return { key: k, row: r }; + return null; + } + const k = keyOf(where); + const r = rows.get(k); + return r ? { key: k, row: r } : null; + }; + const declaredItems = [ + { name: PACKAGED_SET, label: 'Support Agent', _packageId: PKG, objects: {} }, + { name: SHADOW_SET, label: 'Drafted', _packageId: 'sys_metadata', objects: {} }, + ]; + return { + rows, + manifests: new Map([[PKG, { id: PKG }]]), + registry: { + getPackage: () => undefined, + registerItem: () => {}, + registerObject: () => {}, + getItem: () => undefined, + listItems: (type: string) => { + if (opts?.listItemsThrows) throw new Error('registry unreadable'); + return type === 'permission' ? declaredItems : []; + }, + getArtifactItem: (type: string, name: string) => + type === 'permission' && name === PACKAGED_SET + ? { name, _packageId: PKG } + : undefined, + }, + async find(table: string, _opts: { where: Record }) { + if (table === 'sys_metadata_history') return historyRows; + return Array.from(rows.values()); + }, + async findOne(table: string, opts2: { where: Record }) { + if (table === 'sys_metadata_history') return null; + return findRow(opts2.where)?.row ?? null; + }, + async insert(table: string, data: Record) { + if (table === 'sys_metadata_history') { + const h: Row = { ...data }; + if (!h.id) h.id = `h_${historyRows.length + 1}`; + historyRows.push(h); + return { id: h.id as string }; + } + const k = keyOf(data, table); + const row: Row = { id: `r_${rows.size + 1}`, __table: table, ...data }; + rows.set(k, row); + return { id: row.id as string }; + }, + async update(_t: string, data: Record, opts2: { where: Record }) { + assertEngineUpdateDispatch(data, opts2); + const found = findRow(opts2.where); + if (!found) throw new Error('not found'); + rows.set(found.key, { ...found.row, ...data }); + return { id: found.row.id as string }; + }, + async delete(_t: string, opts2: { where: Record }) { + assertEngineDeleteDispatch(opts2); + const found = findRow(opts2.where); + if (!found) return { deleted: 0 }; + rows.delete(found.key); + return { deleted: 1 }; + }, + async transaction(cb: (ctx: unknown, info: { owned: boolean }) => Promise): Promise { + return cb(undefined, { owned: true }); + }, + }; +} + +function boot(opts?: { listItemsThrows?: boolean }) { + const engine = makeFakeEngine(opts); + const protocol = new ObjectStackProtocolImplementation( + engine as never, + () => new Map(), + undefined, // host-config topology — no environmentId + ) as unknown as { + saveMetaItem(req: Record): Promise; + registerAuthoringGate?(type: string, gate: unknown): void; + }; + const wired = registerPackagedPermissionSetLockGate(protocol, engine); + return { engine, protocol, wired }; +} + +const metaRowsOf = (engine: { rows: Map }) => + Array.from(engine.rows.values()).filter((r) => r.__table === 'sys_metadata'); + +const save = ( + protocol: { saveMetaItem(req: Record): Promise }, + req: Record, +) => protocol.saveMetaItem(req).then(() => null, (e: unknown) => e); + +const openHatch = () => { + process.env.OS_METADATA_WRITABLE = 'permission'; + resetEnvWritableMetadataTypes(); + (ObjectStackProtocolImplementation as unknown as { resetEnvWritableCache(): void }).resetEnvWritableCache(); +}; + +describe('#11843 — the lock answers at the metadata door', () => { + beforeEach(() => { + delete process.env.OS_METADATA_WRITABLE; + resetEnvWritableMetadataTypes(); + (ObjectStackProtocolImplementation as unknown as { resetEnvWritableCache(): void }).resetEnvWritableCache(); + }); + afterEach(() => { + delete process.env.OS_METADATA_WRITABLE; + resetEnvWritableMetadataTypes(); + (ObjectStackProtocolImplementation as unknown as { resetEnvWritableCache(): void }).resetEnvWritableCache(); + }); + + // ── the inversion of the measured defect ───────────────────────────────── + + it('hatch OPEN: a package-less save targeting a package-declared set is refused by the LOCK, and no row lands', async () => { + const { engine, protocol, wired } = boot(); + expect(wired).toBe(true); + openHatch(); + + const err = await save(protocol, { type: 'permission', name: PACKAGED_SET, item: body(PACKAGED_SET) }); + + // Class identity is the layer fingerprint — the ADR-0005 tier gate shares + // this code and status, but only the lock constructs this class. + expect(err).toBeInstanceOf(PackagedPermissionSetLockedError); + expect(err).toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 }); + expect((err as Error).message).toContain(PKG); + // The defect was a write that LANDED — the throw alone is half the pin. + expect(metaRowsOf(engine)).toEqual([]); + }, 30_000); + + it('hatch CLOSED: the identical save is refused by the same lock — the refusal does not depend on the hatch', async () => { + const { engine, protocol } = boot(); + + const err = await save(protocol, { type: 'permission', name: PACKAGED_SET, item: body(PACKAGED_SET) }); + + expect(err).toBeInstanceOf(PackagedPermissionSetLockedError); + expect(err).toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 }); + expect(metaRowsOf(engine)).toEqual([]); + }, 30_000); + + it('hatch OPEN: a DRAFT save of the packaged name is refused too — the seam gates both minting paths', async () => { + const { engine, protocol } = boot(); + openHatch(); + + const err = await save(protocol, { + type: 'permission', name: PACKAGED_SET, item: body(PACKAGED_SET), mode: 'draft', + }); + + expect(err).toBeInstanceOf(PackagedPermissionSetLockedError); + expect(err).toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 }); + expect(metaRowsOf(engine)).toEqual([]); + }, 30_000); + + // ── the preservation half — NARROW is retained, per the ruling ─────────── + + it('hatch OPEN: a package-less save to a NON-packaged name still lands, bound to no package', async () => { + const { engine, protocol } = boot(); + openHatch(); + + const err = await save(protocol, { type: 'permission', name: ORG_SET, item: body(ORG_SET) }); + + expect(err).toBeNull(); + const rows = metaRowsOf(engine); + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ package_id: null, organization_id: null }); + }, 30_000); + + it('hatch OPEN: a runtime-shadow set (definition living only in sys_metadata) is NOT locked — ADR-0094 D5-R', async () => { + const { engine, protocol } = boot(); + openHatch(); + + const err = await save(protocol, { type: 'permission', name: SHADOW_SET, item: body(SHADOW_SET) }); + + expect(err).toBeNull(); + expect(metaRowsOf(engine)).toHaveLength(1); + }, 30_000); + + // ── fail-closed, one spelling with the data door ───────────────────────── + + it('no provenance source can answer → the save is refused rather than guessed (fail-closed)', async () => { + // The gate function alone, on a protocol stub with no layered read: the + // registry read throws and no second source exists, which is the exact + // `unknown` verdict the lock's header refuses to accept on a write door. + let gate: ((ctx: { type: string; name: string; body: unknown }) => Promise) | undefined; + const stub = { + registerAuthoringGate: (_type: string, g: typeof gate) => { gate = g; }, + }; + const ql = { registry: { listItems: () => { throw new Error('registry unreadable'); } } }; + expect(registerPackagedPermissionSetLockGate(stub, ql)).toBe(true); + expect(gate).toBeTypeOf('function'); + + const err = await gate!({ type: 'permission', name: PACKAGED_SET, body: body(PACKAGED_SET) }) + .then(() => null, (e: unknown) => e); + + expect(err).toBeInstanceOf(PackagedPermissionSetProvenanceUnknownError); + expect(err).toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 }); + }); + + // ── feature detection, mirroring registerObjectPostureGate ─────────────── + + it('a protocol without the seam keeps its behaviour and the caller can read the false', () => { + expect(registerPackagedPermissionSetLockGate({}, {})).toBe(false); + expect(registerPackagedPermissionSetLockGate(undefined, {})).toBe(false); + }); +}); diff --git a/packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.ts b/packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.ts new file mode 100644 index 0000000000..0dadd6a904 --- /dev/null +++ b/packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.ts @@ -0,0 +1,95 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * THE METADATA-DOOR REGISTRATION of the packaged-permission-set lock + * (#11843; maintainer ruling 2026-08-25, verbatim: 「11843 同意」 — option B: + * keep NARROW, move the lock). + * + * `packaged-permission-set-lock.ts` refuses a write that targets a + * package-declared permission set. Until this file existed it had exactly one + * enforcement point: the `sys_permission_set` DATA door + * (`createPermissionSetWriteThrough`, `permission-set-projection.ts`). The + * metadata protocol's pre-persistence authoring-gate seam (ADR-0094 addendum; + * `registerAuthoringGate`) carried an `'object'` registration only — so a + * metadata-door write targeting the same package-declared set reached + * persistence whenever the ADR-0005 tier gate was open for the type + * (`OS_METADATA_WRITABLE=permission`, the documented operator hatch), and the + * resulting overlay won at read. Same lock, two doors, one of them unguarded. + * + * This registration closes that door WITHOUT authoring a second refusal: + * + * - ⭐ ONE SPELLING (Prime Directive #8). The gate calls + * {@link assertPermissionSetNotPackageDeclared} — the same assertion the + * data door calls, consulting the same {@link classifyPackagedPermissionSet} + * classifier and throwing the same error classes + * (`PackagedPermissionSetLockedError`, and the fail-closed + * `PackagedPermissionSetProvenanceUnknownError` when no provenance source + * can answer — the lock's header says why accepting on `unknown` is the one + * guess a write door must not make). The code (`NOT_OVERRIDABLE`), the + * status (403) and the clone prescription cannot drift between the doors, + * because both doors run the same lines. + * + * - The hatch's documented capability is RETAINED (the ruling explicitly kept + * #8146 NARROW): a write to any name no installed package declares keeps + * working exactly as `environment-variables.mdx` promises, hatch open or + * closed — the gate returns without effect on an `org` verdict. + * + * - The classifier's runtime-shadow exclusion carries over unchanged: a set + * whose definition lives only in `sys_metadata` (ADR-0070 package-door + * authoring; hydrated into the registry as a `sys_metadata`-stamped shadow) + * is NOT "package-declared" to the lock, so the surviving + * `allowRuntimeCreate` tier (ADR-0094 D5-R) stays editable through this + * door. Only an artifact-shipped declaration locks. + * + * - The seam's own contract does the channel split: `saveMetaItem` runs + * authoring gates for BOTH draft and publish-mode environment saves and for + * NEITHER on the `package-author` channel (#6710/#7674) — a package + * publishing its own declaration never meets this gate. + * + * The layered read mirrors the data door's `probeLayered`: the lock's second + * artifact source for kernels with no readable SchemaRegistry, reported as + * `failed` rather than collapsed into "no artifact" when the read throws + * (that distinction is what `unknown` fail-closed rests on). + * + * Like `registerObjectPostureGate`, registration is feature-detected: a + * protocol that predates `registerAuthoringGate` (older embeddings, unit-test + * stubs) keeps its existing behavior, and the caller can read the `false`. + */ + +import { + assertPermissionSetNotPackageDeclared, + type LayeredProbe, +} from './packaged-permission-set-lock.js'; + +/** Context subset of the protocol's MetadataAuthoringGateContext this gate consumes. */ +export interface PermissionSetLockGateContext { + type: string; + name: string; + body: unknown; +} + +/** + * Wire the packaged-permission-set lock onto the metadata protocol's + * pre-persistence authoring-gate seam for the `permission` type. Returns + * `true` when wired. + */ +export function registerPackagedPermissionSetLockGate(protocol: any, ql: any): boolean { + if (!protocol || typeof protocol.registerAuthoringGate !== 'function') return false; + protocol.registerAuthoringGate('permission', async (ctx: PermissionSetLockGateContext) => { + let probe: LayeredProbe | undefined; + if (typeof protocol.getMetaItemLayered === 'function') { + try { + const envelope = await protocol.getMetaItemLayered({ type: 'permission', name: ctx.name }); + probe = { status: 'read', envelope }; + } catch (e) { + probe = { status: 'failed', reason: String((e as Error)?.message ?? e) }; + } + } + // `'update'` on purpose: a metadata-door save that targets a + // package-declared name is an override of the shipped base whether or not + // an overlay row exists yet, so the remedy the refusal teaches is the + // clone path, not "choose a different name". + assertPermissionSetNotPackageDeclared(ctx.name, ql, 'update', probe); + }); + return true; +} diff --git a/packages/plugins/plugin-security/src/security-plugin.ts b/packages/plugins/plugin-security/src/security-plugin.ts index 706edf699e..27a2dcb311 100644 --- a/packages/plugins/plugin-security/src/security-plugin.ts +++ b/packages/plugins/plugin-security/src/security-plugin.ts @@ -39,6 +39,7 @@ import { runPermissionSetDriftDiagnostics } from './permission-set-drift.js'; import { reportPackagedPermissionSetOverlays } from './packaged-permission-set-overlay-detection.js'; import { discardPermissionSetOverlay, type PermissionSetOverlayDiscardDeps } from './permission-set-overlay-discard.js'; import { registerObjectPostureGate } from './object-posture-gate.js'; +import { registerPackagedPermissionSetLockGate } from './packaged-permission-set-lock-gate.js'; import { reconcileAudienceBindingSuggestions, listAudienceBindingSuggestions, @@ -3245,6 +3246,17 @@ export class SecurityPlugin implements Plugin { // Feature-detected; protocols predating registerAuthoringGate keep // the legacy (CLI-lint-only) behavior. registerObjectPostureGate(protocol); + // [#11843 — maintainer ruling 2026-08-25, option B] The packaged- + // permission-set lock's METADATA-door registration: the same + // classifier and error classes the data door runs + // (`packaged-permission-set-lock.ts`), now consulted on the + // pre-persistence seam for `permission` bodies too — so an + // `OS_METADATA_WRITABLE=permission` hatch write targeting a + // package-declared set is refused before persistence instead of + // minting the overlay the lock exists to prevent, while writes to + // non-packaged names keep the documented hatch behavior. One + // spelling of "package-declared", two doors, one refusal. + registerPackagedPermissionSetLockGate(protocol, ql); // [ADR-0094 D4] Converge record ↔ metadata: project env overlays // onto records (creating missing ones), backfill legacy data-door // creations into the metadata store once, and heal drifted records diff --git a/packages/plugins/plugin-security/vitest.config.ts b/packages/plugins/plugin-security/vitest.config.ts index 2dd5591aa7..b4042e2092 100644 --- a/packages/plugins/plugin-security/vitest.config.ts +++ b/packages/plugins/plugin-security/vitest.config.ts @@ -54,6 +54,16 @@ export default defineConfig({ find: /^@objectstack\/types$/, replacement: path.resolve(__dirname, '../../types/src/index.ts'), }, + // [#11843] `packaged-permission-set-lock-gate.test.ts` drives the REAL + // `ObjectStackProtocolImplementation` (its `saveMetaItem` seam is the + // door under test), so the protocol must be read from the producer's + // source in this checkout — a stale `dist/` would run the pin against a + // seam that predates `registerAuthoringGate('permission', ...)` and + // report a verdict about the last build rather than about this tree. + { + find: /^@objectstack\/metadata-protocol$/, + replacement: path.resolve(__dirname, '../../metadata-protocol/src/index.ts'), + }, ], }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a21df8d4d4..9c5edf092a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1758,6 +1758,9 @@ importers: '@objectstack/driver-sql': specifier: workspace:* version: link:../../drivers/driver-sql + '@objectstack/metadata-protocol': + specifier: workspace:* + version: link:../../metadata-protocol '@objectstack/objectql': specifier: workspace:* version: link:../../objectql diff --git a/scripts/engine-double-contract.pinned.json b/scripts/engine-double-contract.pinned.json index 1f9541c944..3eb042f91a 100644 --- a/scripts/engine-double-contract.pinned.json +++ b/scripts/engine-double-contract.pinned.json @@ -1456,6 +1456,16 @@ "verb": "update", "pinned": 1 }, + { + "file": "packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.test.ts", + "verb": "delete", + "pinned": 1 + }, + { + "file": "packages/plugins/plugin-security/src/packaged-permission-set-lock-gate.test.ts", + "verb": "update", + "pinned": 1 + }, { "file": "packages/plugins/plugin-security/src/packaged-permission-set-lock.test.ts", "verb": "delete",