diff --git a/.changeset/23972-installed-package-source.md b/.changeset/23972-installed-package-source.md new file mode 100644 index 00000000000..6365d0bd9df --- /dev/null +++ b/.changeset/23972-installed-package-source.md @@ -0,0 +1,47 @@ +--- +'@objectstack/spec': minor +'@objectstack/objectql': minor +'@objectstack/metadata-protocol': patch +'@objectstack/runtime': patch +'@objectstack/service-package': patch +'@objectstack/cloud-connection': patch +'@objectstack/client': patch +--- + +feat(spec)!: every installed-package row declares where it came from, a closed `source: 'code' | 'marketplace' | 'authored'`, and the `GET /packages` rows declare the `writable` verdict they already served (#23972) + +Clause-②: yes (narrowing — the served row gains two declared keys, and the record schema, the served-row union and the registry's installPackage signature each refuse what they accepted before) + +**BREAKING** — three published accept sets narrow. `InstalledPackageSchema` refuses a row without `source`, or with a value outside `code` / `marketplace` / `authored`. The served-row union `InstalledPackageAtEitherStageSchema` refuses a row without a boolean `writable`. `SchemaRegistry.installPackage` and `EngineSchemaRegistryView.installPackage` take `source` as their second argument and refuse a call without it. The migration is below. None of it is mechanically convertible, because only the door that registers a package knows where it came from. + +**What is new on the wire.** `GET /api/v1/packages` and `GET /api/v1/packages/:id` serve two declared keys on every row: + +- `source` names the package's origin. `code` means the deployment's own code registered it (a `defineStack()` host, a plugin's bundled manifest, a platform package). `marketplace` means the marketplace install-local plugin installed it, and that holds after a restart too, when the plugin rehydrates it from its ledger. `authored` means it is a database-authored package (a Studio base, a duplicate, an HTTP install), including after the restart that rehydrates it from `sys_packages`. The door that registers the package stamps the value. No reader derives it. +- `writable` is the server's own writability verdict (ADR-0070 D2), served on these rows since #14375 and now declared. It is computed per read and never stored, so it is declared on the served row (`InstalledPackageAtEitherStageSchema`) and not on the registry record. + +FROM → TO for a reader of these rows: + +| you did | do instead | +| --- | --- | +| treated `writable: true` as authored, and looked up each non-writable row in the marketplace's installed list (the control plane's, else install-local's) to tell marketplace from code | read `row.source` | +| read the undeclared `writable` | read the declared `writable` (unchanged value) | + +The two keys are separate statements: `source` says where the package came from, and `writable` says what this server lets an org do to it. + +**`InstalledPackageSchema` requires `source`.** It is `InstalledPackageSourceSchema`, exported from `@objectstack/spec/kernel`. A row without it, or with a value outside the closed set, is refused at the `source` key. + +**API change for code that registers packages (`@objectstack/objectql`).** The registry records the origin and never infers it: + +| you wrote | write instead | +| --- | --- | +| `registry.installPackage(manifest)` | `registry.installPackage(manifest, 'code')` (or `'marketplace'` / `'authored'`, whichever is where the package came from) | +| `registry.installPackage(manifest, settings, scope)` | `registry.installPackage(manifest, source, settings, scope)` | +| `ctx.getService('manifest').register(artifact)` for a marketplace install | `ctx.getService('manifest').register(artifact, { source: 'marketplace' })` | + +`SchemaRegistry.installPackage` throws a `TypeError` naming the closed set when `source` is missing or unknown, before anything is recorded. `ObjectQL.registerApp(body, scope?, source?)` and the `manifest` service record `'code'` when no source is given. They accept `'code'` or `'marketplace'` and refuse `'authored'`. The `IObjectQLEngine` registry contract (`@objectstack/spec/contracts`) declares the same `installPackage(manifest, source, settings?)`. + +**The one-line fix:** pass the package's origin as `installPackage`'s second argument. + +Readers: objectui's Setup → Packages Source column and its `?source=` filter (it switches from the two-read join to `source` in its own follow-up), and the client SDK's `packages.list()` / `packages.get()` row types. + + diff --git a/content/docs/references/api/package-api-assembled.mdx b/content/docs/references/api/package-api-assembled.mdx index a9b2fb305ab..a60394e2249 100644 --- a/content/docs/references/api/package-api-assembled.mdx +++ b/content/docs/references/api/package-api-assembled.mdx @@ -67,6 +67,7 @@ Installed package row whose manifest is the assembled package body | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -166,7 +167,7 @@ Get installed package response | **success** | `boolean` | ✅ | Operation success status | | **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: integer; requestId?: string; traceId?: string }` | optional | Response metadata | -| **data** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … } \| … +1 more` | ✅ | Installed package details | +| **data** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … } \| … +1 more` | ✅ | Installed package details | ### Nested Shape: `GetInstalledPackageResponse.error` @@ -193,11 +194,10 @@ Get installed package response ### Nested Shape: `GetInstalledPackageResponse.data[option 1]` -Installed package with runtime lifecycle state - | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -209,14 +209,14 @@ Installed package with runtime lifecycle state | **settings** | `Record` | optional | User-provided configuration settings | | **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | | **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | +| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored | ### Nested Shape: `GetInstalledPackageResponse.data[option 2]` -Installed package row whose manifest is the assembled package body - | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -228,6 +228,7 @@ Installed package row whose manifest is the assembled package body | **settings** | `Record` | optional | User-provided configuration settings | | **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | | **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | +| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored | --- @@ -242,13 +243,12 @@ This schema accepts one of the following structures: #### Option 1 -Installed package with runtime lifecycle state - ### Properties | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -260,6 +260,7 @@ Installed package with runtime lifecycle state | **settings** | `Record` | optional | User-provided configuration settings | | **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | | **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | +| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored | ### Nested Shape: `InstalledPackageAtEitherStage[option 1].manifest` @@ -306,13 +307,12 @@ Installed package with runtime lifecycle state #### Option 2 -Installed package row whose manifest is the assembled package body - ### Properties | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -324,6 +324,7 @@ Installed package row whose manifest is the assembled package body | **settings** | `Record` | optional | User-provided configuration settings | | **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history | | **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package | +| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored | ### Nested Shape: `InstalledPackageAtEitherStage[option 2].manifest` @@ -443,7 +444,7 @@ List installed packages response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **packages** | `({ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … } \| … +1 more)[]` | ✅ | Installed packages | +| **packages** | `({ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … } \| … +1 more)[]` | ✅ | Installed packages | | **total** | `integer` | optional | Total matching packages | | **nextCursor** | `string` | optional | Cursor for the next page | | **hasMore** | `boolean` | ✅ | Whether more packages are available — this door serves one page, so always `false` | diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx index 3e1807d279f..4bd03bc6a4c 100644 --- a/content/docs/references/api/package-api.mdx +++ b/content/docs/references/api/package-api.mdx @@ -383,7 +383,7 @@ Install package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Installed package details | | **dependencyResolution** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | optional | Dependency resolution result | | **namespaceConflicts** | `{ type: 'namespace_conflict'; requestedNamespace: string; conflictingPackageId: string; conflictingPackageName: string; … }[]` | optional | Namespace conflicts detected | | **message** | `string` | optional | Installation status message | diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 15b9df0e4c9..84428a0c50a 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -816,7 +816,7 @@ Disable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Disabled package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Disabled package details | | **message** | `string` | optional | Disable status message | ### Nested Shape: `DisablePackageResponse.package` @@ -824,6 +824,7 @@ Disable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -860,7 +861,7 @@ Enable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Enabled package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Enabled package details | | **message** | `string` | optional | Enable status message | ### Nested Shape: `EnablePackageResponse.package` @@ -868,6 +869,7 @@ Enable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -1669,13 +1671,14 @@ Get package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Package details | ### Nested Shape: `GetPackageResponse.package` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -2105,7 +2108,7 @@ Install package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Installed package details | | **message** | `string` | optional | Installation status message | | **dependencyResolution** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | optional | Dependency resolution result from install analysis | @@ -2114,6 +2117,7 @@ Install package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -2308,7 +2312,7 @@ List packages response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **packages** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }[]` | ✅ | List of installed packages | +| **packages** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }[]` | ✅ | List of installed packages | | **total** | `number` | ✅ | Total package count | ### Nested Shape: `ListPackagesResponse.packages[number]` @@ -2318,6 +2322,7 @@ Installed package with runtime lifecycle state | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 6fddd3beadd..56b5643955b 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1537 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1538 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -26,7 +26,7 @@ counts are sums of the rows they head. Regenerate with | [Data Protocol](/docs/references/data) | 29 | 176 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | | [Integration Protocol](/docs/references/integration) | 1 | 12 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. | -| [Kernel Protocol](/docs/references/kernel) | 30 | 160 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | +| [Kernel Protocol](/docs/references/kernel) | 30 | 161 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | | [Marketplace Protocol](/docs/references/marketplace) | 4 | 30 | The package & marketplace format — package identity and versions, listing, publish, review, search, install, template manifests. | | [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. | | [Security Protocol](/docs/references/security) | 5 | 30 | Permission sets, row-level security, sharing rules, tenancy posture. | @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 36 | 284 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 169 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **197** | **1537** | 14 protocol modules | +| **Total** | **197** | **1538** | 14 protocol modules | --- @@ -198,7 +198,7 @@ The single connector protocol (ADR-0097) — catalog descriptors and provider-bo ## Kernel Protocol -**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **30 pages, 160 schemas** +**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **30 pages, 161 schemas** Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. @@ -220,7 +220,7 @@ Plugin lifecycle and manifests, capabilities and security, metadata loading, ser | [`metadata-plugin.zod.ts`](/docs/references/kernel/metadata-plugin) | `MetadataBulkResult`, `MetadataDependency`, `MetadataPluginConfig`, `MetadataPluginManifest`, `MetadataQuery`, `MetadataQueryResult`, `MetadataType`, `MetadataTypeRegistryEntry`, `MetadataValidationResult` | | [`metadata-protection.zod.ts`](/docs/references/kernel/metadata-protection) | `MetadataLock`, `MetadataLockSource`, `MetadataProvenance` | | [`package-artifact.zod.ts`](/docs/references/kernel/package-artifact) | `ArtifactChecksum`, `ArtifactFileEntry`, `ArtifactSignature`, `MetadataCategoryEnum`, `PackageArtifact` | -| [`package-registry.zod.ts`](/docs/references/kernel/package-registry) | `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `GetPackageRequest`, `GetPackageResponse`, `InstallPackageRequest`, `InstallPackageResponse`, `InstalledPackage`, `ListPackagesRequest`, `ListPackagesResponse`, `NamespaceConflictError`, `NamespaceRegistryEntry`, `PackageStatusEnum`, `UninstallPackageRequest`, `UninstallPackageResponse` | +| [`package-registry.zod.ts`](/docs/references/kernel/package-registry) | `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `GetPackageRequest`, `GetPackageResponse`, `InstallPackageRequest`, `InstallPackageResponse`, `InstalledPackage`, `InstalledPackageSource`, `ListPackagesRequest`, `ListPackagesResponse`, `NamespaceConflictError`, `NamespaceRegistryEntry`, `PackageStatusEnum`, `UninstallPackageRequest`, `UninstallPackageResponse` | | [`package-upgrade.zod.ts`](/docs/references/kernel/package-upgrade) | `MetadataChangeType`, `MetadataDiffItem`, `RollbackPackageRequest`, `RollbackPackageResponse`, `UpgradeImpactLevel`, `UpgradePackageRequest`, `UpgradePackageResponse`, `UpgradePhase`, `UpgradePlan`, `UpgradeSnapshot` | | [`plugin.zod.ts`](/docs/references/kernel/plugin) | `Plugin` | | [`plugin-capability.zod.ts`](/docs/references/kernel/plugin-capability) | `CapabilityConformanceLevel`, `ExtensionPoint`, `PluginCapability`, `PluginCapabilityManifest`, `PluginDependency`, `PluginInterface`, `ProtocolFeature`, `ProtocolReference`, `ProtocolVersion` | diff --git a/content/docs/references/kernel/package-registry.mdx b/content/docs/references/kernel/package-registry.mdx index d88a0b885f3..866ee3c7834 100644 --- a/content/docs/references/kernel/package-registry.mdx +++ b/content/docs/references/kernel/package-registry.mdx @@ -35,8 +35,8 @@ Defines the runtime state and lifecycle operations for installed packages. ## TypeScript Usage ```typescript -import { DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, InstalledPackageSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, NamespaceConflictErrorSchema, NamespaceRegistryEntrySchema, PackageStatusEnum, UninstallPackageRequestSchema, UninstallPackageResponseSchema } from '@objectstack/spec/kernel'; -import type { DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, GetPackageRequest, GetPackageResponse, InstallPackageRequest, InstallPackageResponse, InstalledPackage, ListPackagesRequest, ListPackagesResponse, NamespaceConflictError, NamespaceRegistryEntry, UninstallPackageRequest, UninstallPackageResponse } from '@objectstack/spec/kernel'; +import { DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, InstalledPackageSchema, InstalledPackageSourceSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, NamespaceConflictErrorSchema, NamespaceRegistryEntrySchema, PackageStatusEnum, UninstallPackageRequestSchema, UninstallPackageResponseSchema } from '@objectstack/spec/kernel'; +import type { DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, GetPackageRequest, GetPackageResponse, InstallPackageRequest, InstallPackageResponse, InstalledPackage, InstalledPackageSource, ListPackagesRequest, ListPackagesResponse, NamespaceConflictError, NamespaceRegistryEntry, UninstallPackageRequest, UninstallPackageResponse } from '@objectstack/spec/kernel'; // Validate data const result = DisablePackageRequestSchema.parse(data); @@ -65,7 +65,7 @@ Disable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Disabled package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Disabled package details | | **message** | `string` | optional | Disable status message | ### Nested Shape: `DisablePackageResponse.package` @@ -73,6 +73,7 @@ Disable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -109,7 +110,7 @@ Enable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Enabled package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Enabled package details | | **message** | `string` | optional | Enable status message | ### Nested Shape: `EnablePackageResponse.package` @@ -117,6 +118,7 @@ Enable package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -153,13 +155,14 @@ Get package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Package details | ### Nested Shape: `GetPackageResponse.package` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -230,7 +233,7 @@ Install package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **package** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }` | ✅ | Installed package details | +| **package** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }` | ✅ | Installed package details | | **message** | `string` | optional | Installation status message | | **dependencyResolution** | `{ dependencies: object[]; canProceed: boolean; requiredActions: object[]; installOrder: string[]; … }` | optional | Dependency resolution result from install analysis | @@ -239,6 +242,7 @@ Install package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -273,6 +277,7 @@ Installed package with runtime lifecycle state | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | @@ -327,6 +332,19 @@ Installed package with runtime lifecycle state | **migrationLog** | `string[]` | optional | Migration step logs | +--- + +## InstalledPackageSource + +Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) + +### Allowed Values + +* `code` +* `marketplace` +* `authored` + + --- ## ListPackagesRequest @@ -352,7 +370,7 @@ List packages response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **packages** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … }[]` | ✅ | List of installed packages | +| **packages** | `{ manifest: object; source: Enum<'code' \| 'marketplace' \| 'authored'>; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; … }[]` | ✅ | List of installed packages | | **total** | `number` | ✅ | Total package count | ### Nested Shape: `ListPackagesResponse.packages[number]` @@ -362,6 +380,7 @@ Installed package with runtime lifecycle state | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` | +| **source** | `Enum<'code' \| 'marketplace' \| 'authored'>` | ✅ | Where the installed package came from: code (registered by the deployment's own code), marketplace (installed from the marketplace), or authored (a database-authored package) | | **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error | | **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled | | **installedAt** | `string` | optional | Installation timestamp | diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 9e5f5301958..9fb7dea55de 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -2582,11 +2582,16 @@ export class ObjectStackClient { * SAME envelope for this route, which is what makes it bindable: * `runtime`'s `/packages` domain sends `success({ packages, total: * packages.length })` and `rest`'s `GET {base}/packages` sends - * `sendOk(res, { packages, total: packages.length })`. The REST rows carry - * an extra `source: 'database' | 'registry' | 'both'` discriminator that is - * deliberately NOT declared here — the dispatcher rows have no such key, so - * declaring it would be false on that surface. #8140 bound the identical - * scoped sibling (`ScopedEnvironmentClient.packages.list`) the same way. + * `sendOk(res, { packages, total: packages.length })`. #8140 bound the + * identical scoped sibling (`ScopedEnvironmentClient.packages.list`) the + * same way. (The REST rows' `source: 'database' | 'registry' | 'both'` + * stamp left with those routes in #14503.) + * + * [#23972] Every row carries two declared keys besides the record's own: + * `source` (`'code' | 'marketplace' | 'authored'`, where the package came + * from, stamped by the door that registered it) and `writable` (the + * server's writability verdict, computed per read). Read the origin from + * `source`; ⛔ do not reconstruct it from `writable` or from a second list. * * ⚠️ [#17536] The ROW type moved, the envelope did not. #11925 bound the * element to `InstalledPackage` — the AUTHORING stage — and PR #17517 then @@ -2697,9 +2702,8 @@ export class ObjectStackClient { * manifest stages. It was a COMMENT and never a binding, which is why the * disagreement it describes could sit here unread by any gate. * - * `source` stays undeclared because there is no longer anything that - * emits it on this route; `writable` stays undeclared for the reason - * `list` leaves it undeclared. + * [#23972] The row carries the declared `source` and `writable`, as on + * `list`. * * ⚠️ Clause-② narrowing (#12034). `{ package: any }` is what let * `(await client.packages.get(id)).package` compile, and on the only diff --git a/packages/client/src/packages-write-envelope.test.ts b/packages/client/src/packages-write-envelope.test.ts index f1cb9d8ad5d..f53f39310b7 100644 --- a/packages/client/src/packages-write-envelope.test.ts +++ b/packages/client/src/packages-write-envelope.test.ts @@ -178,7 +178,7 @@ describe('#12034 — the packages write verbs answer the bare row, not `{ packag it('enable resolves to the row, carrying the flag it just set', async () => { const registry = new SchemaRegistry(); - registry.installPackage(MANIFEST as any); + registry.installPackage(MANIFEST as any, 'authored'); registry.disablePackage(MANIFEST.id); const { client, seen } = dispatcherBackedClient(registry); @@ -195,7 +195,7 @@ describe('#12034 — the packages write verbs answer the bare row, not `{ packag it('disable resolves to the row, carrying the flag it just cleared', async () => { const registry = new SchemaRegistry(); - registry.installPackage(MANIFEST as any); + registry.installPackage(MANIFEST as any, 'authored'); const { client, seen } = dispatcherBackedClient(registry); const row = await client.packages.disable(MANIFEST.id); @@ -216,7 +216,7 @@ describe('#12034 — the packages write verbs answer the bare row, not `{ packag */ it('the dispatcher wraps the row exactly once, and the client strips exactly that', async () => { const registry = new SchemaRegistry(); - registry.installPackage(MANIFEST as any); + registry.installPackage(MANIFEST as any, 'authored'); const { client, dispatcher } = dispatcherBackedClient(registry); const raw = await dispatcher.handlePackages('/com.acme.crm/enable', 'PATCH', undefined, {}, CONTEXT()); diff --git a/packages/client/src/return-type-precision.test.ts b/packages/client/src/return-type-precision.test.ts index dae16a6812b..e5819f272ff 100644 --- a/packages/client/src/return-type-precision.test.ts +++ b/packages/client/src/return-type-precision.test.ts @@ -307,9 +307,9 @@ export function searchResultIsNotTheGlobalSearchShape(): void { export async function returnTypePrecisionPins11925(): Promise { // ── bound 1/3: both surfaces agree on this envelope ────────────────── // `runtime`'s `/packages` domain and `rest`'s `GET {base}/packages` both - // send `{ packages, total: packages.length }`. The REST rows also carry a - // `source` discriminator the dispatcher rows lack, so it stays undeclared - // — the same treatment #8140 gave the scoped sibling directly above. + // send `{ packages, total: packages.length }`. (The REST twin's + // `source: 'registry' | 'database' | 'both'` stamp left with its routes in + // #14503; the `source` the rows carry now is the declared origin, #23972.) // ⚠️ [#17536] The ELEMENT moved, the envelope did not — this pin is still // #11925's statement that both mounted surfaces agree on // `{ packages, total }`, and that half is unchanged. @@ -645,8 +645,10 @@ export async function returnTypePrecisionPins12034(): Promise { void (await scoped.packages.get('com.acme.crm')).package; } -declare const authoringRow: InstalledPackage; -declare const assembledRow: AssembledInstalledPackage; +// A SERVED row is the registry record plus the read door's own verdict, +// `writable` (#23972 declared it on the served row, not on the record). +declare const authoringRow: InstalledPackage & { writable: boolean }; +declare const assembledRow: AssembledInstalledPackage & { writable: boolean }; /** * [#17536] The four `/packages` READ members declare the stage the DOOR is diff --git a/packages/cloud-connection/src/marketplace-install-local-hot-loaded.test.ts b/packages/cloud-connection/src/marketplace-install-local-hot-loaded.test.ts index f245a64ece8..09c925f263b 100644 --- a/packages/cloud-connection/src/marketplace-install-local-hot-loaded.test.ts +++ b/packages/cloud-connection/src/marketplace-install-local-hot-loaded.test.ts @@ -63,7 +63,7 @@ async function mount(dir: string, opts: { controlPlaneUrl?: string; failRegister const rawApp = makeRawApp(); const hooks = new Map(); const registry: any[] = []; - const register = vi.fn(async (m: any) => { + const register = vi.fn(async (m: any, _options?: { source?: string }) => { if (opts.failRegisterOf !== undefined && m?.id === opts.failRegisterOf) { throw new Error(REGISTER_FAILURE); } @@ -95,6 +95,9 @@ async function mount(dir: string, opts: { controlPlaneUrl?: string; failRegister logger, /** The manifest ids the install ASKED to register, whether or not the register resolved. */ installRegisterAttempts: () => register.mock.calls.slice(registeredAtBoot).map((call) => call[0]?.id), + /** Every register call as `[manifest id, source stated]`, boot included. */ + registerSources: () => register.mock.calls.map((call: any[]) => [call[0]?.id, call[1]?.source]), + registeredAtBoot, }; } @@ -192,3 +195,24 @@ describe('install-local answers hotLoaded from what the hot-register did', () => expect(readdirSync(dir)).toEqual([]); }); }); + +// [#23972] The registry row of a marketplace install says where it came from: +// the plugin states `source: 'marketplace'` on the hot-register at install AND +// on the boot rehydrate from its ledger, so a restart never reads it back as +// code. Its own UI bundle is the plugin's code and states nothing (`'code'`). +describe('install-local registers its packages as source marketplace', () => { + it('states marketplace on the install register and on the rehydrate after a restart', async () => { + const first = await mount(dir); + const res = await first.install({ manifest: MANIFEST }); + expect(res.status, JSON.stringify(res.payload)).toBe(200); + expect(first.registerSources().slice(first.registeredAtBoot)).toEqual([[APP_ID, 'marketplace']]); + + // A second boot over the same ledger: the rehydrate is the restart half. + const second = await mount(dir); + const atBoot = second.registerSources().slice(0, second.registeredAtBoot); + expect(atBoot.filter(([id]) => id === APP_ID)).toEqual([[APP_ID, 'marketplace']]); + // CONTROL: the plugin's own UI bundle registered at boot without a source. + expect(atBoot.filter(([id]) => id !== APP_ID).every(([, source]) => source === undefined)).toBe(true); + expect(atBoot.length).toBeGreaterThan(1); + }); +}); diff --git a/packages/cloud-connection/src/marketplace-install-local-plugin.ts b/packages/cloud-connection/src/marketplace-install-local-plugin.ts index 7770ed0fd59..1e6d446df04 100644 --- a/packages/cloud-connection/src/marketplace-install-local-plugin.ts +++ b/packages/cloud-connection/src/marketplace-install-local-plugin.ts @@ -740,7 +740,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin { if (entries.length === 0) return; - let manifestService: { register(m: any): void | Promise } | null = null; + let manifestService: { register(m: any, options?: { source?: 'code' | 'marketplace' }): void | Promise } | null = null; try { manifestService = ctx.getService('manifest') as any; } catch { @@ -777,7 +777,10 @@ export class MarketplaceInstallLocalPlugin implements Plugin { // the metadata service (late-registration bridge in // ObjectQLPlugin) — wait for that so metadata consumers see // the package as soon as rehydrate reports success. - await manifestService!.register(entry.manifest); + // [#23972] Every ledger entry is a marketplace install, so its + // registry row says so (`source: 'marketplace'`) after a restart + // exactly as it did when it was installed. + await manifestService!.register(entry.manifest, { source: 'marketplace' }); // Sync schemas so the driver creates tables for the newly- // registered objects (idempotent — already-synced tables // are no-ops). @@ -1551,7 +1554,9 @@ export class MarketplaceInstallLocalPlugin implements Plugin { // Awaited: register also bridges the manifest's objects into the // metadata service — a caller that reads metadata right after a // 200 (AI describe_object, Studio object list) must see them. - await manifestService.register(manifest); + // [#23972] The registry row records where this package came + // from: the marketplace (`source: 'marketplace'`). + await manifestService.register(manifest, { source: 'marketplace' }); } catch (err: any) { const registerError = String(err?.message ?? err); // For offline file imports we treat a register failure as a hard diff --git a/packages/metadata-protocol/src/durable-package.test.ts b/packages/metadata-protocol/src/durable-package.test.ts index af28eb91926..f2f354266a9 100644 --- a/packages/metadata-protocol/src/durable-package.test.ts +++ b/packages/metadata-protocol/src/durable-package.test.ts @@ -51,19 +51,19 @@ function makeImpl(overrides?: { /** * Manifests to replay into the fresh registry before the test runs — a * stand-in for the boot hydration `PackageServicePlugin.start()` performs - * (`for (const rec of await packageService.list()) registry.installPackage(rec.manifest)`, + * (`for (const rec of await packageService.list()) registry.installPackage(rec.manifest, 'authored')`, * `packages/services/service-package/src/index.ts`). A host composed with * the rows a previous host persisted IS that process after a restart; a host * composed with none is a restart that found `sys_packages` empty. */ hydrate?: readonly { id: string; [key: string]: unknown }[]; }) { - const registryCalls: Array<{ manifest: any; settings: any }> = []; + const registryCalls: Array<{ manifest: any; source: string; settings: any }> = []; const installed = new Map(); const registry = { - installPackage: (manifest: any, settings?: any) => { - registryCalls.push({ manifest, settings }); - const pkg = { manifest, status: 'installed', enabled: true }; + installPackage: (manifest: any, source: string, settings?: any) => { + registryCalls.push({ manifest, source, settings }); + const pkg = { manifest, source, status: 'installed', enabled: true }; installed.set(manifest.id, pkg); return pkg; }, @@ -78,7 +78,7 @@ function makeImpl(overrides?: { }; const engine = { registry, find: overrides?.find ?? (async () => []) }; // Seeded through the registry's own verb, exactly as the boot hydration does. - for (const manifest of overrides?.hydrate ?? []) registry.installPackage(manifest); + for (const manifest of overrides?.hydrate ?? []) registry.installPackage(manifest, 'authored'); registryCalls.length = 0; // hydration is the fixture, not an observation const publish = vi.fn(overrides?.publish ?? (async () => ({ success: true }))); diff --git a/packages/metadata-protocol/src/package-unmanaged-install.test.ts b/packages/metadata-protocol/src/package-unmanaged-install.test.ts index afa8f55f6b1..5c8a34122d7 100644 --- a/packages/metadata-protocol/src/package-unmanaged-install.test.ts +++ b/packages/metadata-protocol/src/package-unmanaged-install.test.ts @@ -824,7 +824,7 @@ describe('importUnmanagedPackage — the verb', () => { it('refuses a target id that names an installed package — never into an existing base', async () => { const { protocol, engine, writes } = makeProtocol(); - engine.registry.installPackage({ id: 'com.example.mine', name: 'Mine' }); + engine.registry.installPackage({ id: 'com.example.mine', name: 'Mine' }, 'code'); const err = await refusalOf(() => protocol.importUnmanagedPackage({ manifest: crmBody(), targetPackageId: 'com.example.mine', })); diff --git a/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts b/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts index a7a677ac296..14ddf38831b 100644 --- a/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts +++ b/packages/metadata-protocol/src/protocol.package-delete-refusal.test.ts @@ -110,8 +110,8 @@ const thrownDatabaseError: StoreDelete = async () => { function boot(world: World, storeDelete: StoreDelete = landed, opts: { uninstallRefusal?: Error } = {}) { const rows = new Map; status: string; enabled: boolean }>(); const registry = { - installPackage(manifest: Record) { - const row = { manifest: { ...manifest }, status: 'installed', enabled: true }; + installPackage(manifest: Record, source: string) { + const row = { manifest: { ...manifest }, source, status: 'installed', enabled: true }; rows.set(String(manifest.id), row); return row; }, @@ -129,7 +129,7 @@ function boot(world: World, storeDelete: StoreDelete = landed, opts: { uninstall }, }; // The boot hydration: every stored row is registered. - for (const manifest of world.sysPackages.values()) registry.installPackage(manifest); + for (const manifest of world.sysPackages.values()) registry.installPackage(manifest, 'authored'); const engine = { registry, diff --git a/packages/metadata-protocol/src/protocol.package-persist-failure.test.ts b/packages/metadata-protocol/src/protocol.package-persist-failure.test.ts index e7c2da04170..b570f087b10 100644 --- a/packages/metadata-protocol/src/protocol.package-persist-failure.test.ts +++ b/packages/metadata-protocol/src/protocol.package-persist-failure.test.ts @@ -46,6 +46,7 @@ import { ObjectStackProtocolImplementation } from './index.js'; interface Row { manifest: Record; + source: string; status: string; enabled: boolean; installedAt: string; @@ -61,10 +62,11 @@ function makeRegistry() { return { rows, namespaces, - installPackage(manifest: any, settings?: unknown): Row { + installPackage(manifest: any, source: string, settings?: unknown): Row { const existing = rows.get(manifest.id); const row: Row = { manifest: { ...manifest }, + source, status: existing?.status ?? 'installed', enabled: existing?.enabled ?? true, installedAt: stamp(), @@ -171,7 +173,7 @@ describe('#21243 installPackage — a refused sys_packages write fails the insta it('a re-install over an existing row puts the PRIOR row back, lifecycle included', async () => { const { impl, registry } = makeImpl(returnedDriverFault); - const prior = registry.installPackage({ id: 'com.example.leave', name: 'Leave v1', version: '1.0.0', namespace: 'leave' }); + const prior = registry.installPackage({ id: 'com.example.leave', name: 'Leave v1', version: '1.0.0', namespace: 'leave' }, 'authored'); registry.disablePackage('com.example.leave'); const priorContent = JSON.parse(JSON.stringify(registry.getPackage('com.example.leave'))); @@ -221,7 +223,7 @@ describe('#21243 updatePackage — a refused sys_packages write fails the edit a ['thrown DATABASE_ERROR', 'DATABASE_ERROR', thrownDatabaseError], ] as const)('%s → 500 %s, the prior manifest back in place', async (_label, code, publish) => { const { impl, registry } = makeImpl(publish); - const row = registry.installPackage({ id: 'com.example.leave', name: 'Leave', version: '1.0.0' }); + const row = registry.installPackage({ id: 'com.example.leave', name: 'Leave', version: '1.0.0' }, 'authored'); const manifestObject = row.manifest; const priorContent = JSON.parse(JSON.stringify(row)); @@ -243,7 +245,7 @@ describe('#21243 updatePackage — a refused sys_packages write fails the edit a it('CONTROL — a landed write answers the edited row', async () => { const { impl, registry } = makeImpl(async () => ({ success: true })); - registry.installPackage({ id: 'com.example.leave', name: 'Leave', version: '1.0.0' }); + registry.installPackage({ id: 'com.example.leave', name: 'Leave', version: '1.0.0' }, 'authored'); const res = await impl.updatePackage({ packageId: 'com.example.leave', patch: { description: 'patched' } }); diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 74fb23d7fcf..ead219b8790 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -31390,7 +31390,9 @@ export class ObjectStackProtocolImplementation implements // install REPLACES the row object (and may register the namespace). const undoInstall = this.packageInstallUndo(manifest); - let pkg = this.engine.registry.installPackage(manifest as any, request.settings); + // [#23972] Every package this primitive installs is AUTHORED — a Studio + // base, a duplicate, an HTTP install — persisted to `sys_packages` below. + let pkg = this.engine.registry.installPackage(manifest as any, 'authored', request.settings); // [#19277] HONOUR `enableOnInstall` — the key THIS request contract // declares and this primitive read past. `InstallPackageRequestSchema` diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index d45112dd186..337b4843974 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -98,7 +98,7 @@ import { resolveBundleLocale, SystemFieldName, } from '@objectstack/spec/system'; -import { ExecutionContext, ExecutionContextSchema } from '@objectstack/spec/kernel'; +import { ExecutionContext, ExecutionContextSchema, type InstalledPackageSource } from '@objectstack/spec/kernel'; // [#21120] The family-wide stored-metadata-body seam. A `data.record.*` event // carries the written row in `after` (and the input patch in `changes`), so a // subscriber to `sys_metadata` / `sys_metadata_history` events would receive @@ -7863,10 +7863,30 @@ export class ObjectQL implements IObjectQLEngine { * * Key: Package ≠ App. The manifest is the package. The apps[] array inside * the manifest contains UI navigation definitions (AppSchema). + * + * [#23972] `source` is where the package came from, stamped on its registry + * row (`InstalledPackageSchema.source`). This is the door the deployment's + * own code registers through — a `defineStack()` host, a plugin's bundled + * manifest, a platform package — so `'code'` is what the door means when the + * caller says nothing. The one other producer on this door, the marketplace + * install-local plugin, states `'marketplace'` through the `manifest` + * service's `register(artifact, { source })`. An authored package never + * comes through here (it is installed by `installPackage` in + * `@objectstack/metadata-protocol`), so `'authored'` is not accepted. */ - registerApp(registeredBody: any, scope?: ArtifactInstallScope) { + registerApp(registeredBody: any, scope?: ArtifactInstallScope, source: Exclude = 'code') { // The CONVERTED body, for everything below — the package record, the // live manifest, every item it registers (see {@link convertPackageBodyForward}). + // [#23972] Refused before anything registers — see the docblock: an + // untyped caller handing this door `'authored'` (or any other value) + // would record a row whose origin contradicts the read-only verdict + // this door's `manifests` entry produces. + if (source !== 'code' && source !== 'marketplace') { + throw new TypeError( + `[ObjectQL] registerApp: source must be 'code' or 'marketplace'; received ${JSON.stringify(source)}. ` + + `An authored package is installed through the protocol's installPackage, not registered here.`, + ); + } const manifest = this.convertPackageBodyForward(registeredBody); const id = manifest.id || manifest.name; const namespace = manifest.namespace as string | undefined; @@ -7899,7 +7919,7 @@ export class ObjectQL implements IObjectQLEngine { // tell a CO-OWNER (another package from this same artifact) from a // stranger — the question D1 corrected the gate to ask. Absent for every // single-package caller, where the gate behaves exactly as before (D7). - this._registry.installPackage(manifest, undefined, scope); + this._registry.installPackage(manifest, source, undefined, scope); this.logger.debug('Installed Package', { id: manifest.id, name: manifest.name, namespace }); // 2. Register owned objects diff --git a/packages/objectql/src/package-disable-enforcement.test.ts b/packages/objectql/src/package-disable-enforcement.test.ts index 5048415d737..f26100cf360 100644 --- a/packages/objectql/src/package-disable-enforcement.test.ts +++ b/packages/objectql/src/package-disable-enforcement.test.ts @@ -174,7 +174,7 @@ async function seed(protocol: any, registry: SchemaRegistry) { [PKG, 'demo_widget', 'demo_app'], [OTHER_PKG, 'healthy_widget', 'healthy_app'], ] as const) { - registry.installPackage({ id: pkg, name: pkg, version: '1.0.0' } as any); + registry.installPackage({ id: pkg, name: pkg, version: '1.0.0' } as any, 'code'); await protocol.saveMetaItem({ type: 'object', name: obj, packageId: pkg, item: objectBody(obj) }); // `app` rather than `view` as the non-object specimen: a `view` is saved as // an ADR-0017 CONTAINER and only becomes independently listable once the diff --git a/packages/objectql/src/package-source-stamp.test.ts b/packages/objectql/src/package-source-stamp.test.ts new file mode 100644 index 00000000000..5909a9645dd --- /dev/null +++ b/packages/objectql/src/package-source-stamp.test.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Every installed-package row carries `source` — where the package came from — + * stamped by the door that registered it (`InstalledPackageSchema.source`, + * #23972; the closed set is ruling B on #23892: code / marketplace / authored). + * + * This file holds the engine's two doors onto the package table: + * + * - the `manifest` service → `ObjectQL.registerApp`: the deployment's own code + * registers through it, so a caller that says nothing records `'code'`; the + * marketplace install-local plugin states `{ source: 'marketplace' }`, and + * every package of a multi-package artifact it hands over carries it; + * - `SchemaRegistry.installPackage`, the record seam itself: it takes the + * origin as a required argument and refuses a call without one, or with a + * value outside the set, before anything is recorded. + * + * The authored door (`installPackage` in `@objectstack/metadata-protocol`) is + * pinned beside it, in `protocol-install-package-enable-on-install.test.ts`. + */ + +import { describe, it, expect, afterEach } from 'vitest'; +import { ObjectKernel, type Plugin, type PluginContext } from '@objectstack/core'; +import { ObjectQLPlugin } from './plugin.js'; +import type { ObjectQL } from './engine.js'; +import { SchemaRegistry } from './registry.js'; + +type ManifestService = { register(m: unknown, options?: { source?: 'code' | 'marketplace' }): void | Promise }; + +const CODE_ID = 'com.example.source.code'; +const MARKET_ID = 'com.example.source.market'; +const BUNDLE_A = 'com.example.source.bundle'; +const BUNDLE_B = 'com.example.source.bundle.extra'; + +const body = (id: string, namespace: string) => ({ id, name: namespace, namespace, version: '1.0.0', type: 'app' }); + +const kernels: ObjectKernel[] = []; + +/** Boot a kernel whose one plugin registers `steps` through the `manifest` service in `init()`. */ +async function boot(steps: Array<[unknown, { source?: 'code' | 'marketplace' } | undefined]>): Promise { + const kernel = new ObjectKernel({ logger: { level: 'silent' }, gracefulShutdown: false }); + kernels.push(kernel); + await kernel.use(new ObjectQLPlugin()); + await kernel.use({ + name: 'test.package-source.registrar', + dependencies: ['com.objectstack.engine.objectql'], + init: async (ctx: PluginContext) => { + const manifest = ctx.getService('manifest'); + for (const [artifact, options] of steps) await manifest.register(artifact, options); + }, + } as Plugin); + await kernel.bootstrap(); + return kernel.getService('objectql'); +} + +const sourceOf = (ql: ObjectQL, id: string) => (ql.registry.getPackage(id) as { source?: unknown } | undefined)?.source; + +afterEach(async () => { + while (kernels.length) { + const kernel = kernels.pop()!; + if (kernel.getState() === 'running') await kernel.shutdown(); + } +}); + +describe('the manifest service stamps the registering caller\'s source (#23972)', () => { + it('a package registered by code, with no source stated, records source code', async () => { + const ql = await boot([[body(CODE_ID, 'srccode'), undefined]]); + expect(sourceOf(ql, CODE_ID)).toBe('code'); + }); + + it('a package the marketplace registers records source marketplace', async () => { + const ql = await boot([ + [body(CODE_ID, 'srccode'), undefined], + [body(MARKET_ID, 'srcmarket'), { source: 'marketplace' }], + ]); + expect(sourceOf(ql, MARKET_ID)).toBe('marketplace'); + // CONTROL: the code package registered beside it keeps its own origin. + expect(sourceOf(ql, CODE_ID)).toBe('code'); + }); + + it('every package of a multi-package marketplace artifact records source marketplace', async () => { + const artifact = { + ...body(BUNDLE_A, 'srcbundle'), + packages: [{ manifest: body(BUNDLE_A, 'srcbundle') }, { manifest: body(BUNDLE_B, 'srcbundlex') }], + }; + const ql = await boot([[artifact, { source: 'marketplace' }]]); + expect([sourceOf(ql, BUNDLE_A), sourceOf(ql, BUNDLE_B)]).toEqual(['marketplace', 'marketplace']); + }); + + it('registerApp refuses `authored` — an authored package is installed, not registered here — and records nothing', async () => { + const ql = await boot([]); + expect(() => ql.registerApp(body(MARKET_ID, 'srcmarket'), undefined, 'authored' as never)).toThrow(TypeError); + expect(ql.registry.getPackage(MARKET_ID)).toBeUndefined(); + }); +}); + +describe('SchemaRegistry.installPackage requires the origin and refuses an unknown one (#23972)', () => { + it('records the source it is handed on the row', () => { + const registry = new SchemaRegistry({ logLevel: 'silent' } as never); + for (const [id, source] of [['com.a.one', 'code'], ['com.a.two', 'marketplace'], ['com.a.three', 'authored']] as const) { + expect(registry.installPackage({ id, name: id, version: '1.0.0', type: 'app' } as never, source).source).toBe(source); + } + }); + + it('refuses a call that states no source, or a value outside the closed set, and records no row', () => { + const registry = new SchemaRegistry({ logLevel: 'silent' } as never); + const manifest = { id: 'com.a.refused', name: 'refused', namespace: 'refused', version: '1.0.0', type: 'app' }; + expect(() => registry.installPackage(manifest as never, undefined as never)).toThrow(TypeError); + expect(() => registry.installPackage(manifest as never, 'cloud' as never)).toThrow(TypeError); + expect(registry.getPackage('com.a.refused')).toBeUndefined(); + // Refused ahead of every mutation: the namespace was not claimed either. + expect(registry.getNamespaceOwners('refused')).toEqual([]); + }); +}); diff --git a/packages/objectql/src/plugin.ts b/packages/objectql/src/plugin.ts index c4ec5086e02..28b1d035366 100644 --- a/packages/objectql/src/plugin.ts +++ b/packages/objectql/src/plugin.ts @@ -667,7 +667,12 @@ export class ObjectQLPlugin implements Plugin { // register/bridge interleave would bridge a body that the very next // package is about to change. For the single-package artifact this is the // same one register followed by the same one bridge as before (D7). - register: (registeredArtifact: any) => { + // + // [#23972] `options.source` is where every package in this artifact came + // from, stamped on each registry row (`InstalledPackageSchema.source`). + // Absent means `'code'`, the deployment's own code (`registerApp`'s + // default); the marketplace install-local plugin states `'marketplace'`. + register: (registeredArtifact: any, options?: { source?: 'code' | 'marketplace' }) => { // ADR-0087 D2 at the registration door, for an artifact that carries // `packages[]`: its bodies are strictly parsed by the ordering below // BEFORE `registerApp` could convert each one, so a bundle handed here @@ -734,7 +739,7 @@ export class ObjectQLPlugin implements Plugin { } for (const manifest of ordered) { - ql.registerApp(manifest, scope); + ql.registerApp(manifest, scope, options?.source); ctx.logger.debug('Manifest registered via manifest service', { id: manifest.id || manifest.name }); diff --git a/packages/objectql/src/protocol-boot-hydration-scoped.test.ts b/packages/objectql/src/protocol-boot-hydration-scoped.test.ts index b82baec221e..c218b792eef 100644 --- a/packages/objectql/src/protocol-boot-hydration-scoped.test.ts +++ b/packages/objectql/src/protocol-boot-hydration-scoped.test.ts @@ -316,10 +316,10 @@ describe('security catalog read — a name two packages ship (ADR-0131 D4, ruled // its in-memory registrar runs (Phase 2, `AppPlugin.start`). const stack = (id: string, label: string) => ({ id, name: id, version: '1.0.0', type: 'app', positions: [{ name: 'regional_manager', label }] }) as never; - registry.installPackage(stack(PKG_A, 'first stack')); + registry.installPackage(stack(PKG_A, 'first stack'), 'code'); metadata.registerInMemory('position', 'regional_manager', { name: 'regional_manager', label: 'first stack' }); - const refusal = refusalOf(() => registry.installPackage(stack(PKG_B, 'second stack'))); + const refusal = refusalOf(() => registry.installPackage(stack(PKG_B, 'second stack'), 'code')); expect(refusal?.code).toBe(NAMESPACE_CONFLICT_CODE); expect(refusal?.status).toBe(422); expect(refusal?.incomingPackageId).toBe(PKG_B); diff --git a/packages/objectql/src/protocol-install-package-enable-on-install.test.ts b/packages/objectql/src/protocol-install-package-enable-on-install.test.ts index 5e544f40223..1978caf2225 100644 --- a/packages/objectql/src/protocol-install-package-enable-on-install.test.ts +++ b/packages/objectql/src/protocol-install-package-enable-on-install.test.ts @@ -283,3 +283,16 @@ describe('#19277 — a BOOT-SEEDED disable, the durable state this seam can see' expect(registry.getPackage('com.acme.seeded.control')?.enabled).toBe(true); }); }); + +// [#23972] The protocol primitive is the authored door: a Studio base, a +// duplicate, an HTTP install. Every row it installs says `source: 'authored'`, +// on the row it returns and on the registry row the read doors serve. +describe('#23972 — the in-process install door stamps source authored', () => { + it('installs the row with source authored, in both records', async () => { + const registry = freshRegistry(); + const { protocol } = makeDoor(registry); + const res = await install(protocol, { manifest: manifest('com.acme.authored', 'authored') }); + expect(res.package.source, 'the row this primitive RETURNED').toBe('authored'); + expect(registry.getPackage('com.acme.authored')?.source, 'the registry the next read serves from').toBe('authored'); + }); +}); diff --git a/packages/objectql/src/protocol-install-package.test.ts b/packages/objectql/src/protocol-install-package.test.ts index 8ac578b498d..bd3afc2beb2 100644 --- a/packages/objectql/src/protocol-install-package.test.ts +++ b/packages/objectql/src/protocol-install-package.test.ts @@ -63,6 +63,7 @@ describe('protocol.installPackage (ADR-0033 consolidation)', () => { // from the id (`app.demo` → `demo`) since none was declared (see below). expect(registry.installPackage).toHaveBeenCalledWith( { ...manifest, namespace: 'demo' }, + 'authored', undefined, ); expect(publish).toHaveBeenCalledTimes(1); @@ -93,7 +94,7 @@ describe('protocol.installPackage (ADR-0033 consolidation)', () => { const { protocol, registry } = makeProtocol(); const manifest = { id: 'app.s', name: 'S', version: '1.0.0', type: 'application' }; await protocol.installPackage({ manifest, settings: { theme: 'dark' } } as never); - expect(registry.installPackage).toHaveBeenCalledWith(manifest, { theme: 'dark' }); + expect(registry.installPackage).toHaveBeenCalledWith(manifest, 'authored', { theme: 'dark' }); }); it('still registers in-memory when no package service is present (non-fatal)', async () => { diff --git a/packages/objectql/src/protocol-lock-artifact-package-axis.test.ts b/packages/objectql/src/protocol-lock-artifact-package-axis.test.ts index 46b5404e429..375ee3d7ef6 100644 --- a/packages/objectql/src/protocol-lock-artifact-package-axis.test.ts +++ b/packages/objectql/src/protocol-lock-artifact-package-axis.test.ts @@ -55,7 +55,7 @@ function protocolFor(arrangement: Arrangement, order: readonly string[]) { } as Record, 'name' as never, packageId); } for (const packageId of arrangement.disabled ?? []) { - registry.installPackage({ id: packageId, name: packageId, version: '1.0.0', type: 'app' } as never); + registry.installPackage({ id: packageId, name: packageId, version: '1.0.0', type: 'app' } as never, 'code'); registry.disablePackage(packageId); } const rows = arrangement.rowLock === undefined ? [] : [{ diff --git a/packages/objectql/src/protocol-object-overlay-layer.test.ts b/packages/objectql/src/protocol-object-overlay-layer.test.ts index 01a2a774ce1..d29c012aa85 100644 --- a/packages/objectql/src/protocol-object-overlay-layer.test.ts +++ b/packages/objectql/src/protocol-object-overlay-layer.test.ts @@ -218,7 +218,7 @@ async function persistOverlayRow(name: string, packageId?: string): Promise - registry.installPackage({ id, name: 'My App', version: '1.0.0' } as any); + registry.installPackage({ id, name: 'My App', version: '1.0.0' } as any, 'code'); const ownerPackageId = (registry: SchemaRegistry, name: string) => registry.getObjectOwner(name)?.packageId; diff --git a/packages/objectql/src/protocol-packaged-object-base.test.ts b/packages/objectql/src/protocol-packaged-object-base.test.ts index 5f7b1c8daea..94c45ebc7c7 100644 --- a/packages/objectql/src/protocol-packaged-object-base.test.ts +++ b/packages/objectql/src/protocol-packaged-object-base.test.ts @@ -120,7 +120,7 @@ function makeSession() { /** The packaged object, as a code package registers it. */ function registerPackaged(registry: SchemaRegistry) { - registry.installPackage({ id: PKG, name: 'Showcase', version: '1.0.0' } as any); + registry.installPackage({ id: PKG, name: 'Showcase', version: '1.0.0' } as any, 'code'); registry.registerObject( { name: OBJ, diff --git a/packages/objectql/src/publish-package-drafts-response-conformance.test.ts b/packages/objectql/src/publish-package-drafts-response-conformance.test.ts index 0292e9ed854..e80fad1d2b7 100644 --- a/packages/objectql/src/publish-package-drafts-response-conformance.test.ts +++ b/packages/objectql/src/publish-package-drafts-response-conformance.test.ts @@ -141,7 +141,7 @@ async function makeProtocol() { // The base every draft here is bound to is INSTALLED: a flow may be saved // only into a package the registry holds (#20863). Its manifest declares // no namespace, so the ADR-0028 prefix pre-flight is unchanged. - engine.registry.installPackage({ id: PKG, name: 'Edu', version: '1.0.0' } as never); + engine.registry.installPackage({ id: PKG, name: 'Edu', version: '1.0.0' } as never, 'code'); return new ObjectStackProtocolImplementation(engine); } diff --git a/packages/objectql/src/registry-artifact-co-ownership.test.ts b/packages/objectql/src/registry-artifact-co-ownership.test.ts index 55a91323757..3e9a71dcd89 100644 --- a/packages/objectql/src/registry-artifact-co-ownership.test.ts +++ b/packages/objectql/src/registry-artifact-co-ownership.test.ts @@ -266,7 +266,7 @@ describe('ADR-0130 D1 — the relaxation does not widen the same-id reinstall ex manifest: { id: string; namespace: string; objects?: Array<{ name: string }> }, scope?: { packageIds: string[] }, ) => { - registry.installPackage(manifest as never, undefined, scope); + registry.installPackage(manifest as never, 'code', undefined, scope); for (const obj of manifest.objects ?? []) { registry.registerObject(obj as never, manifest.id, manifest.namespace, 'own'); } @@ -293,7 +293,7 @@ describe('ADR-0130 D1 — the relaxation does not widen the same-id reinstall ex let caught: Envelope | undefined; try { - registry.installPackage(crmBillingColliding() as never, undefined, scope); + registry.installPackage(crmBillingColliding() as never, 'code', undefined, scope); } catch (e) { caught = e as Envelope; } expect(caught?.code).toBe('DUPLICATE_ARTIFACT_OBJECT_NAME'); @@ -306,9 +306,9 @@ describe('ADR-0130 D1 — the relaxation does not widen the same-id reinstall ex // id, so same package, so a reload — including a reload arriving from a // different artifact, which is what a version upgrade is. install(crmCore(), scopeOf('com.acme.crm')); - expect(() => registry.installPackage(crmCore() as never, undefined, scopeOf('com.acme.crm'))).not.toThrow(); - expect(() => registry.installPackage(crmCore() as never)).not.toThrow(); - expect(() => registry.installPackage(crmCore() as never, undefined, scopeOf('com.acme.crm', 'com.acme.crm.billing'))) + expect(() => registry.installPackage(crmCore() as never, 'code', undefined, scopeOf('com.acme.crm'))).not.toThrow(); + expect(() => registry.installPackage(crmCore() as never, 'code')).not.toThrow(); + expect(() => registry.installPackage(crmCore() as never, 'code', undefined, scopeOf('com.acme.crm', 'com.acme.crm.billing'))) .not.toThrow(); }); @@ -323,7 +323,7 @@ describe('ADR-0130 D1 — the relaxation does not widen the same-id reinstall ex let caught: Envelope | undefined; try { - registry.installPackage(stranger as never, undefined, scopeOf('com.other.suite')); + registry.installPackage(stranger as never, 'code', undefined, scopeOf('com.other.suite')); } catch (e) { caught = e as Envelope; } expect(caught).toBeUndefined(); @@ -340,12 +340,12 @@ describe('ADR-0130 D1 — the relaxation does not widen the same-id reinstall ex warnReg.logLevel = 'silent'; const scope = scopeOf('com.acme.crm', 'com.acme.crm.billing'); const base = crmCore(); - warnReg.installPackage(base as never, undefined, scope); + warnReg.installPackage(base as never, 'code', undefined, scope); for (const obj of base.objects) warnReg.registerObject(obj as never, base.id, base.namespace, 'own'); let caught: Envelope | undefined; try { - warnReg.installPackage(crmBillingColliding() as never, undefined, scope); + warnReg.installPackage(crmBillingColliding() as never, 'code', undefined, scope); } catch (e) { caught = e as Envelope; } expect(caught?.code).toBe('DUPLICATE_ARTIFACT_OBJECT_NAME'); diff --git a/packages/objectql/src/registry-assert-package-uninstallable.test.ts b/packages/objectql/src/registry-assert-package-uninstallable.test.ts index bce59b5ca1b..89c790a6af8 100644 --- a/packages/objectql/src/registry-assert-package-uninstallable.test.ts +++ b/packages/objectql/src/registry-assert-package-uninstallable.test.ts @@ -139,7 +139,7 @@ describe('#21276 SchemaRegistry.assertPackageUninstallable', () => { expect(ask).toHaveBeenCalledWith('com.owner'); expect(registry.getObject('free')).toBeDefined(); - registry.installPackage({ id: 'com.owner', name: 'Owner', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.owner', name: 'Owner', version: '1.0.0' } as any, 'code'); ask.mockClear(); const viaUninstall = thrownBy(() => registry.uninstallPackage('com.owner')); expect(ask).toHaveBeenCalledTimes(1); diff --git a/packages/objectql/src/registry-default-record-page.test.ts b/packages/objectql/src/registry-default-record-page.test.ts index 65bf531f565..a6cf4bdb920 100644 --- a/packages/objectql/src/registry-default-record-page.test.ts +++ b/packages/objectql/src/registry-default-record-page.test.ts @@ -165,7 +165,7 @@ describe('a second package registering a default record page for an object anoth it('a package installed with no pages registered still holds what it declared (its claim)', async () => { const { registry, register } = await boot(); // A bare install — `POST /api/v1/packages` stores the manifest alone. - registry.installPackage(pkg('com.acme.auth', [recordPage('acme_org_detail', 'acme_org')]) as any); + registry.installPackage(pkg('com.acme.auth', [recordPage('acme_org_detail', 'acme_org')]) as any, 'code'); expect(registry.listItems('page')).toEqual([]); const err = await register(pkg('com.acme.tenant', [recordPage('tenant_org_detail', 'acme_org')])); expectRefusal(err, 'acme_org', pkgPage('com.acme.tenant', 'tenant_org_detail'), pkgPage('com.acme.auth', 'acme_org_detail')); @@ -301,8 +301,8 @@ describe('controls — what is not a second default', () => { it('`collisionPolicy: \'warn\'` does not downgrade it', async () => { const registry = new SchemaRegistry({ collisionPolicy: 'warn' }); registry.logLevel = 'silent'; - registry.installPackage(pkg('com.acme.auth', [recordPage('acme_org_detail', 'acme_org')]) as never); - const err = await caught(() => registry.installPackage(pkg('com.acme.tenant', [recordPage('tenant_org_detail', 'acme_org')]) as never)); + registry.installPackage(pkg('com.acme.auth', [recordPage('acme_org_detail', 'acme_org')]) as never, 'code'); + const err = await caught(() => registry.installPackage(pkg('com.acme.tenant', [recordPage('tenant_org_detail', 'acme_org')]) as never, 'code')); expectRefusal(err, 'acme_org', pkgPage('com.acme.tenant', 'tenant_org_detail'), pkgPage('com.acme.auth', 'acme_org_detail')); }); }); diff --git a/packages/objectql/src/registry-i18n-bundle-key.test.ts b/packages/objectql/src/registry-i18n-bundle-key.test.ts index 7296706aac8..430201f29f4 100644 --- a/packages/objectql/src/registry-i18n-bundle-key.test.ts +++ b/packages/objectql/src/registry-i18n-bundle-key.test.ts @@ -304,7 +304,7 @@ describe('SchemaRegistry — i18n bundle keys (#7730)', () => { it('hides the whole bundle when its owning package is disabled', () => { // `listItems` filters by each item's `_packageId` (PR #7700). Re-keying // must not let a bundle member slip past that filter. - registry.installPackage({ id: 'com.acme.crm', name: 'CRM', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.acme.crm', name: 'CRM', version: '1.0.0' } as any, 'code'); registry.registerItem('email_template', tpl('auth.welcome', 'en-US'), 'name', 'com.acme.crm'); registry.registerItem('email_template', tpl('auth.welcome', 'zh-CN'), 'name', 'com.acme.crm'); expect(registry.listItems('email_template')).toHaveLength(2); diff --git a/packages/objectql/src/registry-install-preserves-lifecycle.test.ts b/packages/objectql/src/registry-install-preserves-lifecycle.test.ts index 670b2a2a47e..371529c2dd4 100644 --- a/packages/objectql/src/registry-install-preserves-lifecycle.test.ts +++ b/packages/objectql/src/registry-install-preserves-lifecycle.test.ts @@ -46,12 +46,12 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state }); it('a re-install carries `enabled` / `status` / `statusChangedAt` over from the existing row', () => { - registry.installPackage(manifest('com.acme.carry')); + registry.installPackage(manifest('com.acme.carry'), 'code'); const disabled = registry.disablePackage('com.acme.carry'); const stampedAt = disabled?.statusChangedAt; expect(typeof stampedAt, 'precondition: the disable stamped a time').toBe('string'); - const again = registry.installPackage(manifest('com.acme.carry')); + const again = registry.installPackage(manifest('com.acme.carry'), 'code'); expect(again.enabled, 'the row the registry RETURNED').toBe(false); expect(again.status).toBe('disabled'); @@ -65,8 +65,8 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state it('⛔ the control leg — a re-install does not disable an ENABLED row either', () => { // Without this, «preserve» is satisfiable by a registry that simply // stopped enabling anything. - registry.installPackage(manifest('com.acme.stayon')); - const again = registry.installPackage(manifest('com.acme.stayon')); + registry.installPackage(manifest('com.acme.stayon'), 'code'); + const again = registry.installPackage(manifest('com.acme.stayon'), 'code'); expect(again.enabled).toBe(true); expect(again.status).toBe('installed'); @@ -76,7 +76,7 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state it('the boot seed still decides for an id that has NO row yet', () => { registry.setInitialDisabledPackageIds(['com.acme.seeded']); - const first = registry.installPackage(manifest('com.acme.seeded')); + const first = registry.installPackage(manifest('com.acme.seeded'), 'code'); expect(first.enabled, 'boot hydration must still replay a persisted disable').toBe(false); expect(first.status).toBe('disabled'); @@ -87,10 +87,10 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state // The card's own defect, at this layer. Before #18877 the second // install read the seed again and re-landed the package disabled. registry.setInitialDisabledPackageIds(['com.acme.seeded.enabled']); - registry.installPackage(manifest('com.acme.seeded.enabled')); + registry.installPackage(manifest('com.acme.seeded.enabled'), 'code'); registry.enablePackage('com.acme.seeded.enabled'); - const again = registry.installPackage(manifest('com.acme.seeded.enabled')); + const again = registry.installPackage(manifest('com.acme.seeded.enabled'), 'code'); expect(again.enabled, 'the most recent explicit lifecycle action stands').toBe(true); expect(again.status).toBe('installed'); @@ -99,7 +99,7 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state it('an id the seed never named still lands at the declared default', () => { registry.setInitialDisabledPackageIds(['com.acme.someone.else']); - const fresh = registry.installPackage(manifest('com.acme.unseeded')); + const fresh = registry.installPackage(manifest('com.acme.unseeded'), 'code'); expect(fresh.enabled).toBe(true); expect(fresh.status).toBe('installed'); }); @@ -108,10 +108,10 @@ describe('#18877 — installPackage preserves an existing row\'s lifecycle state // ⛔ The scope leg: 「preserve」 is about lifecycle state only. A // re-install is still an install, and the row it writes is still the // new manifest's. - registry.installPackage({ ...manifest('com.acme.scope'), version: '1.0.0' } as any); + registry.installPackage({ ...manifest('com.acme.scope'), version: '1.0.0' } as any, 'code'); registry.disablePackage('com.acme.scope'); - const again = registry.installPackage({ ...manifest('com.acme.scope'), version: '2.0.0' } as any); + const again = registry.installPackage({ ...manifest('com.acme.scope'), version: '2.0.0' } as any, 'code'); expect(again.manifest.version, 'the re-install really replaced the manifest').toBe('2.0.0'); expect(again.enabled, 'and did so without touching the lifecycle state').toBe(false); @@ -128,11 +128,11 @@ describe('#18877 item 3 — uninstallPackage forgets the id from the boot seed', it('a package deleted and re-installed in the same boot is a FRESH install', () => { registry.setInitialDisabledPackageIds(['com.acme.gone']); - expect(registry.installPackage(manifest('com.acme.gone')).enabled, 'precondition: the seed applied').toBe(false); + expect(registry.installPackage(manifest('com.acme.gone'), 'code').enabled, 'precondition: the seed applied').toBe(false); expect(registry.uninstallPackage('com.acme.gone')).toBe(true); - const reinstalled = registry.installPackage(manifest('com.acme.gone')); + const reinstalled = registry.installPackage(manifest('com.acme.gone'), 'code'); expect( reinstalled.enabled, 'a row that no longer exists has no lifecycle state — and the seed must not supply one', @@ -142,10 +142,10 @@ describe('#18877 item 3 — uninstallPackage forgets the id from the boot seed', it('⛔ and it forgets only the id it was given', () => { registry.setInitialDisabledPackageIds(['com.acme.gone2', 'com.acme.neighbour']); - registry.installPackage(manifest('com.acme.gone2')); + registry.installPackage(manifest('com.acme.gone2'), 'code'); registry.uninstallPackage('com.acme.gone2'); - const neighbour = registry.installPackage(manifest('com.acme.neighbour')); + const neighbour = registry.installPackage(manifest('com.acme.neighbour'), 'code'); expect(neighbour.enabled, 'the neighbour\'s persisted disable is untouched').toBe(false); }); @@ -155,7 +155,7 @@ describe('#18877 item 3 — uninstallPackage forgets the id from the boot seed', // anyway, or a refused uninstall would silently re-enable the package // on its next registration. registry.setInitialDisabledPackageIds(['com.acme.extended']); - registry.installPackage(manifest('com.acme.extended', 'ext')); + registry.installPackage(manifest('com.acme.extended', 'ext'), 'code'); registry.registerObject({ name: 'account', fields: {} } as any, 'com.acme.extended', 'ext', 'own'); registry.registerObject({ name: 'account', fields: {} } as any, 'com.acme.extender', undefined, 'extend'); @@ -169,7 +169,7 @@ describe('#18877 item 3 — uninstallPackage forgets the id from the boot seed', registry.unregisterObjectsByPackage('com.acme.extender'); expect(registry.uninstallPackage('com.acme.extended'), 'now it really goes').toBe(true); expect( - registry.installPackage(manifest('com.acme.extended', 'ext')).enabled, + registry.installPackage(manifest('com.acme.extended', 'ext'), 'code').enabled, 'and only THEN is the id forgotten', ).toBe(true); }); diff --git a/packages/objectql/src/registry-log-level.test.ts b/packages/objectql/src/registry-log-level.test.ts index 2f27e0ecd43..fb25983293c 100644 --- a/packages/objectql/src/registry-log-level.test.ts +++ b/packages/objectql/src/registry-log-level.test.ts @@ -60,15 +60,15 @@ describe('SchemaRegistry log-level gating (#3420)', () => { const info = new SchemaRegistry({ multiTenant: false }); expect(info.logLevel).toBe('info'); - info.installPackage(manifest); - info.installPackage(manifest); // same-package reload → "Overwriting package" + info.installPackage(manifest, 'code'); + info.installPackage(manifest, 'code'); // same-package reload → "Overwriting package" expect(warn).not.toHaveBeenCalledWith(expect.stringContaining('Overwriting package')); expect(debug).not.toHaveBeenCalledWith(expect.stringContaining('Overwriting package')); debug.mockClear(); const dbg = new SchemaRegistry({ multiTenant: false, logLevel: 'debug' }); - dbg.installPackage(manifest); - dbg.installPackage(manifest); + dbg.installPackage(manifest, 'code'); + dbg.installPackage(manifest, 'code'); expect(debug).toHaveBeenCalledWith(expect.stringContaining('Overwriting package: com.test')); }); diff --git a/packages/objectql/src/registry-namespace-install-gate.test.ts b/packages/objectql/src/registry-namespace-install-gate.test.ts index 584f3eae2ac..cfab96e9dfa 100644 --- a/packages/objectql/src/registry-namespace-install-gate.test.ts +++ b/packages/objectql/src/registry-namespace-install-gate.test.ts @@ -30,16 +30,16 @@ describe('SchemaRegistry — namespace install gate (ADR-0048 Phase 1)', () => { }); it('refuses a package whose namespace is already owned by a different package', () => { - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); expect(() => - registry.installPackage(manifest('com.beta.crm', 'crm') as any), + registry.installPackage(manifest('com.beta.crm', 'crm') as any, 'code'), ).toThrowError(NamespaceConflictError); }); it('error names both packages and the namespace', () => { - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); try { - registry.installPackage(manifest('com.beta.crm', 'crm') as any); + registry.installPackage(manifest('com.beta.crm', 'crm') as any, 'code'); throw new Error('expected a namespace conflict error'); } catch (e) { expect(e).toBeInstanceOf(NamespaceConflictError); @@ -66,10 +66,10 @@ describe('SchemaRegistry — namespace install gate (ADR-0048 Phase 1)', () => { // after it. `resolveThrownHttpError` reads exactly these two fields off the // throw, so they are what the door's answer is MADE of — asserting the // class instead asserts something the wire never sees. - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); let caught: (Error & { code?: string; status?: number }) | undefined; try { - registry.installPackage(manifest('com.beta.crm', 'crm') as any); + registry.installPackage(manifest('com.beta.crm', 'crm') as any, 'code'); } catch (e) { caught = e as Error & { code?: string; status?: number }; } expect(caught?.code).toBe('NAMESPACE_CONFLICT'); @@ -80,24 +80,24 @@ describe('SchemaRegistry — namespace install gate (ADR-0048 Phase 1)', () => { }); it('allows the same package to reinstall/reload its own namespace', () => { - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); expect(() => - registry.installPackage(manifest('com.acme.crm', 'crm') as any), + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'), ).not.toThrow(); }); it('allows two packages with distinct namespaces', () => { - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); expect(() => - registry.installPackage(manifest('com.acme.hr', 'hr') as any), + registry.installPackage(manifest('com.acme.hr', 'hr') as any, 'code'), ).not.toThrow(); }); it('exempts shareable platform namespaces (base/system/sys)', () => { for (const ns of ['base', 'system', 'sys']) { - registry.installPackage(manifest(`com.a.${ns}`, ns) as any); + registry.installPackage(manifest(`com.a.${ns}`, ns) as any, 'code'); expect(() => - registry.installPackage(manifest(`com.b.${ns}`, ns) as any), + registry.installPackage(manifest(`com.b.${ns}`, ns) as any, 'code'), ).not.toThrow(); } }); @@ -105,19 +105,19 @@ describe('SchemaRegistry — namespace install gate (ADR-0048 Phase 1)', () => { it('downgrades to a warning under collisionPolicy "warn"', () => { const warnReg = new SchemaRegistry({ multiTenant: false, collisionPolicy: 'warn' }); warnReg.logLevel = 'silent'; - warnReg.installPackage(manifest('com.acme.crm', 'crm') as any); + warnReg.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); expect(() => - warnReg.installPackage(manifest('com.beta.crm', 'crm') as any), + warnReg.installPackage(manifest('com.beta.crm', 'crm') as any, 'code'), ).not.toThrow(); // Both packages are recorded; the namespace now has two owners. expect(warnReg.getNamespaceOwners('crm').sort()).toEqual(['com.acme.crm', 'com.beta.crm']); }); it('releases the namespace on uninstall, allowing a different package to claim it', () => { - registry.installPackage(manifest('com.acme.crm', 'crm') as any); + registry.installPackage(manifest('com.acme.crm', 'crm') as any, 'code'); registry.uninstallPackage('com.acme.crm'); expect(() => - registry.installPackage(manifest('com.beta.crm', 'crm') as any), + registry.installPackage(manifest('com.beta.crm', 'crm') as any, 'code'), ).not.toThrow(); expect(registry.getNamespaceOwners('crm')).toEqual(['com.beta.crm']); }); diff --git a/packages/objectql/src/registry-package-manifest-serializable.test.ts b/packages/objectql/src/registry-package-manifest-serializable.test.ts index 1f4a8dad473..95f521a82af 100644 --- a/packages/objectql/src/registry-package-manifest-serializable.test.ts +++ b/packages/objectql/src/registry-package-manifest-serializable.test.ts @@ -82,7 +82,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { it('survives a plugin instance that closes a cycle through the engine', () => { const plugin = new FakeConnectorPlugin(); plugin.init(makeCyclicEngine()); - registry.installPackage(baseManifest({ plugins: [plugin] })); + registry.installPackage(baseManifest({ plugins: [plugin] }), 'code'); // The expression every read door runs. expect(() => JSON.stringify(registry.getPackage('com.example.showcase'))).not.toThrow(); @@ -100,7 +100,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { const manifest = baseManifest({ plugins: [plugin] }); expect(() => JSON.stringify(manifest)).not.toThrow(); - registry.installPackage(manifest); + registry.installPackage(manifest, 'code'); plugin.init(makeCyclicEngine()); expect(() => JSON.stringify(registry.getPackage('com.example.showcase'))).not.toThrow(); @@ -109,7 +109,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { it('survives a reference cycle among PLAIN data in the manifest', () => { const cyclic: Record = { name: 'self' }; cyclic.self = cyclic; - registry.installPackage(baseManifest({ data: { node: cyclic } })); + registry.installPackage(baseManifest({ data: { node: cyclic } }), 'code'); const record = registry.getPackage('com.example.showcase')!; expect(() => JSON.stringify(record)).not.toThrow(); @@ -120,7 +120,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { it('keeps the declarative half of the manifest byte-for-byte', () => { const manifest = baseManifest({ plugins: [new FakeConnectorPlugin()] }); - registry.installPackage(manifest); + registry.installPackage(manifest, 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; for (const key of [ @@ -137,7 +137,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { onEnable: () => undefined, registryHandle: new Map([['a', 1]]), publishedAt, - })); + }), 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; expect(stored.onEnable).toBeUndefined(); @@ -151,7 +151,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { // Projecting must therefore copy, never strip in place. const plugin = new FakeConnectorPlugin(); const manifest = baseManifest({ plugins: [plugin] }); - registry.installPackage(manifest); + registry.installPackage(manifest, 'code'); expect(manifest.plugins).toHaveLength(1); expect(manifest.plugins[0]).toBe(plugin); @@ -159,10 +159,10 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { }); it('projects on REINSTALL too (rebuild / HMR overwrite)', () => { - registry.installPackage(baseManifest()); + registry.installPackage(baseManifest(), 'code'); const plugin = new FakeConnectorPlugin(); plugin.init(makeCyclicEngine()); - registry.installPackage(baseManifest({ plugins: [plugin] })); + registry.installPackage(baseManifest({ plugins: [plugin] }), 'code'); expect(() => JSON.stringify(registry.getPackage('com.example.showcase'))).not.toThrow(); }); @@ -172,10 +172,10 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { // caller's whole listing, not just the offending package. const plugin = new FakeConnectorPlugin(); plugin.init(makeCyclicEngine()); - registry.installPackage(baseManifest({ plugins: [plugin] })); + registry.installPackage(baseManifest({ plugins: [plugin] }), 'code'); registry.installPackage({ id: 'com.objectstack.setup', name: 'Setup', namespace: 'setup', version: '9.3.0', - } as any); + } as any, 'code'); expect(() => JSON.stringify(registry.getAllPackages())).not.toThrow(); expect(registry.getAllPackages()).toHaveLength(2); @@ -183,7 +183,7 @@ describe('SchemaRegistry.installPackage — the record is serializable', () => { it('still records the namespace and the lifecycle state it always did', () => { // The projection must not cost the record its non-manifest half. - registry.installPackage(baseManifest({ plugins: [new FakeConnectorPlugin()] })); + registry.installPackage(baseManifest({ plugins: [new FakeConnectorPlugin()] }), 'code'); const record = registry.getPackage('com.example.showcase')!; expect(record.status).toBe('installed'); @@ -227,7 +227,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu }); it('records BOTH spellings — the bare one as a handler-less declaration', () => { - registry.installPackage(baseManifest({ functions: showcaseFunctions() })); + registry.installPackage(baseManifest({ functions: showcaseFunctions() }), 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; expect(Object.keys(stored.functions).sort()).toEqual(['summarizeCompletedTask', 'sweepProjectHealth']); @@ -240,7 +240,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu }); it('⛔ mints no handler ref — an absent `handler` is the honest statement', () => { - registry.installPackage(baseManifest({ functions: showcaseFunctions() })); + registry.installPackage(baseManifest({ functions: showcaseFunctions() }), 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; for (const entry of Object.values(stored.functions) as Array>) { @@ -253,7 +253,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu // no runtime module re-attached the callables: a bare ref and a lowered // record. Neither is a callable, so neither is normalised. const lowered = { bare: 'bare', declared: { handler: 'declared', effect: 'writes' } }; - registry.installPackage(baseManifest({ functions: lowered })); + registry.installPackage(baseManifest({ functions: lowered }), 'code'); expect((registry.getPackage('com.example.showcase')!.manifest as any).functions).toEqual(lowered); }); @@ -264,13 +264,13 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu // default, so ⛔ nothing is written where the schema declares nothing. registry.installPackage(baseManifest({ functions: [{ name: 'syncBilling', handler: () => 'billed', effect: 'writes' }], - })); + }), 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; expect(stored.functions).toEqual([{ name: 'syncBilling', effect: 'writes' }]); const bare = new SchemaRegistry({ multiTenant: false, collisionPolicy: 'error' }); bare.logLevel = 'silent'; - bare.installPackage(baseManifest({ functions: [{ name: 'syncBilling', handler: () => 'billed' }] })); + bare.installPackage(baseManifest({ functions: [{ name: 'syncBilling', handler: () => 'billed' }] }), 'code'); expect((bare.getPackage('com.example.showcase')!.manifest as any).functions) .toEqual([{ name: 'syncBilling' }]); }); @@ -280,7 +280,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu // object. Normalising must copy, exactly as the projection does. const functions = showcaseFunctions(); const manifest = baseManifest({ functions }); - registry.installPackage(manifest); + registry.installPackage(manifest, 'code'); expect(typeof manifest.functions.summarizeCompletedTask).toBe('function'); expect(manifest.functions).toBe(functions); @@ -292,7 +292,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu // declared, must reach the projection as the SAME object — the normaliser // copies only when it really rewrote an entry. const untouched = baseManifest(); - registry.installPackage(untouched); + registry.installPackage(untouched, 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; expect('functions' in stored).toBe(false); expect(stored.objects).toEqual(untouched.objects); @@ -307,7 +307,7 @@ describe('SchemaRegistry.installPackage — the record reports every declared fu { name: 'on_insert', object: 'invoice', events: ['beforeInsert'], handler: () => 'hooked' }, { name: 'on_update', object: 'invoice', events: ['beforeUpdate'], handler: 'on_update' }, ], - })); + }), 'code'); const stored = registry.getPackage('com.example.showcase')!.manifest as any; expect(stored.hooks).toEqual([ diff --git a/packages/objectql/src/registry-security-catalog-names-held-by.test.ts b/packages/objectql/src/registry-security-catalog-names-held-by.test.ts index 0706dbcb409..075fb4984df 100644 --- a/packages/objectql/src/registry-security-catalog-names-held-by.test.ts +++ b/packages/objectql/src/registry-security-catalog-names-held-by.test.ts @@ -21,22 +21,22 @@ describe('SchemaRegistry.securityCatalogNamesHeldBy', () => { }); it('answers the permission-set names a package declared, and no other package\'s', () => { - registry.installPackage(manifest('com.acme.tasks', ['tasks_user', 'tasks_admin'])); - registry.installPackage(manifest('com.acme.other', ['other_user'])); + registry.installPackage(manifest('com.acme.tasks', ['tasks_user', 'tasks_admin']), 'code'); + registry.installPackage(manifest('com.acme.other', ['other_user']), 'code'); expect(registry.securityCatalogNamesHeldBy('com.acme.tasks', 'permission')).toEqual(['tasks_admin', 'tasks_user']); expect(registry.securityCatalogNamesHeldBy('com.acme.tasks', 'position')).toEqual([]); }); it('a disabled package still holds its names', () => { - registry.installPackage(manifest('com.acme.tasks', ['tasks_user'])); + registry.installPackage(manifest('com.acme.tasks', ['tasks_user']), 'code'); registry.disablePackage('com.acme.tasks'); expect(registry.securityCatalogNamesHeldBy('com.acme.tasks', 'permission')).toEqual(['tasks_user']); }); it('nothing once the package is uninstalled — which is why the doors read it first', () => { - registry.installPackage(manifest('com.acme.tasks', ['tasks_user'])); + registry.installPackage(manifest('com.acme.tasks', ['tasks_user']), 'code'); registry.uninstallPackage('com.acme.tasks'); expect(registry.securityCatalogNamesHeldBy('com.acme.tasks', 'permission')).toEqual([]); diff --git a/packages/objectql/src/registry-security-catalog-namespace.test.ts b/packages/objectql/src/registry-security-catalog-namespace.test.ts index 7122b94af11..67a3e31da32 100644 --- a/packages/objectql/src/registry-security-catalog-namespace.test.ts +++ b/packages/objectql/src/registry-security-catalog-namespace.test.ts @@ -252,8 +252,8 @@ describe('the refusal reports, and only refuses, what the ruling covers', () => it('collisionPolicy "warn" does not downgrade it', async () => { const registry = new SchemaRegistry({ collisionPolicy: 'warn' }); registry.logLevel = 'silent'; - registry.installPackage(pkg('com.acme.first', { position: ['p1'] }) as never); - const err = await caught(() => registry.installPackage(pkg('com.acme.second', { position: ['p1'] }) as never)); + registry.installPackage(pkg('com.acme.first', { position: ['p1'] }) as never, 'code'); + const err = await caught(() => registry.installPackage(pkg('com.acme.second', { position: ['p1'] }) as never, 'code')); expectRefusal(err, 'com.acme.second', { kind: 'package', packageId: 'com.acme.first' }); }); diff --git a/packages/objectql/src/registry-unregister-items-by-package.test.ts b/packages/objectql/src/registry-unregister-items-by-package.test.ts index ddc484c926b..d5a034505bc 100644 --- a/packages/objectql/src/registry-unregister-items-by-package.test.ts +++ b/packages/objectql/src/registry-unregister-items-by-package.test.ts @@ -192,7 +192,7 @@ describe('#7221 — SchemaRegistry.unregisterItemsByPackage', () => { describe('#7221 — SchemaRegistry.uninstallPackage closes the same gap', () => { it('removes the package’s generic items, not just its objects and record', () => { const reg = quiet(); - reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any); + reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any, 'code'); reg.registerItem('page', { name: 'home' }, 'name', 'crm'); reg.registerItem('flow', { name: 'onboard' }, 'name', 'crm'); reg.registerObject(objectBody('contact'), 'crm'); @@ -209,7 +209,7 @@ describe('#7221 — SchemaRegistry.uninstallPackage closes the same gap', () => it('removes NOTHING when the object half refuses (ADR-0029 extenders)', () => { const reg = quiet(); - reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any); + reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any, 'code'); reg.registerItem('page', { name: 'home' }, 'name', 'crm'); reg.registerObject(objectBody('contact'), 'crm'); reg.registerObject(objectBody('contact', 'extra'), 'analytics', undefined, 'extend'); @@ -224,7 +224,7 @@ describe('#7221 — SchemaRegistry.uninstallPackage closes the same gap', () => it('keeps the package’s own `package` record addressable by its bare id', () => { const reg = quiet(); - reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any); + reg.installPackage({ id: 'crm', name: 'CRM', version: '1.0.0' } as any, 'code'); // The sweep is prefix-scoped to `crm:`, so the bare-keyed package record is // not collateral — `uninstallPackage` still owns removing it, and reports // `true` because it found it. diff --git a/packages/objectql/src/registry.test.ts b/packages/objectql/src/registry.test.ts index 29e979e8407..90a2b41ab9a 100644 --- a/packages/objectql/src/registry.test.ts +++ b/packages/objectql/src/registry.test.ts @@ -476,7 +476,7 @@ describe('SchemaRegistry', () => { describe('Package Management', () => { it('should install package with namespace', () => { const manifest = { id: 'com.test', name: 'Test', namespace: 'test', version: '1.0.0' }; - const pkg = registry.installPackage(manifest as any); + const pkg = registry.installPackage(manifest as any, 'code'); expect(pkg.status).toBe('installed'); expect(registry.getNamespaceOwner('test')).toBe('com.test'); @@ -484,7 +484,7 @@ describe('SchemaRegistry', () => { it('should uninstall package and release namespace', () => { const manifest = { id: 'com.test', name: 'Test', namespace: 'test', version: '1.0.0' }; - registry.installPackage(manifest as any); + registry.installPackage(manifest as any, 'code'); registry.uninstallPackage('com.test'); expect(registry.getPackage('com.test')).toBeUndefined(); @@ -504,7 +504,7 @@ describe('SchemaRegistry', () => { * Latent by grade: no in-tree caller reaches the refusal path today. */ it('[#7970] a refused uninstall leaves the namespace still resolving', () => { - registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any, 'code'); registry.registerObject({ name: 'contact', fields: {} }, 'com.crm', 'crm', 'own'); registry.registerObject({ name: 'contact', fields: {} }, 'com.analytics', undefined, 'extend'); @@ -520,7 +520,7 @@ describe('SchemaRegistry', () => { }); it('[#7970] a refused uninstall leaves the whole package intact, not just the namespace', () => { - registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any, 'code'); // Registered ahead of the extended object, so the object walk reaches // this one before it can refuse. registry.registerObject({ name: 'account', fields: {} }, 'com.crm', 'crm', 'own'); @@ -539,7 +539,7 @@ describe('SchemaRegistry', () => { }); it('[#7970] the successful path still releases the namespace after the object verb', () => { - registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.crm', name: 'CRM', namespace: 'crm', version: '1.0.0' } as any, 'code'); registry.registerObject({ name: 'contact', fields: {} }, 'com.crm', 'crm', 'own'); expect(registry.uninstallPackage('com.crm')).toBe(true); @@ -550,7 +550,7 @@ describe('SchemaRegistry', () => { }); it('updatePackageManifest merges editable fields, preserving lifecycle state', () => { - registry.installPackage({ id: 'com.test', name: 'Old', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.test', name: 'Old', version: '1.0.0' } as any, 'code'); registry.disablePackage('com.test'); // lifecycle state that must survive an edit const updated = registry.updatePackageManifest('com.test', { @@ -568,7 +568,7 @@ describe('SchemaRegistry', () => { }); it('updatePackageManifest only overwrites the fields present in the patch', () => { - registry.installPackage({ id: 'com.test', name: 'Keep', version: '1.0.0' } as any); + registry.installPackage({ id: 'com.test', name: 'Keep', version: '1.0.0' } as any, 'code'); const updated = registry.updatePackageManifest('com.test', { description: 'only this' }); expect(updated?.manifest.name).toBe('Keep'); expect(updated?.manifest.version).toBe('1.0.0'); diff --git a/packages/objectql/src/registry.ts b/packages/objectql/src/registry.ts index 154295ad543..db17afe5981 100644 --- a/packages/objectql/src/registry.ts +++ b/packages/objectql/src/registry.ts @@ -44,7 +44,7 @@ import { SystemFieldName, scalarOverridesPackagedBase } from '@objectstack/spec/ import { resolveTenancyPosture, resolveSearchPinyinEnabled } from '@objectstack/types'; import { postureEnforcesWall } from '@objectstack/spec/security'; import { provisionSearchCompanion, SEARCH_COMPANION_FIELD } from './search-companion.js'; -import { ObjectStackManifest, ManifestSchema, InstalledPackage, InstalledPackageSchema, checkFieldCompleteness } from '@objectstack/spec/kernel'; +import { ObjectStackManifest, ManifestSchema, InstalledPackage, InstalledPackageSchema, InstalledPackageSourceSchema, type InstalledPackageSource, checkFieldCompleteness } from '@objectstack/spec/kernel'; import { DEFAULT_FLOW_FUNCTION_EFFECT } from '@objectstack/spec/automation'; import { AppSchema } from '@objectstack/spec/ui'; import { applyProtection } from '@objectstack/spec/shared'; @@ -5439,9 +5439,27 @@ export class SchemaRegistry { installPackage( manifest: ObjectStackManifest, + source: InstalledPackageSource, settings?: Record, scope?: ArtifactInstallScope, ): InstalledPackage { + // [#23972] `source` is the CALLER's statement of where this package came + // from — the door that registers it is the only party that knows + // (`registerApp`: code or marketplace; `installPackage` in + // `@objectstack/metadata-protocol`, the `POST /packages` fallback and the + // `sys_packages` rehydrate: authored). The registry records it and never + // infers it, so an untyped caller that omits it, or spells a value outside + // the closed set, is refused here — ahead of every check and mutation + // below, so a refused call leaves no row behind — rather than recording a + // row the read door would serve without an origin. + if (!InstalledPackageSourceSchema.safeParse(source).success) { + throw new TypeError( + `[Registry] installPackage('${String(manifest?.id)}') needs the package's source, one of ` + + `${InstalledPackageSourceSchema.options.map((v) => `'${v}'`).join(' | ')}; received ` + + `${source === undefined ? 'none' : JSON.stringify(source)}. The door that registers a package ` + + `states where it came from; the registry does not guess.`, + ); + } // [ADR-0130 D1] The co-owners of this package — the OTHER packages the same // release artifact is delivering. Computed once and read by both halves of // the install-time pair below, so the relaxation and the refusal cannot @@ -5567,6 +5585,7 @@ export class SchemaRegistry { // equal so the structural projection reports BOTH, without teaching the // projection a key name. manifest: toRecordManifest(withDeclaredFunctionEntries(manifest)), + source, ...lifecycle, installedAt: now, updatedAt: now, diff --git a/packages/runtime/src/app-plugin.disabled-seed.test.ts b/packages/runtime/src/app-plugin.disabled-seed.test.ts index da4d35a7ee3..cc46def8338 100644 --- a/packages/runtime/src/app-plugin.disabled-seed.test.ts +++ b/packages/runtime/src/app-plugin.disabled-seed.test.ts @@ -43,7 +43,7 @@ interface InstalledPackageView { enabled?: boolean; } interface TestRegistry { - installPackage(manifest: Record): unknown; + installPackage(manifest: Record, source: 'code' | 'marketplace' | 'authored'): unknown; getPackage(id: string): InstalledPackageView | undefined; } @@ -114,7 +114,7 @@ describe('empty-env boot seeds persisted package disable-state (#5047)', () => { const { kernel, registry } = await bootEmptyEnv(); // Nothing came from the (empty) artifact; this is the post-boot // registration every package in such an env goes through. - registry.installPackage(manifestFor(DISABLED_ID)); + registry.installPackage(manifestFor(DISABLED_ID), 'code'); expect(registry.getPackage(DISABLED_ID)).toMatchObject({ status: 'disabled', @@ -128,7 +128,7 @@ describe('empty-env boot seeds persisted package disable-state (#5047)', () => { setPackageDisabled(ENVIRONMENT_ID, DISABLED_ID, true); const { kernel, registry } = await bootEmptyEnv(); - registry.installPackage(manifestFor(ENABLED_ID)); + registry.installPackage(manifestFor(ENABLED_ID), 'code'); expect(registry.getPackage(ENABLED_ID)).toMatchObject({ status: 'installed', @@ -170,7 +170,7 @@ describe('empty-env boot seeds persisted package disable-state (#5047)', () => { setPackageDisabled(ENVIRONMENT_ID, DISABLED_ID, false); const { kernel, registry } = await bootEmptyEnv(); - registry.installPackage(manifestFor(DISABLED_ID)); + registry.installPackage(manifestFor(DISABLED_ID), 'code'); expect(registry.getPackage(DISABLED_ID)).toMatchObject({ status: 'installed', @@ -182,7 +182,7 @@ describe('empty-env boot seeds persisted package disable-state (#5047)', () => { it('boots an empty env with no persisted state at all (nothing to seed)', async () => { const { kernel, registry } = await bootEmptyEnv(); - registry.installPackage(manifestFor(DISABLED_ID)); + registry.installPackage(manifestFor(DISABLED_ID), 'code'); expect(registry.getPackage(DISABLED_ID)).toMatchObject({ enabled: true }); diff --git a/packages/runtime/src/domains/automation-flow-door-package-address.test.ts b/packages/runtime/src/domains/automation-flow-door-package-address.test.ts index 3aad0426e25..599b75ec1df 100644 --- a/packages/runtime/src/domains/automation-flow-door-package-address.test.ts +++ b/packages/runtime/src/domains/automation-flow-door-package-address.test.ts @@ -135,8 +135,8 @@ async function boot() { engine.registry.registerObject(SysMetadataAuditObject as any, TEST_PACKAGE_ID); await engine.syncSchemas(); // Two installed, writable bases: no read-only manifest scope, and no code package booted them. - engine.registry.installPackage({ id: ALPHA, name: ALPHA, namespace: 'alpha', version: '1.0.0', type: 'app' } as any); - engine.registry.installPackage({ id: BETA, name: BETA, namespace: 'beta', version: '1.0.0', type: 'app' } as any); + engine.registry.installPackage({ id: ALPHA, name: ALPHA, namespace: 'alpha', version: '1.0.0', type: 'app' } as any, 'code'); + engine.registry.installPackage({ id: BETA, name: BETA, namespace: 'beta', version: '1.0.0', type: 'app' } as any, 'code'); const protocol = new ObjectStackProtocolImplementation(engine as any); const flows = new Map(); diff --git a/packages/runtime/src/domains/dispatcher-query-arity.test.ts b/packages/runtime/src/domains/dispatcher-query-arity.test.ts index be53fa13fff..338f916c7e9 100644 --- a/packages/runtime/src/domains/dispatcher-query-arity.test.ts +++ b/packages/runtime/src/domains/dispatcher-query-arity.test.ts @@ -94,8 +94,8 @@ function boot() { function bootPackages() { const registry = new SchemaRegistry({ multiTenant: false, collisionPolicy: 'error' }); (registry as any).logLevel = 'silent'; - registry.installPackage({ id: 'com.acme.app', name: 'com.acme.app', namespace: 'app', version: '1.0.0', type: 'app' } as any); - registry.installPackage({ id: 'com.acme.plugin', name: 'com.acme.plugin', namespace: 'plugin', version: '1.0.0', type: 'plugin' } as any); + registry.installPackage({ id: 'com.acme.app', name: 'com.acme.app', namespace: 'app', version: '1.0.0', type: 'app' } as any, 'code'); + registry.installPackage({ id: 'com.acme.plugin', name: 'com.acme.plugin', namespace: 'plugin', version: '1.0.0', type: 'plugin' } as any, 'code'); (registry.getPackage('com.acme.plugin') as any).status = 'error'; const getAllPackages = vi.spyOn(registry, 'getAllPackages'); const get = (n: string) => (n === 'objectql' ? { registry } : null); diff --git a/packages/runtime/src/domains/package-door-producer-key-carry.test.ts b/packages/runtime/src/domains/package-door-producer-key-carry.test.ts index 44cf0789827..d1e2d74d5ad 100644 --- a/packages/runtime/src/domains/package-door-producer-key-carry.test.ts +++ b/packages/runtime/src/domains/package-door-producer-key-carry.test.ts @@ -201,7 +201,7 @@ function realRegistry(): SchemaRegistry { objects: [{ name: 'invoice', fields: { total: { type: 'currency' } } }], apps: [{ name: 'showcase', label: 'Showcase' }], plugins: [plugin], - } as never); + } as never, 'code'); // Init AFTER install — the measured ordering: the manifest serialised // cleanly during boot and only became cyclic once the plugins came up. plugin.init(cyclicEngine()); @@ -209,11 +209,11 @@ function realRegistry(): SchemaRegistry { registry.installPackage({ id: SYSTEM_SCOPED, name: 'Setup', namespace: 'setup', version: '9.3.0', type: 'plugin', scope: 'system', - } as never); + } as never, 'code'); registry.installPackage({ id: DB_BASE, name: 'My Base', namespace: 'mybase', version: '1.0.0', type: 'app', - } as never); + } as never, 'code'); // Every declared field carries a value from here on — see // `seatDeclaredFields` for the measurement that made this necessary. diff --git a/packages/runtime/src/domains/packages-get-version-scope.test.ts b/packages/runtime/src/domains/packages-get-version-scope.test.ts index 4cbd50798aa..2a9d6f547af 100644 --- a/packages/runtime/src/domains/packages-get-version-scope.test.ts +++ b/packages/runtime/src/domains/packages-get-version-scope.test.ts @@ -82,7 +82,7 @@ const ABSENT = '99.0.0'; function make() { const registry = new SchemaRegistry({ logLevel: 'silent' } as any); - registry.installPackage({ id: PKG, name: PKG, version: INSTALLED, scope: 'project', type: 'app' } as any); + registry.installPackage({ id: PKG, name: PKG, version: INSTALLED, scope: 'project', type: 'app' } as any, 'code'); const objectql = { registry, manifests: new Map() }; const kernel: any = { context: { getService: (name: string) => (name === 'objectql' ? objectql : null) }, diff --git a/packages/runtime/src/domains/packages-install-manifest-id.test.ts b/packages/runtime/src/domains/packages-install-manifest-id.test.ts index 422f9fda11f..291dd31513c 100644 --- a/packages/runtime/src/domains/packages-install-manifest-id.test.ts +++ b/packages/runtime/src/domains/packages-install-manifest-id.test.ts @@ -333,7 +333,7 @@ describe('§2 a conforming id still installs', () => { // The gate sits ahead of BOTH limbs, so the fallback limb is reached by // a conforming manifest exactly as before this card. expect(r.response?.status).toBe(201); - expect(registryInstall).toHaveBeenCalledWith(WELL_FORMED, undefined); + expect(registryInstall).toHaveBeenCalledWith(WELL_FORMED, 'authored', undefined); }); }); diff --git a/packages/runtime/src/domains/packages-install-manifest-version.test.ts b/packages/runtime/src/domains/packages-install-manifest-version.test.ts index 4641c36991d..f1c921ef1fa 100644 --- a/packages/runtime/src/domains/packages-install-manifest-version.test.ts +++ b/packages/runtime/src/domains/packages-install-manifest-version.test.ts @@ -288,7 +288,7 @@ describe('§2 a well-formed manifest still installs', () => { // The gate sits ahead of BOTH limbs, so the fallback limb is reached by // a well-formed manifest exactly as before this card. expect(r.response?.status).toBe(201); - expect(registryInstall).toHaveBeenCalledWith(WELL_FORMED, undefined); + expect(registryInstall).toHaveBeenCalledWith(WELL_FORMED, 'authored', undefined); }); }); diff --git a/packages/runtime/src/domains/packages-list-enabled-filter.test.ts b/packages/runtime/src/domains/packages-list-enabled-filter.test.ts index e4de12442a0..67a8d31cc6b 100644 --- a/packages/runtime/src/domains/packages-list-enabled-filter.test.ts +++ b/packages/runtime/src/domains/packages-list-enabled-filter.test.ts @@ -93,7 +93,7 @@ function fixtureRegistry(): SchemaRegistry { const install = (id: string, type: string) => registry.installPackage({ id, name: id, namespace: id.split('.').pop(), version: '1.0.0', type, - } as any); + } as any, 'code'); install(ENABLED_APP, 'app'); install(DISABLED_APP, 'app'); diff --git a/packages/runtime/src/domains/packages-orgless-grants-capability-gate.test.ts b/packages/runtime/src/domains/packages-orgless-grants-capability-gate.test.ts index 33e92db132f..a1830e86a35 100644 --- a/packages/runtime/src/domains/packages-orgless-grants-capability-gate.test.ts +++ b/packages/runtime/src/domains/packages-orgless-grants-capability-gate.test.ts @@ -133,7 +133,7 @@ const SESSIONS: Record