diff --git a/openspec/changes/bootstrap-openbuilt/.openspec.yaml b/openspec/changes/archive/2026-05-12-bootstrap-openbuilt/.openspec.yaml similarity index 100% rename from openspec/changes/bootstrap-openbuilt/.openspec.yaml rename to openspec/changes/archive/2026-05-12-bootstrap-openbuilt/.openspec.yaml diff --git a/openspec/changes/bootstrap-openbuilt/design.md b/openspec/changes/archive/2026-05-12-bootstrap-openbuilt/design.md similarity index 100% rename from openspec/changes/bootstrap-openbuilt/design.md rename to openspec/changes/archive/2026-05-12-bootstrap-openbuilt/design.md diff --git a/openspec/changes/bootstrap-openbuilt/proposal.md b/openspec/changes/archive/2026-05-12-bootstrap-openbuilt/proposal.md similarity index 100% rename from openspec/changes/bootstrap-openbuilt/proposal.md rename to openspec/changes/archive/2026-05-12-bootstrap-openbuilt/proposal.md diff --git a/openspec/changes/bootstrap-openbuilt/specs/openbuilt-application-register/spec.md b/openspec/changes/archive/2026-05-12-bootstrap-openbuilt/specs/openbuilt-application-register/spec.md similarity index 100% rename from openspec/changes/bootstrap-openbuilt/specs/openbuilt-application-register/spec.md rename to openspec/changes/archive/2026-05-12-bootstrap-openbuilt/specs/openbuilt-application-register/spec.md diff --git a/openspec/changes/bootstrap-openbuilt/specs/openbuilt-runtime/spec.md b/openspec/changes/archive/2026-05-12-bootstrap-openbuilt/specs/openbuilt-runtime/spec.md similarity index 100% rename from openspec/changes/bootstrap-openbuilt/specs/openbuilt-runtime/spec.md rename to openspec/changes/archive/2026-05-12-bootstrap-openbuilt/specs/openbuilt-runtime/spec.md diff --git a/openspec/changes/bootstrap-openbuilt/tasks.md b/openspec/changes/archive/2026-05-12-bootstrap-openbuilt/tasks.md similarity index 100% rename from openspec/changes/bootstrap-openbuilt/tasks.md rename to openspec/changes/archive/2026-05-12-bootstrap-openbuilt/tasks.md diff --git a/openspec/changes/openbuilt-export-to-real-app/.openspec.yaml b/openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/.openspec.yaml similarity index 100% rename from openspec/changes/openbuilt-export-to-real-app/.openspec.yaml rename to openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/.openspec.yaml diff --git a/openspec/changes/openbuilt-export-to-real-app/README.md b/openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/README.md similarity index 100% rename from openspec/changes/openbuilt-export-to-real-app/README.md rename to openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/README.md diff --git a/openspec/changes/openbuilt-export-to-real-app/design.md b/openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/design.md similarity index 100% rename from openspec/changes/openbuilt-export-to-real-app/design.md rename to openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/design.md diff --git a/openspec/changes/openbuilt-export-to-real-app/proposal.md b/openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/proposal.md similarity index 100% rename from openspec/changes/openbuilt-export-to-real-app/proposal.md rename to openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/proposal.md diff --git a/openspec/changes/openbuilt-export-to-real-app/specs/openbuilt-exporter/spec.md b/openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/specs/openbuilt-exporter/spec.md similarity index 100% rename from openspec/changes/openbuilt-export-to-real-app/specs/openbuilt-exporter/spec.md rename to openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/specs/openbuilt-exporter/spec.md diff --git a/openspec/changes/openbuilt-export-to-real-app/tasks.md b/openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/tasks.md similarity index 100% rename from openspec/changes/openbuilt-export-to-real-app/tasks.md rename to openspec/changes/archive/2026-05-12-openbuilt-export-to-real-app/tasks.md diff --git a/openspec/changes/openbuilt-rbac/.openspec.yaml b/openspec/changes/archive/2026-05-12-openbuilt-rbac/.openspec.yaml similarity index 100% rename from openspec/changes/openbuilt-rbac/.openspec.yaml rename to openspec/changes/archive/2026-05-12-openbuilt-rbac/.openspec.yaml diff --git a/openspec/changes/openbuilt-rbac/design.md b/openspec/changes/archive/2026-05-12-openbuilt-rbac/design.md similarity index 100% rename from openspec/changes/openbuilt-rbac/design.md rename to openspec/changes/archive/2026-05-12-openbuilt-rbac/design.md diff --git a/openspec/changes/openbuilt-rbac/proposal.md b/openspec/changes/archive/2026-05-12-openbuilt-rbac/proposal.md similarity index 100% rename from openspec/changes/openbuilt-rbac/proposal.md rename to openspec/changes/archive/2026-05-12-openbuilt-rbac/proposal.md diff --git a/openspec/changes/openbuilt-rbac/specs/openbuilt-application-register/spec.md b/openspec/changes/archive/2026-05-12-openbuilt-rbac/specs/openbuilt-application-register/spec.md similarity index 100% rename from openspec/changes/openbuilt-rbac/specs/openbuilt-application-register/spec.md rename to openspec/changes/archive/2026-05-12-openbuilt-rbac/specs/openbuilt-application-register/spec.md diff --git a/openspec/changes/openbuilt-rbac/specs/openbuilt-rbac/spec.md b/openspec/changes/archive/2026-05-12-openbuilt-rbac/specs/openbuilt-rbac/spec.md similarity index 100% rename from openspec/changes/openbuilt-rbac/specs/openbuilt-rbac/spec.md rename to openspec/changes/archive/2026-05-12-openbuilt-rbac/specs/openbuilt-rbac/spec.md diff --git a/openspec/changes/openbuilt-rbac/specs/openbuilt-runtime/spec.md b/openspec/changes/archive/2026-05-12-openbuilt-rbac/specs/openbuilt-runtime/spec.md similarity index 100% rename from openspec/changes/openbuilt-rbac/specs/openbuilt-runtime/spec.md rename to openspec/changes/archive/2026-05-12-openbuilt-rbac/specs/openbuilt-runtime/spec.md diff --git a/openspec/changes/openbuilt-rbac/tasks.md b/openspec/changes/archive/2026-05-12-openbuilt-rbac/tasks.md similarity index 100% rename from openspec/changes/openbuilt-rbac/tasks.md rename to openspec/changes/archive/2026-05-12-openbuilt-rbac/tasks.md diff --git a/openspec/changes/openbuilt-schema-editor/.openspec.yaml b/openspec/changes/archive/2026-05-12-openbuilt-schema-editor/.openspec.yaml similarity index 100% rename from openspec/changes/openbuilt-schema-editor/.openspec.yaml rename to openspec/changes/archive/2026-05-12-openbuilt-schema-editor/.openspec.yaml diff --git a/openspec/changes/openbuilt-schema-editor/design.md b/openspec/changes/archive/2026-05-12-openbuilt-schema-editor/design.md similarity index 100% rename from openspec/changes/openbuilt-schema-editor/design.md rename to openspec/changes/archive/2026-05-12-openbuilt-schema-editor/design.md diff --git a/openspec/changes/openbuilt-schema-editor/proposal.md b/openspec/changes/archive/2026-05-12-openbuilt-schema-editor/proposal.md similarity index 100% rename from openspec/changes/openbuilt-schema-editor/proposal.md rename to openspec/changes/archive/2026-05-12-openbuilt-schema-editor/proposal.md diff --git a/openspec/changes/openbuilt-schema-editor/specs/openbuilt-runtime/spec.md b/openspec/changes/archive/2026-05-12-openbuilt-schema-editor/specs/openbuilt-runtime/spec.md similarity index 100% rename from openspec/changes/openbuilt-schema-editor/specs/openbuilt-runtime/spec.md rename to openspec/changes/archive/2026-05-12-openbuilt-schema-editor/specs/openbuilt-runtime/spec.md diff --git a/openspec/changes/openbuilt-schema-editor/specs/openbuilt-schema-designer/spec.md b/openspec/changes/archive/2026-05-12-openbuilt-schema-editor/specs/openbuilt-schema-designer/spec.md similarity index 100% rename from openspec/changes/openbuilt-schema-editor/specs/openbuilt-schema-designer/spec.md rename to openspec/changes/archive/2026-05-12-openbuilt-schema-editor/specs/openbuilt-schema-designer/spec.md diff --git a/openspec/changes/openbuilt-schema-editor/tasks.md b/openspec/changes/archive/2026-05-12-openbuilt-schema-editor/tasks.md similarity index 100% rename from openspec/changes/openbuilt-schema-editor/tasks.md rename to openspec/changes/archive/2026-05-12-openbuilt-schema-editor/tasks.md diff --git a/openspec/changes/openbuilt-versioning/.openspec.yaml b/openspec/changes/archive/2026-05-12-openbuilt-versioning/.openspec.yaml similarity index 100% rename from openspec/changes/openbuilt-versioning/.openspec.yaml rename to openspec/changes/archive/2026-05-12-openbuilt-versioning/.openspec.yaml diff --git a/openspec/changes/openbuilt-versioning/design.md b/openspec/changes/archive/2026-05-12-openbuilt-versioning/design.md similarity index 100% rename from openspec/changes/openbuilt-versioning/design.md rename to openspec/changes/archive/2026-05-12-openbuilt-versioning/design.md diff --git a/openspec/changes/openbuilt-versioning/proposal.md b/openspec/changes/archive/2026-05-12-openbuilt-versioning/proposal.md similarity index 100% rename from openspec/changes/openbuilt-versioning/proposal.md rename to openspec/changes/archive/2026-05-12-openbuilt-versioning/proposal.md diff --git a/openspec/changes/openbuilt-versioning/specs/openbuilt-application-register/spec.md b/openspec/changes/archive/2026-05-12-openbuilt-versioning/specs/openbuilt-application-register/spec.md similarity index 100% rename from openspec/changes/openbuilt-versioning/specs/openbuilt-application-register/spec.md rename to openspec/changes/archive/2026-05-12-openbuilt-versioning/specs/openbuilt-application-register/spec.md diff --git a/openspec/changes/openbuilt-versioning/specs/openbuilt-runtime/spec.md b/openspec/changes/archive/2026-05-12-openbuilt-versioning/specs/openbuilt-runtime/spec.md similarity index 100% rename from openspec/changes/openbuilt-versioning/specs/openbuilt-runtime/spec.md rename to openspec/changes/archive/2026-05-12-openbuilt-versioning/specs/openbuilt-runtime/spec.md diff --git a/openspec/changes/openbuilt-versioning/specs/openbuilt-version-snapshots/spec.md b/openspec/changes/archive/2026-05-12-openbuilt-versioning/specs/openbuilt-version-snapshots/spec.md similarity index 100% rename from openspec/changes/openbuilt-versioning/specs/openbuilt-version-snapshots/spec.md rename to openspec/changes/archive/2026-05-12-openbuilt-versioning/specs/openbuilt-version-snapshots/spec.md diff --git a/openspec/changes/openbuilt-versioning/tasks.md b/openspec/changes/archive/2026-05-12-openbuilt-versioning/tasks.md similarity index 100% rename from openspec/changes/openbuilt-versioning/tasks.md rename to openspec/changes/archive/2026-05-12-openbuilt-versioning/tasks.md diff --git a/openspec/specs/openbuilt-application-register/spec.md b/openspec/specs/openbuilt-application-register/spec.md new file mode 100644 index 00000000..c2636d37 --- /dev/null +++ b/openspec/specs/openbuilt-application-register/spec.md @@ -0,0 +1,263 @@ +# openbuilt-application-register Specification + +## Purpose +TBD - created by archiving change bootstrap-openbuilt. Update Purpose after archive. +## Requirements +### Requirement: REQ-OBA-001 Application schema registered in OpenRegister + +The system SHALL declare an `Application` schema in +`lib/Settings/openbuilt_register.json` under the `openbuilt` register +namespace. The schema SHALL define properties `uuid` (string, +UUID-format), `slug` (string, kebab-case pattern), `name` (string, +required), `description` (string, optional), `manifest` (object, +required — the manifest JSON blob), `version` (string, semver pattern, +required), and `status` (string, enum: `draft | published | archived`, +required, default `draft`). The schema SHALL be imported into +OpenRegister at app install / post-migration time via a repair step. + +#### Scenario: Schema is available after install + +- **WHEN** the OpenBuilt app is installed and its repair step runs +- **THEN** OpenRegister exposes the `openbuilt` register containing + the `Application` schema +- **AND** the schema's properties match the declaration in + `lib/Settings/openbuilt_register.json` + +#### Scenario: Application object is created via OR REST + +- **WHEN** a client POSTs a payload to OR's REST endpoint for the + `openbuilt/application` namespace with a valid `manifest`, `slug`, + `name`, `version`, and `status: draft` +- **THEN** OR persists the object, returns 201, and the returned + object carries an OR-assigned `uuid` and the submitted fields + +### Requirement: REQ-OBA-002 Manifest blob is structurally valid + +The `manifest` property of every `Application` object SHALL validate +against the canonical app-manifest schema at +`@conduction/nextcloud-vue/src/schemas/app-manifest.schema.json` +(v1.4.0 or later). The system SHALL reject save operations whose +`manifest` blob fails schema validation, returning a 4xx response that +names the failing JSON path. Validation runs both at save time +(server-side via the seeded JSON-schema reference in the OR schema's +`manifest` property) and pre-save in the textarea editor (client-side +via the `validateManifest` utility re-exported from +`@conduction/nextcloud-vue`). + +#### Scenario: Save rejects a structurally invalid manifest + +- **WHEN** a client attempts to save an Application whose `manifest` + blob omits the required `pages` array +- **THEN** the system returns a 4xx error citing the missing field +- **AND** no Application object is persisted + +#### Scenario: Save accepts a minimal valid manifest + +- **WHEN** a client saves an Application whose `manifest` validates + against the canonical schema (has `version`, `menu`, `pages`) +- **THEN** the system persists the object and returns the saved + representation + +### Requirement: REQ-OBA-003 Declarative lifecycle drives state transitions + +The `Application` schema SHALL declare its state machine via +`x-openregister-lifecycle` in +`lib/Settings/openbuilt_register.json`. The lifecycle SHALL define +three states (`draft`, `published`, `archived`) and the allowed +transitions: `draft → published`, `published → archived`, +`archived → draft` (re-open for editing). No service class (e.g. +`ApplicationLifecycleService`) SHALL be written; the lifecycle is the +canonical declarative example for this spec per ADR-031. Each +transition SHALL be recorded in OR's audit trail. + +#### Scenario: Allowed transition succeeds with an audit entry + +- **WHEN** an authorised user transitions a `draft` Application to + `published` via the lifecycle endpoint +- **THEN** the object's `status` becomes `published` +- **AND** OR's audit trail records a `lifecycle.transition` event with + the from-state, to-state, and actor identity + +#### Scenario: Disallowed transition is rejected + +- **WHEN** a client attempts to transition a `draft` Application + directly to `archived` (a transition not declared in the lifecycle) +- **THEN** the system returns a 4xx error +- **AND** the object's `status` remains `draft` +- **AND** no audit entry is recorded + +### Requirement: REQ-OBA-004 BuiltAppRoute index for slug lookup + +The system SHALL declare a `BuiltAppRoute` schema in +`lib/Settings/openbuilt_register.json` with properties `slug` (string, +required, kebab-case pattern) and `applicationUuid` (string, +UUID-format, required). The `slug` property SHALL be unique within an +organisation. The repair step SHALL create or maintain a +`BuiltAppRoute` row for every published Application, keyed by its +slug, so that the runtime can resolve `slug → Application UUID` in a +single OR lookup without scanning every Application. + +#### Scenario: Publishing an Application creates a BuiltAppRoute + +- **WHEN** an Application with `slug: hello-world` transitions from + `draft` to `published` +- **THEN** a `BuiltAppRoute` object exists with `slug: hello-world` + and `applicationUuid` matching the Application's UUID + +#### Scenario: Slug uniqueness is enforced per organisation + +- **WHEN** a client attempts to publish a second Application with + `slug: hello-world` in the same organisation +- **THEN** the system returns a 4xx error citing the slug conflict +- **AND** no second `BuiltAppRoute` is created + +### Requirement: REQ-OBA-005 Multi-tenant scoping via OR organisation + +Every `Application` and `BuiltAppRoute` object SHALL inherit +OpenRegister's `organisation` field for multi-tenant scoping. List, +read, write, and lifecycle operations SHALL only return / accept +objects in the caller's organisation scope, enforced by OR's existing +authorization layer (ADR-022 — no app-local RBAC duplication). + +#### Scenario: Cross-organisation reads are blocked + +- **WHEN** a user in organisation A requests Applications owned by + organisation B +- **THEN** OR returns an empty list (or a 403, per its standard + contract) — the cross-org objects are not visible + +### Requirement: REQ-OBA-006 Application schema carries a currentVersion reference + +The `Application` schema declared in `lib/Settings/openbuilt_register.json` (REQ-OBA-001) SHALL be extended with a `currentVersion` property of type string with UUID-format. The property SHALL be optional (an Application that has never been published has no `currentVersion`). When populated, it SHALL hold the `uuid` of the most recent `ApplicationVersion` row for this Application (see capability `openbuilt-version-snapshots`, REQ-OBV-006). The schema change SHALL remain backward-compatible: existing Applications imported from spec #1 carry no `currentVersion` and SHALL continue to load, list, and edit without error. + +#### Scenario: Existing Applications remain valid without currentVersion + +- **WHEN** the OpenBuilt repair step runs an upgrade on an install + that already has seeded Applications from spec #1 +- **THEN** those Applications continue to load via OR REST +- **AND** their `currentVersion` field is absent or `null` +- **AND** the textarea editor renders them without validation + errors + +#### Scenario: currentVersion is updated atomically with the snapshot + +- **WHEN** an Application transitions from `draft` to `published` +- **THEN** the same lifecycle action that creates the + `ApplicationVersion` row also writes the new row's `uuid` into + the Application's `currentVersion` +- **AND** both writes are observed by a subsequent OR REST GET of + the Application + +### Requirement: REQ-OBA-007 Draft-to-published transition declares a snapshot action + +The `x-openregister-lifecycle` block on the `Application` schema (REQ-OBA-003) SHALL declare an `on_transition` action on the `draft → published` edge that creates a new `ApplicationVersion` row populated from the Application's current `manifest`, `version`, the actor's NC user id, and the transition timestamp; updates the Application's `currentVersion` to the new row's `uuid`; and sets the Application's `status` back to `draft` so that the next edit session continues from a draft state, while the just-created `ApplicationVersion` serves as the "published" record (see design.md Decision 3 for rationale). + +If OR's lifecycle engine cannot yet express a sibling-object create action in `on_transition`, the action MAY be implemented as a single PHP listener subscribed to `ObjectLifecycleTransitionedEvent` per ADR-031 §Exceptions(1) — mirroring the OQ-1 escape hatch bootstrap-openbuilt established. The observed behaviour SHALL be identical in either case. The implementer SHALL NOT introduce a generic `VersioningService` / `SnapshotService` class. + +#### Scenario: Declarative path emits the snapshot + +- **WHEN** OR's engine supports `on_transition.create_relation` (or + equivalent) +- **AND** an Application transitions from `draft` to `published` +- **THEN** a snapshot is created without any custom PHP listener + being invoked +- **AND** the OR audit trail records both the transition and the + snapshot create + +#### Scenario: Listener fallback produces the same outcome + +- **WHEN** OR's engine does not yet expose the sibling-create action + and the fallback `ApplicationVersionSnapshotListener` is registered +- **AND** an Application transitions from `draft` to `published` +- **THEN** the listener creates the `ApplicationVersion` row, + updates `currentVersion`, and resets the Application's `status` + to `draft` +- **AND** the resulting Application + ApplicationVersion records + are byte-equal (modulo `uuid` and timestamps) to the declarative + path + +### Requirement: REQ-OBA-006 Application schema carries a permissions block + +The system SHALL extend the `Application` schema in `lib/Settings/openbuilt_register.json` with an optional `permissions` property of shape: + +```json +{ + "permissions": { + "type": "object", + "properties": { + "owners": { "type": "array", "items": { "type": "string" } }, + "editors": { "type": "array", "items": { "type": "string" } }, + "viewers": { "type": "array", "items": { "type": "string" } } + }, + "additionalProperties": false + } +} +``` + +Each array element is a Nextcloud group ID (`gid`) string. The +property is optional in the schema so that legacy Applications +created by spec #1's repair step (the seeded `hello-world` +Application) remain schema-valid; a migration step (see +REQ-OBA-007) populates a default value for every existing +Application on apply. New Applications created after this spec +lands carry `permissions` from the moment of creation by virtue of +REQ-OBRBAC-001 in the `openbuilt-rbac` capability. The OpenBuilt +repair step that imports the register configuration SHALL update +the schema in place idempotently via +`ConfigurationService::importFromApp()` (memory rule). No new +schema is introduced; the `permissions` property is a declarative +addition to `Application` per ADR-031 (no service class). + +#### Scenario: Schema declares the permissions property after install + +- **WHEN** the OpenBuilt app is installed (or upgraded) and its + repair step runs +- **THEN** the `Application` schema in the `openbuilt` register + exposes the `permissions` property with the shape above +- **AND** the property is omittable (legacy Application objects + without it remain schema-valid) + +#### Scenario: Saving an Application with a permissions block round-trips + +- **WHEN** a client PUTs an Application via OR REST with + `permissions = { owners: ["team-alpha"], editors: ["qa-alpha"], viewers: [] }` +- **THEN** OR persists the object and a subsequent GET returns the + same `permissions` block byte-for-byte + +#### Scenario: Saving with extra properties is rejected + +- **WHEN** a client PUTs an Application with + `permissions = { owners: ["x"], admins: ["y"] }` (note the + unknown `admins` key) +- **THEN** OR rejects the save with a 4xx citing the unknown + property under `permissions` + +### Requirement: REQ-OBA-007 Migration populates permissions for pre-existing Applications + +The OpenBuilt repair step SHALL include an idempotent migration +that, for every existing `Application` object whose `permissions` +property is missing or null, populates `permissions.owners` with the +system organisation's `admin` group, and sets `editors` and +`viewers` to empty arrays. The migration SHALL skip any Application +that already has a non-empty `permissions.owners`. The seeded +`hello-world` Application from spec #1 (which has no `permissions` +field) is the canonical case the migration covers; after this +spec's apply phase, every Application in every installed instance +has a populated `permissions` field. + +#### Scenario: Pre-existing Application receives a default permissions block + +- **GIVEN** an existing Application with `slug: hello-world` and no + `permissions` field (the spec #1 seed) +- **WHEN** this spec's repair step runs +- **THEN** the Application's `permissions.owners` contains the + `admin` group of its organisation +- **AND** `permissions.editors = []` and `permissions.viewers = []` + +#### Scenario: Migration is idempotent + +- **WHEN** the migration runs a second time on an already-migrated + install +- **THEN** no Application is changed +- **AND** no duplicate audit entries are produced + diff --git a/openspec/specs/openbuilt-exporter/spec.md b/openspec/specs/openbuilt-exporter/spec.md new file mode 100644 index 00000000..8fbd2e8f --- /dev/null +++ b/openspec/specs/openbuilt-exporter/spec.md @@ -0,0 +1,387 @@ +# openbuilt-exporter Specification + +## Purpose +TBD - created by archiving change openbuilt-export-to-real-app. Update Purpose after archive. +## Requirements +### Requirement: ExportJob schema declaration + +The system SHALL declare an `ExportJob` schema in +`lib/Settings/openbuilt_register.json` (OpenAPI 3.0.0) carrying the +properties `uuid`, `applicationUuid` (UUID-format, required), +`applicationVersion` (semver-pattern, required), `target` +(enum `zip|github`, required), `status` (enum +`queued|running|succeeded|failed`, default `queued`, required), +`githubOrg` (string, optional), `githubRepo` (string, optional), +`githubVisibility` (enum `public|private`, optional), +`includeSeedData` (boolean, default `false`), `downloadUrl` (string, +optional), `downloadExpiresAt` (date-time, optional), +`errorMessage` (string, optional), `log` (array of strings, +optional, append-only progress notes). The schema SHALL declare +`x-openregister-lifecycle` with the +`queued → running → succeeded|failed` state machine (no terminal +re-entry; `failed → queued` permitted only via explicit retry). + +#### Scenario: Schema validates a well-formed ExportJob object + +- **WHEN** an integrator POSTs an ExportJob with + `applicationUuid`, `applicationVersion: "1.0.0"`, `target: "zip"` + to OR REST +- **THEN** OR creates the object with `status: "queued"` and a + fresh `uuid`, and the OR audit trail records the creation event. + +#### Scenario: Schema rejects an invalid target + +- **WHEN** an integrator POSTs an ExportJob with + `target: "ftp"` +- **THEN** OR returns a 4xx validation error referencing the enum + constraint on `target` and the ExportJob is NOT created. + +#### Scenario: Disallowed lifecycle transition rejected + +- **WHEN** the system attempts to transition an ExportJob from + `succeeded` back to `running` +- **THEN** the OR lifecycle engine rejects the transition with a + 4xx error and the audit trail records the attempt. + +--- + +### Requirement: Export targets a specific Application version + +The export pipeline SHALL operate on a **specific published version** +of an Application — never on the in-flight draft. The frontend dialog +SHALL default the version field to the Application's current +`published` version (per `openbuilt-versioning`). The system SHALL +reject an export request whose `applicationVersion` does not match any +known published version of the referenced Application. + +#### Scenario: Default version is the current published version + +- **WHEN** the user opens the Export dialog for an Application whose + current published version is `1.2.0` +- **THEN** the dialog's version field is pre-filled with `1.2.0`. + +#### Scenario: Reject export of an unknown version + +- **WHEN** the user submits an export with + `applicationVersion: "9.9.9"` and no such published version exists +- **THEN** the controller returns 422 with an error message naming + the unknown version and no ExportJob is created. + +#### Scenario: Reject export of a draft + +- **WHEN** the user submits an export with an `applicationVersion` + that resolves to a `draft` (not `published`) snapshot +- **THEN** the controller returns 422 with an error message + indicating drafts cannot be exported. + +--- + +### Requirement: Exported tree shape conforms to the nextcloud-app-template baseline + +The exported archive SHALL contain a directory tree matching the +snapshot of `nextcloud-app-template` embedded under +`lib/Resources/template/`, with every placeholder +(`{{appId}}`, `{{appNamespace}}`, `{{appName}}`, +`{{appDescription}}`, `{{appVersion}}`, `{{authorName}}`, +`{{authorEmail}}`, `{{license}}`) replaced by values derived from +the source Application's manifest + ExportJob inputs. The tree +SHALL include at minimum: + +- `appinfo/info.xml` carrying the new id, namespace, version, + navigation entry, and dependencies declared by the source + manifest. +- `lib/AppInfo/Application.php` with the new namespace. +- `lib/Settings/_register.json` carrying the companion + schemas referenced by the manifest, slug-prefixed where the + source uses the shared `openbuilt` namespace. +- `lib/Repair/InitializeSettings.php` invoking + `ConfigurationService::importFromApp()` against the new + register. +- `src/manifest.json` — the source Application's manifest blob, + with its `version` field set to the exported `applicationVersion`. +- `src/main.js` mounting `` via + `useAppManifest('', bundledManifest)` (Tier-4 pattern). +- `src/App.vue` shell. +- `package.json` with deps (Vue 2.7, `@conduction/nextcloud-vue`, + `@nextcloud/vue`, build tooling) carried over from the snapshot. +- `composer.json` with PHP deps + the Conduction PHPCS / PHPMD / + Psalm / PHPStan / PHPUnit toolchain carried over. +- `.github/workflows/code-quality.yml`, + `.github/workflows/release-stable.yml`, + `.github/workflows/release-beta.yml` — Conduction-standard + pipelines from the snapshot, with `{{appId}}` placeholders + resolved. +- `README.md`, `LICENSE` (defaulting to EUPL-1.2; user-overridable + per Decision 6 of `design.md`), `phpcs.xml`, `phpmd.xml`, + `psalm.xml`, `phpstan.neon`, `phpunit.xml`. + +#### Scenario: Tree shape matches the snapshot + +- **WHEN** an export against a minimal manifest completes +- **THEN** unzipping the archive yields every path listed in the + embedded template's path manifest, with no unresolved + `{{placeholder}}` tokens remaining in any text file. + +#### Scenario: info.xml carries the manifest's navigation entry + +- **WHEN** the source manifest declares a menu entry + `{ id: "Things", label: "...", route: "Things" }` +- **THEN** the exported `appinfo/info.xml` contains a corresponding + `` declaration whose `id` matches the + exported appId and whose `name` matches the manifest entry's + label. + +--- + +### Requirement: Companion schemas migrate into the exported app's own namespace + +The exporter SHALL emit a `lib/Settings/_register.json` +declaring a fresh OR register namespace named after the exported +appId, and SHALL relocate every companion schema referenced by the +source manifest from OpenBuilt's `openbuilt` namespace into that +new namespace. The exporter SHALL rewrite every +`config.register` / `config.schema` reference inside the embedded +`src/manifest.json` so the exported app reads from its own +register, not from `openbuilt`. The exporter SHALL NOT copy the +`Application`, `BuiltAppRoute`, or `ExportJob` schemas into the +new register (those are OpenBuilt's internal machinery). + +#### Scenario: Manifest references rewritten to the new namespace + +- **WHEN** the source manifest references + `{ register: "openbuilt", schema: "hello-message" }` on a page + config +- **THEN** the exported `src/manifest.json` references + `{ register: "hello-world", schema: "hello-message" }` (assuming + exported appId `hello-world`). + +#### Scenario: OpenBuilt internals excluded from the exported register + +- **WHEN** the exporter writes + `lib/Settings/_register.json` +- **THEN** the file contains the companion schemas referenced by + the manifest but contains NO `Application`, `BuiltAppRoute`, or + `ExportJob` schema entries. + +--- + +### Requirement: Exported manifest is bundled and Tier-4 + +The exported `src/manifest.json` SHALL be the **sole** manifest +source for the exported app — there SHALL NOT be a per-slug manifest +endpoint, an `options.fetcher` redirect, or any other runtime +indirection (the workaround documented in bootstrap-openbuilt +Decision 4 collapses for the exported app because it owns exactly +one manifest). The generated `src/main.js` SHALL call +`useAppManifest('', bundledManifest)` with the bundled blob +directly. The exported app SHALL NOT mount a nested `CnAppRoot` — +its `CnAppRoot` is the top-level mount (the nested-mount +arrangement of bootstrap-openbuilt Decision 5 collapses for the +same reason). + +#### Scenario: Generated main.js mounts CnAppRoot at top level + +- **WHEN** an export completes and `src/main.js` is inspected +- **THEN** the file contains `useAppManifest('', + bundledManifest)` and the `` mount is on + `#content`, with no parent `` wrapper. + +#### Scenario: No manifest endpoint exists in the exported app + +- **WHEN** the exported `appinfo/routes.php` is inspected +- **THEN** the file contains NO route mapping to a + `getManifest` controller method. + +--- + +### Requirement: Export target — ZIP archive + +When the user selects target `zip`, the system SHALL produce a +single `.zip` file containing the full exported tree, store it in +Nextcloud's app-data area under +`appdata_/openbuilt/exports//`, set the +ExportJob's `downloadUrl` to +`/index.php/apps/openbuilt/api/exports/{uuid}/download`, set +`downloadExpiresAt` to 24 hours after job completion, and transition +the job to `succeeded`. After expiry, the download endpoint SHALL +return 410 Gone and the archive SHALL be purged by a daily +cleanup background job. + +#### Scenario: ZIP download succeeds within 24h + +- **WHEN** the user requests an export with target `zip` and the + job completes 5 minutes ago +- **THEN** GETting `downloadUrl` returns a 200 with + `Content-Type: application/zip` and a body whose unzip is + byte-equivalent to the produced archive. + +#### Scenario: ZIP download expires after 24h + +- **WHEN** the user requests the same `downloadUrl` 25 hours after + job completion +- **THEN** the endpoint returns 410 Gone and the archive has been + removed from app-data. + +--- + +### Requirement: Export target — GitHub repository + +When the user selects target `github`, the system SHALL: + +1. Create a new GitHub repository under the user-supplied org with + the user-supplied name and visibility (`public` or `private`). +2. Push the exported tree as an initial commit on a `bootstrap` + branch. +3. Open a pull request from `bootstrap` to the repo's default + branch (`development` if the org's standard ruleset prescribes + it, otherwise `main`) with a placeholder title + `"chore: bootstrap from OpenBuilt"` and a body linking back to + the source OpenBuilt Application. +4. Populate the ExportJob's `downloadUrl` field with the resulting + PR URL. + +The GitHub PAT SHALL be provided once by the user in the export +dialog and SHALL be stored exclusively via Nextcloud's +`ICredentialsManager`. The PAT SHALL NOT be persisted on the +ExportJob object, in plaintext logs, or in any +`x-openregister-lifecycle` audit field. Token usage SHALL be +scoped to the single export run; the credential record SHALL be +deleted on job terminal state (succeeded or failed). + +#### Scenario: GitHub export creates repo + PR + +- **WHEN** the user submits an export with `target: github`, org + `acme-co`, repo `hello-world`, visibility `public`, and a valid + PAT +- **THEN** the job completes with `status: succeeded`, + `downloadUrl` set to the PR URL, the repo exists at + `github.com/acme-co/hello-world`, the `bootstrap` branch + contains the exported tree, and a PR is open against the + default branch. + +#### Scenario: PAT is wiped on job terminal state + +- **WHEN** an ExportJob reaches `succeeded` or `failed` +- **THEN** no record of the PAT exists in + `ICredentialsManager` for that job's key. + +#### Scenario: Auth failure surfaces in errorMessage + +- **WHEN** the user submits an export with an invalid PAT +- **THEN** the job transitions to `failed`, `errorMessage` + contains a human-readable auth-failure summary (without echoing + the PAT), and no repo is created. + +--- + +### Requirement: Export is asynchronous via Nextcloud's IJob + +The exporter SHALL run as a Nextcloud background job +(`lib/BackgroundJob/RunExportJob.php` implementing +`OCP\BackgroundJob\IJob`) registered in `appinfo/info.xml`. The +`POST /api/applications/{slug}/exports` endpoint SHALL return 202 +Accepted immediately with the ExportJob's UUID, and the background +job SHALL pick up the queued job on its next tick (or sooner if +Nextcloud's job scheduler is configured for immediate dispatch). +The frontend SHALL poll the ExportJob via OR REST every 2 seconds +until terminal state. + +#### Scenario: POST returns 202 immediately + +- **WHEN** the user submits an export +- **THEN** the controller returns 202 in under 500ms with the + ExportJob UUID in the response body. + +#### Scenario: Background job advances the ExportJob + +- **WHEN** the background job runs against a `queued` ExportJob +- **THEN** the job transitions through `running` to + `succeeded` (or `failed`) and the `log` array gains entries + describing the major phases (`template-copy`, + `placeholder-replacement`, `manifest-bundling`, + `schema-emission`, `archive-or-push`, `complete`). + +--- + +### Requirement: Re-exports are idempotent + +The system SHALL ensure that re-exporting the same Application version with the same +`includeSeedData` flag produces a byte-equivalent ZIP archive. +The exporter SHALL NOT embed creation timestamps, random UUIDs, or +the running OpenBuilt instance's identity into any text file +committed to the exported tree. The PHP `composer.json` and JS +`package.json` SHALL pin dependency versions identically across +runs. + +#### Scenario: Two ZIPs of the same version match byte-for-byte + +- **WHEN** the user exports `applicationVersion: 1.0.0` twice in + a row with the same `includeSeedData` value +- **THEN** the two resulting ZIPs are byte-equivalent (or, if a + modern ZIP tool's timestamp encoding precludes byte equality, + their unzipped trees produce identical SHA-256 file digests). + +#### Scenario: GitHub re-export against an existing repo fails fast + +- **WHEN** the user re-exports to GitHub with the same + `githubOrg` + `githubRepo` that already exist +- **THEN** the job transitions to `failed` with + `errorMessage: "Repository / already exists"` and no + destructive push is attempted. + +--- + +### Requirement: Optional seed-data inclusion + +When `includeSeedData: true`, the exporter SHALL include a +`lib/Repair/SeedSampleData.php` step in the exported tree that +seeds the sample objects currently held in the source +Application's namespace into the exported app's namespace on +first install. The repair step SHALL guard on existing-object +identity to remain idempotent across re-installs. + +#### Scenario: Seed data appears in the exported tree when toggled on + +- **WHEN** the user exports an Application whose namespace + contains three sample `hello-message` objects with + `includeSeedData: true` +- **THEN** the exported tree contains + `lib/Repair/SeedSampleData.php` carrying those three objects' + payloads, registered as a `` step in + `appinfo/info.xml`. + +#### Scenario: Seed data omitted when toggled off + +- **WHEN** the user exports the same Application with + `includeSeedData: false` +- **THEN** the exported tree contains NO `SeedSampleData.php` + file and no `` reference to it in + `appinfo/info.xml`. + +--- + +### Requirement: Exported app boots standalone with zero OpenBuilt dependency + +The system SHALL ensure that the exported app, when installed in a Nextcloud +instance that does NOT have OpenBuilt installed, boots to a working +`CnAppRoot`-rendered surface using only its bundled +`src/manifest.json` + companion register + standard Conduction +runtime dependencies. The exported `composer.json`, +`package.json`, and `appinfo/info.xml` SHALL NOT reference +`openbuilt` as a dependency, peer dependency, or required app. + +#### Scenario: Exported app installs without OpenBuilt + +- **WHEN** the exported app is enabled on a Nextcloud instance + that has OpenRegister installed but NOT OpenBuilt +- **THEN** the app's top-bar entry appears, navigating to it + renders the manifest-driven index page, and no error logs + reference a missing `openbuilt` dependency. + +#### Scenario: No openbuilt string in exported dependency files + +- **WHEN** the exported `composer.json`, `package.json`, and + `appinfo/info.xml` are inspected +- **THEN** none of them contains the substring `openbuilt` + (case-insensitive) as a dependency reference. + diff --git a/openspec/specs/openbuilt-rbac/spec.md b/openspec/specs/openbuilt-rbac/spec.md new file mode 100644 index 00000000..faf0c8b7 --- /dev/null +++ b/openspec/specs/openbuilt-rbac/spec.md @@ -0,0 +1,247 @@ +# openbuilt-rbac Specification + +## Purpose +TBD - created by archiving change openbuilt-rbac. Update Purpose after archive. +## Requirements +### Requirement: REQ-OBRBAC-001 Permissions field shape and default on creation + +The system SHALL extend the `Application` schema with an optional +`permissions` property of shape +`{ owners: string[], editors: string[], viewers: string[] }` where +each array element is a Nextcloud group ID (`gid`). The arrays MAY be +empty. When a new `Application` is created without an explicit +`permissions` value, the system SHALL default `permissions.owners` to +an array containing the **creator's primary Nextcloud group** +(`IUserSession::getUser()->getUID()`'s first group from +`IGroupManager::getUserGroups()`); `editors` and `viewers` SHALL +default to empty arrays. If the creator has no group membership, the +system SHALL fall back to the `admin` group as the sole owner so the +Application is never created in an unreachable "no owner" state. + +#### Scenario: New Application gets creator's primary group as owner + +- **WHEN** a user whose primary group is `team-alpha` creates a new + `Application` via OR REST without sending a `permissions` field +- **THEN** the persisted Application has + `permissions.owners = ["team-alpha"]`, `permissions.editors = []`, + and `permissions.viewers = []` + +#### Scenario: Groupless creator falls back to admin + +- **WHEN** a user with no Nextcloud group memberships creates a new + `Application` without sending a `permissions` field +- **THEN** the persisted Application has + `permissions.owners = ["admin"]` +- **AND** the user is recorded as the actor in the OR audit trail + +### Requirement: REQ-OBRBAC-002 Manifest endpoint enforces role membership + +The system SHALL augment +`GET /index.php/apps/openbuilt/api/applications/{slug}/manifest` so +that, after the existing organisation-scope check passes and the +Application is resolved, the controller SHALL verify the caller is a +member of at least one group present in +`permissions.owners ∪ permissions.editors ∪ permissions.viewers`. If +the caller has no group intersection with the Application's +`permissions` (and is not a Nextcloud admin who has explicitly +elevated via the admin-bypass declared in REQ-OBRBAC-006), the +controller SHALL respond `403 Forbidden` with a JSON error body. The +check SHALL run before any other branch that would return the +manifest payload — deny-by-default per ADR-005. + +#### Scenario: Member of viewer group reads the manifest + +- **WHEN** user `bob` whose groups include `viewers-alpha` requests + the manifest for an Application whose + `permissions.viewers = ["viewers-alpha"]` +- **THEN** the response is `200 application/json` carrying the + manifest blob + +#### Scenario: Non-member cannot read the manifest + +- **WHEN** user `eve` whose groups do not intersect with any of the + Application's `permissions.owners`, `permissions.editors`, or + `permissions.viewers` requests its manifest +- **THEN** the response is `403 Forbidden` +- **AND** no part of the manifest payload appears in the response + body + +#### Scenario: 403 is returned before 404 disambiguation + +- **WHEN** an unauthorised caller probes the manifest endpoint with a + slug that does exist in their organisation but to which they have + no role +- **THEN** the response is `403`, not `404` +- **AND** the response body does not leak the Application's + `name`, `description`, or any manifest content + +### Requirement: REQ-OBRBAC-003 Application list filters out unauthorised entries + +The OpenBuilt shell's Application list view SHALL display only +Applications on which the caller has at least one role +(`owner | editor | viewer`). The filter SHALL be applied in this +order of preference: + +1. **Preferred** — declarative, via OR's authorization extension. If + OR's schema vocabulary supports an + `x-openregister-authorization` rule that expresses "caller's + group ∈ object's `permissions.owners ∪ editors ∪ viewers`", the + Application schema SHALL declare it and the OR REST list endpoint + SHALL return only matching rows; the frontend filters nothing. +2. **Fallback** — thin app-local filter. If the declarative path is + not yet supported, the frontend SHALL filter the list returned by + OR REST using the caller's group set echoed via `loadState` + (per ADR-004; no `document.getElementById().dataset` reads). + +In both paths, the user-visible behaviour is identical: unauthorised +Applications do not appear in the list. + +#### Scenario: List omits Applications without any role + +- **WHEN** user `bob` opens the OpenBuilt Application list +- **AND** the organisation contains 10 Applications, of which 3 grant + `bob`'s group at least one role +- **THEN** the rendered list shows exactly 3 entries +- **AND** the omitted 7 do not appear in the response payload + consumed by the frontend + +### Requirement: REQ-OBRBAC-004 Role-to-action mapping in editor UIs + +The system SHALL gate destructive and write actions in the OpenBuilt +editor UIs according to the following role → action mapping. Buttons +or controls that would trigger a forbidden action SHALL be hidden +(`v-if`) for `viewer` and rendered disabled (`:disabled="true"`) for +`editor` where the action requires `owner`. The mapping is the +single source of truth — all current and future editor UIs (textarea +editor today; visual editors from chain spec #5/#6 when they land) +SHALL consume the same `useRole(application)` composable. + +| Action | viewer | editor | owner | +|---|:---:|:---:|:---:| +| Read manifest / browse Application | yes | yes | yes | +| Save manifest draft | no | yes | yes | +| Publish (`draft → published`) | no | no | yes | +| Archive (`published → archived`) | no | no | yes | +| Re-open (`archived → draft`) | no | no | yes | +| Edit `permissions` | no | no | yes | +| Transfer ownership | no | no | yes | +| Delete Application | no | no | yes | + +#### Scenario: Viewer cannot save manifest edits + +- **WHEN** a user with only `viewer` role on an Application opens it + in the textarea editor +- **THEN** the textarea SHALL be rendered read-only (or the Save + button SHALL be hidden) +- **AND** any attempted PUT to the OR REST Application endpoint + SHALL be rejected (covered by REQ-OBRBAC-002 on the manifest + endpoint; OR's existing write authorization covers the OR REST + PUT) + +#### Scenario: Editor cannot publish + +- **WHEN** a user with only `editor` role on an Application opens it +- **THEN** the Save button SHALL be enabled +- **AND** the Publish button SHALL be hidden (or disabled with a + tooltip explaining "owner role required") + +### Requirement: REQ-OBRBAC-005 Transfer-ownership flow + +The system SHALL support an owner replacing the `permissions.owners` +list of an Application. The transfer SHALL be a single declarative +update to the Application's `permissions` property via OR REST — no +dedicated `TransferOwnershipService` or `transfer` endpoint. The +frontend SHALL surface a "Transfer ownership" affordance in the +permissions panel of the editor (`owner`-gated per REQ-OBRBAC-004) +that opens a group picker and PUTs the updated `permissions` block. +The system SHALL reject (`4xx`) any transfer that would result in an +empty `permissions.owners` array, preventing accidental orphaning. + +#### Scenario: Owner transfers ownership to a different group + +- **WHEN** a user with `owner` role transfers ownership from + `team-alpha` to `team-beta` via the permissions panel +- **THEN** the persisted Application has + `permissions.owners = ["team-beta"]` +- **AND** the OR audit trail records the permissions change with + before / after values and the actor identity +- **AND** the actor (who is no longer in `owners`, `editors`, or + `viewers` of the Application) loses access on the next page load + +#### Scenario: Empty owners array is rejected + +- **WHEN** a user with `owner` role attempts to PUT a `permissions` + block with `owners: []` +- **THEN** the system returns a `4xx` error citing the orphan-check +- **AND** the Application's `permissions` is unchanged + +### Requirement: REQ-OBRBAC-006 Global `openbuilt.use` navigation-entry permission + +The system SHALL extend `appinfo/info.xml` to declare an +`openbuilt.use` group-permission on the `` entry. The +permission SHALL be: + +- **Default** — no group restriction (the entry is visible to every + authenticated user, preserving spec #1's auth-only posture + documented in its OQ-2). +- **Admin-grantable** — through Nextcloud's standard + `/` mechanism, an administrator MAY + restrict the OpenBuilt top-bar entry to one or more Nextcloud + groups via the Nextcloud admin UI. +- **Independent** — the permission gates only the **navigation + entry**. It does not replace the per-Application `permissions` + enforced by REQ-OBRBAC-002 / REQ-OBRBAC-003 / REQ-OBRBAC-004; a + user with `openbuilt.use` who has no role on any Application sees + an empty list, not an error. + +A Nextcloud administrator MAY also bypass per-Application +`permissions` checks for incident response, but ONLY when explicitly +acting in admin mode (`IUserSession::isLoggedIn()` and the user is in +the `admin` group). The bypass SHALL record a +`rbac.admin_bypass` event in the OR audit trail every time it is +exercised so the action is reviewable. + +#### Scenario: Admin restricts the navigation entry to one group + +- **WHEN** an administrator restricts the OpenBuilt navigation entry + to the group `digital-team` via Nextcloud's admin UI +- **AND** a user outside `digital-team` logs in +- **THEN** the OpenBuilt top-bar entry is not visible to that user +- **AND** the user cannot reach the OpenBuilt shell via direct URL + (Nextcloud's existing navigation-permission middleware blocks it) + +#### Scenario: Admin bypass is audited + +- **WHEN** a Nextcloud administrator accesses an Application's + manifest endpoint without being in any of the Application's + `permissions` groups +- **THEN** the controller serves the manifest (200) +- **AND** the OR audit trail contains a `rbac.admin_bypass` event + naming the actor, the slug, and the timestamp + +### Requirement: REQ-OBRBAC-007 Permission changes are recorded in the OR audit trail + +The system SHALL record every change to an Application's `permissions` property in OpenRegister's standard per-object audit trail, regardless of whether the change is made through the OpenBuilt frontend permissions panel, the textarea editor, OR REST directly, or the transfer-ownership flow. The audit +entry SHALL be the OR-native object-change event (no app-local +audit duplication); it SHALL carry the before / after `permissions` +values, the actor's UID, and the timestamp, leveraging OR's existing +change-tracking per ADR-022. The OpenBuilt editor SHALL expose this +audit trail in a "Permission history" panel visible to `owner` role +holders only. + +#### Scenario: Permission change appears in the audit trail + +- **WHEN** an owner adds the group `qa-alpha` to + `permissions.editors` and saves +- **THEN** the OR audit trail for that Application contains an entry + showing `permissions.editors` changed from `[]` to `["qa-alpha"]` +- **AND** the entry names the acting user and the timestamp + +#### Scenario: Permission history is owner-only + +- **WHEN** a user with only `viewer` or `editor` role opens an + Application +- **THEN** the "Permission history" panel SHALL NOT be visible +- **AND** any direct API call the panel would make SHALL be gated by + the same owner-only check + diff --git a/openspec/specs/openbuilt-runtime/spec.md b/openspec/specs/openbuilt-runtime/spec.md new file mode 100644 index 00000000..2f613ab4 --- /dev/null +++ b/openspec/specs/openbuilt-runtime/spec.md @@ -0,0 +1,462 @@ +# openbuilt-runtime Specification + +## Purpose +TBD - created by archiving change bootstrap-openbuilt. Update Purpose after archive. +## Requirements +### Requirement: REQ-OBR-001 Manifest endpoint per virtual-app slug + +The system SHALL expose +`GET /index.php/apps/openbuilt/api/applications/{slug}/manifest` +backed by `ApplicationsController::getManifest`. The endpoint SHALL +resolve `{slug}` to an `Application` via the `BuiltAppRoute` index, +return the stored `manifest` JSON blob with `Content-Type: +application/json`, and respond `200` on success or `404` when no +matching published Application exists in the caller's organisation +scope. The endpoint SHALL be registered via `appinfo/routes.php` +(ADR-016) with `#[NoAdminRequired]` and a route-auth posture that +treats it as authenticated-user-readable. + +#### Scenario: Endpoint returns the stored manifest + +- **WHEN** an authenticated user requests + `/index.php/apps/openbuilt/api/applications/hello-world/manifest` +- **AND** a published `Application` with `slug: hello-world` exists + in their organisation +- **THEN** the response is `200 application/json` and the body is the + exact `manifest` blob persisted on the Application + +#### Scenario: Unknown slug returns 404 + +- **WHEN** an authenticated user requests the manifest for a slug + that has no matching `BuiltAppRoute` +- **THEN** the response is `404` with a JSON error body + +### Requirement: REQ-OBR-002 OpenBuilt shell mounts a nested CnAppRoot per virtual app + +The OpenBuilt frontend SHALL register a route `/builder/:slug/*` whose +view (`BuilderHost.vue`) mounts a **nested** `CnAppRoot` instance. +The nested mount SHALL be supplied with `appId = openbuilt-{slug}` +and a `bundledManifest` value, so that +`useAppManifest(appId, bundledManifest)` deep-merges the per-slug +endpoint response over the bundled placeholder and renders the virtual +app inside the OpenBuilt shell. The outer OpenBuilt shell's +`CnAppNav`, header, and chrome SHALL remain visible; the inner +`CnAppRoot` SHALL render only into the OpenBuilt page area. + +#### Scenario: Navigating into a virtual app renders its manifest pages + +- **WHEN** an authenticated user navigates to + `/index.php/apps/openbuilt/builder/hello-world` +- **THEN** the outer OpenBuilt shell stays mounted +- **AND** a nested `CnAppRoot` mounts inside the page area with + `appId = openbuilt-hello-world` +- **AND** the index page declared in the `hello-world` manifest + renders + +### Requirement: REQ-OBR-003 Path segments after the slug forward to the inner router + +For routes matching `/builder/:slug/*`, the system SHALL forward the +path segments after `/{slug}` to the **inner** manifest's vue-router +so that detail, form, and dashboard pages inside the virtual app +resolve correctly. The outer OpenBuilt router SHALL treat everything +after `/{slug}/` as opaque to the inner router; the inner router +MUST match its own routes against that suffix. + +#### Scenario: Detail route inside a virtual app resolves + +- **WHEN** an authenticated user navigates to + `/index.php/apps/openbuilt/builder/hello-world/messages/00000000-0000-0000-0000-000000000000` +- **THEN** the inner `CnAppRoot`'s router matches its `detail` page + for the `hello-message` schema +- **AND** the detail page renders for the requested object id + +### Requirement: REQ-OBR-004 Seeded hello-world Application exercises index, detail, form + +The repair step SHALL seed a single Application with `slug: +hello-world`, `status: published`, a `manifest` declaring at least +one `type: index`, one `type: detail`, and one `type: form` page over +a seeded `hello-message` schema in the OpenBuilt register, plus three +sample `hello-message` objects. The seed SHALL be idempotent (safe to +re-run) and SHALL only run when no `Application` with `slug: +hello-world` exists in the system organisation scope. + +#### Scenario: Fresh install renders the seeded virtual app + +- **WHEN** the OpenBuilt app is installed on a fresh Nextcloud +- **AND** an administrator navigates to + `/index.php/apps/openbuilt/builder/hello-world` +- **THEN** the seeded index page lists the three sample + `hello-message` objects +- **AND** opening one of them renders the seeded detail page +- **AND** the seeded form page is reachable from the index actions + +#### Scenario: Re-running the repair step is idempotent + +- **WHEN** the repair step runs a second time on an already-seeded + install +- **THEN** no duplicate `hello-world` Application is created +- **AND** no duplicate `hello-message` objects are created + +### Requirement: REQ-OBR-005 Textarea manifest editor saves to the Application object + +The OpenBuilt shell SHALL render a JSON `