diff --git a/.agents/skills/speckit-analyze/SKILL.md b/.agents/skills/speckit-analyze/SKILL.md index 83cd91b..54110a1 100644 --- a/.agents/skills/speckit-analyze/SKILL.md +++ b/.agents/skills/speckit-analyze/SKILL.md @@ -1,13 +1,10 @@ --- name: "speckit-analyze" description: "Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation." -argument-hint: "Optional focus areas for analysis" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: author: "github-spec-kit" source: "templates/commands/analyze.md" -user-invocable: true -disable-model-invocation: false --- diff --git a/.agents/skills/speckit-checklist/SKILL.md b/.agents/skills/speckit-checklist/SKILL.md index 927ef4a..35dadff 100644 --- a/.agents/skills/speckit-checklist/SKILL.md +++ b/.agents/skills/speckit-checklist/SKILL.md @@ -1,13 +1,10 @@ --- name: "speckit-checklist" description: "Generate a custom checklist for the current feature based on user requirements." -argument-hint: "Domain or focus area for the checklist" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: author: "github-spec-kit" source: "templates/commands/checklist.md" -user-invocable: true -disable-model-invocation: false --- diff --git a/.agents/skills/speckit-clarify/SKILL.md b/.agents/skills/speckit-clarify/SKILL.md index 1715752..36e82e0 100644 --- a/.agents/skills/speckit-clarify/SKILL.md +++ b/.agents/skills/speckit-clarify/SKILL.md @@ -1,13 +1,10 @@ --- name: "speckit-clarify" description: "Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec." -argument-hint: "Optional areas to clarify in the spec" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: author: "github-spec-kit" source: "templates/commands/clarify.md" -user-invocable: true -disable-model-invocation: false --- diff --git a/.agents/skills/speckit-constitution/SKILL.md b/.agents/skills/speckit-constitution/SKILL.md index 79a7b24..502ae8d 100644 --- a/.agents/skills/speckit-constitution/SKILL.md +++ b/.agents/skills/speckit-constitution/SKILL.md @@ -1,13 +1,10 @@ --- name: "speckit-constitution" description: "Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync." -argument-hint: "Principles or values for the project constitution" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: author: "github-spec-kit" source: "templates/commands/constitution.md" -user-invocable: true -disable-model-invocation: false --- @@ -87,7 +84,7 @@ Follow this execution flow: - Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles. - Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints. - Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline). - - Read each installed Spec Kit command file for your agent (including this one) — named `speckit.*` or `speckit-*` (dot or hyphen depending on the agent), or laid out as `speckit-/SKILL.md` for skills-based integrations, e.g. in `.github/agents/`, `.github/skills/`, `.Codex/skills/`, or your agent's equivalent commands directory — to verify no outdated references (Codex-only or other agent-specific names) remain when generic guidance is required. + - Read each installed Spec Kit command file for your agent (including this one) — named `speckit.*` or `speckit-*` (dot or hyphen depending on the agent), or laid out as `speckit-/SKILL.md` for skills-based integrations, e.g. in `.github/agents/`, `.github/skills/`, `.claude/skills/`, or your agent's equivalent commands directory — to verify no outdated references (CLAUDE-only or other agent-specific names) remain when generic guidance is required. - Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed. 5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update): diff --git a/.agents/skills/speckit-converge/SKILL.md b/.agents/skills/speckit-converge/SKILL.md index 61c8241..0c7a095 100644 --- a/.agents/skills/speckit-converge/SKILL.md +++ b/.agents/skills/speckit-converge/SKILL.md @@ -5,8 +5,6 @@ compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: author: "github-spec-kit" source: "templates/commands/converge.md" -user-invocable: true -disable-model-invocation: false --- diff --git a/.agents/skills/speckit-implement/SKILL.md b/.agents/skills/speckit-implement/SKILL.md index 747783f..8fc24a8 100644 --- a/.agents/skills/speckit-implement/SKILL.md +++ b/.agents/skills/speckit-implement/SKILL.md @@ -1,13 +1,10 @@ --- name: "speckit-implement" description: "Execute the implementation plan by processing and executing all tasks defined in tasks.md" -argument-hint: "Optional implementation guidance or task filter" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: author: "github-spec-kit" source: "templates/commands/implement.md" -user-invocable: true -disable-model-invocation: false --- diff --git a/.agents/skills/speckit-plan/SKILL.md b/.agents/skills/speckit-plan/SKILL.md index 7408c61..0e54d61 100644 --- a/.agents/skills/speckit-plan/SKILL.md +++ b/.agents/skills/speckit-plan/SKILL.md @@ -1,13 +1,10 @@ --- name: "speckit-plan" description: "Execute the implementation planning workflow using the plan template to generate design artifacts." -argument-hint: "Optional guidance for the planning phase" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: author: "github-spec-kit" source: "templates/commands/plan.md" -user-invocable: true -disable-model-invocation: false --- diff --git a/.agents/skills/speckit-specify/SKILL.md b/.agents/skills/speckit-specify/SKILL.md index 992a6c4..5d641ea 100644 --- a/.agents/skills/speckit-specify/SKILL.md +++ b/.agents/skills/speckit-specify/SKILL.md @@ -1,13 +1,10 @@ --- name: "speckit-specify" description: "Create or update the feature specification from a natural language feature description." -argument-hint: "Describe the feature you want to specify" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: author: "github-spec-kit" source: "templates/commands/specify.md" -user-invocable: true -disable-model-invocation: false --- diff --git a/.agents/skills/speckit-tasks/SKILL.md b/.agents/skills/speckit-tasks/SKILL.md index aa488b7..b254aec 100644 --- a/.agents/skills/speckit-tasks/SKILL.md +++ b/.agents/skills/speckit-tasks/SKILL.md @@ -1,13 +1,10 @@ --- name: "speckit-tasks" description: "Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts." -argument-hint: "Optional task generation constraints" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: author: "github-spec-kit" source: "templates/commands/tasks.md" -user-invocable: true -disable-model-invocation: false --- diff --git a/.agents/skills/speckit-taskstoissues/SKILL.md b/.agents/skills/speckit-taskstoissues/SKILL.md index ce9e1a2..dc81526 100644 --- a/.agents/skills/speckit-taskstoissues/SKILL.md +++ b/.agents/skills/speckit-taskstoissues/SKILL.md @@ -1,13 +1,10 @@ --- name: "speckit-taskstoissues" description: "Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts." -argument-hint: "Optional filter or label for GitHub issues" compatibility: "Requires spec-kit project structure with .specify/ directory" metadata: author: "github-spec-kit" source: "templates/commands/taskstoissues.md" -user-invocable: true -disable-model-invocation: false --- diff --git a/.gitignore b/.gitignore index c13206a..b1debe0 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ coverage/ .nyc_output/ # Logs +*.log npm-debug.log* yarn-debug.log* yarn-error.log* @@ -109,6 +110,7 @@ Thumbs.db # --------------------------------------------------------------------------- # Misc # --------------------------------------------------------------------------- +*.tmp *.tsbuildinfo .cache/ tmp/ diff --git a/.opencode/commands/speckit.converge.md b/.opencode/commands/speckit.converge.md index 8d882c6..fac7ec8 100644 --- a/.opencode/commands/speckit.converge.md +++ b/.opencode/commands/speckit.converge.md @@ -199,19 +199,13 @@ Append to the **end** of `tasks.md`, per the append contract: (highest existing phase + 1). 2. Write a single new section header `## Phase N: Convergence`. 3. Emit one checklist item per actionable finding, ordered CRITICAL/HIGH first, assigning - zero-padded IDs `T{M+1:03d}, T{M+2:03d}, …`, following the same checkbox/ID/marker - format as `/speckit.tasks` (`- [ ] [TaskID] [P?] [Story?] Description with file path`): + zero-padded IDs `T{M+1:03d}, T{M+2:03d}, …`: ```markdown - - [ ] T042 [P?] [Story?] per () + - [ ] T042 per () ``` - Include `[P]` only when the task is parallelizable (different file, no dependency on an - incomplete task). Include `[Story]` (e.g. `[US1]`) only when `` identifies a - specific user story (e.g. `US1/AC2`); omit it for plan-level or constitution-level - findings, matching `/speckit.tasks`'s rule that Setup/Foundational/Polish tasks carry no - story label. - + The imperative description MUST name the exact affected file path. `` traces the task to its origin: e.g. `FR-003`, `SC-002`, `US1/AC2`, `plan: storage decision`, `Constitution II`. diff --git a/.opencode/commands/speckit.specify.md b/.opencode/commands/speckit.specify.md index 98e0bea..e5b3663 100644 --- a/.opencode/commands/speckit.specify.md +++ b/.opencode/commands/speckit.specify.md @@ -139,9 +139,9 @@ Given that feature description, do this: 7. Identify Key Entities (if data involved) 8. Return: SUCCESS (spec ready for planning) -7. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. +6. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. -8. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria: +7. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria: a. **Create Spec Quality Checklist**: Generate a checklist file at `SPECIFY_FEATURE_DIRECTORY/checklists/requirements.md` using the checklist template structure with these validation items: diff --git a/.opencode/commands/speckit.tasks.md b/.opencode/commands/speckit.tasks.md index 087dc8c..01f50aa 100644 --- a/.opencode/commands/speckit.tasks.md +++ b/.opencode/commands/speckit.tasks.md @@ -165,7 +165,7 @@ Every task MUST strictly follow this format: **Examples**: -- ✅ CORRECT: `- [ ] T001 Create project structure (src/, tests/) per implementation plan` +- ✅ CORRECT: `- [ ] T001 Create project entry points in src/main.py and tests/conftest.py per implementation plan` - ✅ CORRECT: `- [ ] T005 [P] Implement authentication middleware in src/middleware/auth.py` - ✅ CORRECT: `- [ ] T012 [P] [US1] Create User model in src/models/user.py` - ✅ CORRECT: `- [ ] T014 [US1] Implement UserService in src/services/user_service.py` diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ec45bdb --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +build/ +coverage/ +.next/ +package-lock.json +yarn.lock +pnpm-lock.yaml diff --git a/.specify/feature.json b/.specify/feature.json index a503348..388001e 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/003-brand-crud" + "feature_directory": "specs/004-provider-keys" } diff --git a/.specify/init-options.json b/.specify/init-options.json index d5d98f7..642e6a5 100644 --- a/.specify/init-options.json +++ b/.specify/init-options.json @@ -1,8 +1,9 @@ { - "ai": "opencode", + "ai": "codex", + "ai_skills": true, "feature_numbering": "sequential", "here": true, - "integration": "opencode", + "integration": "codex", "script": "sh", "speckit_version": "0.12.14" } \ No newline at end of file diff --git a/.specify/integration.json b/.specify/integration.json index c5f2531..8580f99 100644 --- a/.specify/integration.json +++ b/.specify/integration.json @@ -2,14 +2,14 @@ "version": "0.12.14", "integration_state_schema": 1, "installed_integrations": [ - "opencode" + "codex" ], "integration_settings": { - "opencode": { + "codex": { "script": "sh", - "invoke_separator": "." + "invoke_separator": "-" } }, - "integration": "opencode", - "default_integration": "opencode" + "integration": "codex", + "default_integration": "codex" } diff --git a/.specify/integrations/codex.manifest.json b/.specify/integrations/codex.manifest.json new file mode 100644 index 0000000..845097c --- /dev/null +++ b/.specify/integrations/codex.manifest.json @@ -0,0 +1,17 @@ +{ + "integration": "codex", + "version": "0.12.14", + "installed_at": "2026-07-28T11:21:21.220147+00:00", + "files": { + ".agents/skills/speckit-analyze/SKILL.md": "2e89ffed73764eb9d3ee2d9fba7e245a5e73229b809ddec52409ca74c44e585d", + ".agents/skills/speckit-clarify/SKILL.md": "5095627c89a249d402ad2d0fad425ea0c0bd5e8559e9d6f5b682dea4f513ed54", + ".agents/skills/speckit-constitution/SKILL.md": "c57bf65daa9c23e24c97071bc0e4f930b3f458549b52792c4a65830de6b28f6b", + ".agents/skills/speckit-implement/SKILL.md": "248c2fa4be8a969ea771c88c38546d6735d7d4994bbe698bbabd18f53f88f0cd", + ".agents/skills/speckit-converge/SKILL.md": "1355b748919c7750cc87be94534192e5ff227229c14cbd34ca993a5f977a4ce2", + ".agents/skills/speckit-plan/SKILL.md": "c5522489fd177028e02ff4f1588a06bde811a0146cdc1d3495812694d6ea41b3", + ".agents/skills/speckit-checklist/SKILL.md": "4eb7e030d04014e6bbf5177f84aeef19b11adfa8470a14dd18506f65c7cf9dcb", + ".agents/skills/speckit-specify/SKILL.md": "6313b902b49bf2ae3582421be21c2453ff98a863974acca3e6a83cdad9d4b70b", + ".agents/skills/speckit-tasks/SKILL.md": "2dceed4345af65dbc6625157be8817dc35f2b0943b5896815ef1bc4d3421efba", + ".agents/skills/speckit-taskstoissues/SKILL.md": "2c00fdc774cb9b435ea8566c7efa8b208568d533d57529e61c35f18aa52d6c67" + } +} diff --git a/.specify/integrations/opencode.manifest.json b/.specify/integrations/opencode.manifest.json index afc644f..a76bfad 100644 --- a/.specify/integrations/opencode.manifest.json +++ b/.specify/integrations/opencode.manifest.json @@ -1,17 +1,17 @@ { "integration": "opencode", "version": "0.12.14", - "installed_at": "2026-07-19T16:37:19.006397+00:00", + "installed_at": "2026-07-26T16:19:25.218431+00:00", "files": { ".opencode/commands/speckit.analyze.md": "07e1e9f997bce9e06d3823ecf1ed315bb62381b81e7d6a7aef19c5087c320e70", ".opencode/commands/speckit.clarify.md": "595ae8988179a91780ce10e258afcccaa313b0924a79e720c66226e31af7ec00", ".opencode/commands/speckit.constitution.md": "e0d1d7c237d4657914dcccb6ea0c3a3a10d8d1d9a36b198282e22b3d0b9a55e5", ".opencode/commands/speckit.implement.md": "6a5713a4db8d48dd7c93e38b9957c6e54b6b7dc899ebab66f9ccb3bce58fdfeb", - ".opencode/commands/speckit.converge.md": "e585bc94a3f01cb45eb4f4f6003268a7bc7e1f1685d343c25ad5dc3c484b4816", + ".opencode/commands/speckit.converge.md": "fb8dcfb3e9c5df97793081825c9be11d8fe1b0d4bdc5478629c73608f3e5f60b", ".opencode/commands/speckit.plan.md": "ff1809d5d72b399ec364414a150c1a20e782677972d11f0bdf8c65c73f898a71", ".opencode/commands/speckit.checklist.md": "2124a817bddbff7f469fd47b08e43dd9a39b769499d367f6888a74e1e821412d", ".opencode/commands/speckit.specify.md": "e24c01f27d269dab73c4178ce5bb1c31198dbfc36b9198cc6e407ef1122f4745", - ".opencode/commands/speckit.tasks.md": "e3569f2fbdb99704b81e3647ce9e52fb812764d6b2590fc497a2c2130b43ba72", + ".opencode/commands/speckit.tasks.md": "201668645aa013b8e4dab7ef89325559071e6f57e99500112fdba837780ea81a", ".opencode/commands/speckit.taskstoissues.md": "0a436a42ceca2db309df88985d1939ee7f176c4a7a2d7c7a8d2253006cfe6870" } } diff --git a/.specify/integrations/speckit.manifest.json b/.specify/integrations/speckit.manifest.json index 9b649cd..10d3a14 100644 --- a/.specify/integrations/speckit.manifest.json +++ b/.specify/integrations/speckit.manifest.json @@ -5,13 +5,13 @@ "files": { ".specify/scripts/bash/common.sh": "6ff86bf39f6b4684b0f80927dc7a1dadec26b4671988a3fe4d6c2523cbd3aa22", ".specify/scripts/bash/setup-plan.sh": "4469b22960f43c07c33dca00de6dedb252145e9a9ce8fbb0e63be82e02b082ab", - ".specify/scripts/bash/setup-tasks.sh": "cf21ba2212b4dd5b435c5ea8527500cfd27768b86c0bbc7ebc3207759f118d27", - ".specify/scripts/bash/check-prerequisites.sh": "a7d8a14ecf87332b600cd966b5d0e7cb9d594abce7e4d1ee4372b2b5b3efff06", + ".specify/scripts/bash/setup-tasks.sh": "1d4bcebe93f3e4e778964978cfe9bfb67ee94e7dc688f2ff3be224f647f61f1f", + ".specify/scripts/bash/check-prerequisites.sh": "ac3e96258a05d029d048076393a03aadff5c7c3a55a26d0a9f5c17886a1c659d", ".specify/scripts/bash/create-new-feature.sh": "dd531f9ba47c9ce9975b597947377be9542b7236681d6dc033513c4e3cfc50f2", ".specify/templates/constitution-template.md": "ce7549540fa45543cca797a150201d868e64495fdff39dc38246fb17bd4024b3", - ".specify/templates/checklist-template.md": "709d8ab8384a3a49f5e0f64479f71553ef6d6f8bb4f00281b05f47837993b536", - ".specify/templates/tasks-template.md": "fc29a233f6f5a27ca31f1aa46b596af6500c627441c6e62b2bc4a1d721525842", - ".specify/templates/spec-template.md": "3945437fc35cd30a5b2bf7beea680337c3516826d3efa5a6b92c4a7eca1ba28e", - ".specify/templates/plan-template.md": "7e637502d41eccf0ca672496636365691fdca62ef37b27ec07fcb412dbfa90d4" + ".specify/templates/checklist-template.md": "0ad704b60af2df817aee1c0a2ecc0e0304b271d2de34047df1c891735967033e", + ".specify/templates/tasks-template.md": "61f9bf0d34c75f97e91486037ff35cdde35915d8d9f9b2fa755882c35eb497f9", + ".specify/templates/spec-template.md": "8bc198906589c75c3490d1e1edd09194bc94348b3ae8ee46657a62a7f9ebd4d8", + ".specify/templates/plan-template.md": "bbe9f9504a26f7400b2d92015b858a30dbc92e5069cf02840cec19c45b6abc95" } } diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 4f8f5ec..3c3953e 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,29 +1,30 @@ @@ -61,7 +62,7 @@ The technology stack is fixed for MVP: | Layer | Technology | |-------|------------| -| Frontend | Next.js 14 monolith | +| Frontend | Next.js 15 monolith | | Backend | FastAPI | | Auth, DB, Vault, Storage | Supabase | | Hosting | Bunny Magic Containers | @@ -98,14 +99,39 @@ Security implementation MUST follow these requirements: ### VII. Definition of Done -A feature is complete only when ALL of the following are verified: - -- [ ] Works correctly for a brand with no brand kit (0 answers) -- [ ] Works correctly for a brand with a completed brand kit -- [ ] Works with OpenAI provider -- [ ] Works with Gemini provider -- [ ] RLS policies tested OR explicit integration checks documented -- [ ] Hard delete verified: database rows removed AND storage assets removed +A feature is complete only when all universal checks and every capability check +applicable to the current implementation phase are verified. + +Universal checks: + +- [ ] Acceptance scenarios for the feature pass at the API, data, and user-facing + layers it changes +- [ ] Every table introduced or changed has RLS, forced RLS, policies, and required + privileges verified by direct integration tests; backend-only tables also require + tests proving client roles are denied +- [ ] Server-side ownership and secret/logging rules are tested for every affected + read and write operation +- [ ] Every deletable database row, secret, or stored asset affected by the feature + is physically removed on successful deletion; soft delete is forbidden + +Capability checks become mandatory only when their prerequisite capability exists: + +- [ ] Brand-kit zero-answer and completed-kit scenarios are required beginning with + the feature that implements Brand Kit, and for later brand-scoped features whose + behavior reads or depends on Brand Kit. Before Brand Kit exists, these checks are + not applicable. +- [ ] OpenAI behavior is required beginning with the feature that first integrates + OpenAI, and for every later feature that calls or changes that integration. +- [ ] Gemini behavior is required beginning with the feature that first integrates + Gemini, and for every later feature that calls or changes that integration. +- [ ] Generation lifecycle, platform preset, and PNG-output checks are required + beginning with the feature that implements image generation, and for later + features that change generation behavior. + +A plan MAY mark a capability check not applicable only when its prerequisite has not +been implemented or the feature cannot affect that capability. The plan MUST record +that rationale. Once a prerequisite capability exists, applicable checks MUST NOT be +deferred merely because they belong to an earlier or later roadmap phase. ## Governance @@ -128,4 +154,4 @@ A feature is complete only when ALL of the following are verified: - Plan documents MUST include a Constitution Check section - Definition of Done checklist MUST be completed before feature merge -**Version**: 1.1.0 | **Ratified**: 2025-01-28 | **Last Amended**: 2026-07-19 \ No newline at end of file +**Version**: 2.0.0 | **Ratified**: 2025-01-28 | **Last Amended**: 2026-07-26 diff --git a/.specify/scripts/bash/check-prerequisites.sh b/.specify/scripts/bash/check-prerequisites.sh index bf75140..55575db 100755 --- a/.specify/scripts/bash/check-prerequisites.sh +++ b/.specify/scripts/bash/check-prerequisites.sh @@ -120,20 +120,20 @@ fi # Validate required directories and files if [[ ! -d "$FEATURE_DIR" ]]; then echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2 - echo "Run /speckit-specify first to create the feature structure." >&2 + echo "Run /speckit.specify first to create the feature structure." >&2 exit 1 fi if [[ ! -f "$IMPL_PLAN" ]]; then echo "ERROR: plan.md not found in $FEATURE_DIR" >&2 - echo "Run /speckit-plan first to create the implementation plan." >&2 + echo "Run /speckit.plan first to create the implementation plan." >&2 exit 1 fi # Check for tasks.md if required if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2 - echo "Run /speckit-tasks first to create the task list." >&2 + echo "Run /speckit.tasks first to create the task list." >&2 exit 1 fi diff --git a/.specify/scripts/bash/setup-tasks.sh b/.specify/scripts/bash/setup-tasks.sh index ae0d7bd..15cc618 100755 --- a/.specify/scripts/bash/setup-tasks.sh +++ b/.specify/scripts/bash/setup-tasks.sh @@ -30,13 +30,13 @@ unset _paths_output # Validate required files if [[ ! -f "$IMPL_PLAN" ]]; then echo "ERROR: plan.md not found in $FEATURE_DIR" >&2 - echo "Run /speckit-plan first to create the implementation plan." >&2 + echo "Run /speckit.plan first to create the implementation plan." >&2 exit 1 fi if [[ ! -f "$FEATURE_SPEC" ]]; then echo "ERROR: spec.md not found in $FEATURE_DIR" >&2 - echo "Run /speckit-specify first to create the feature structure." >&2 + echo "Run /speckit.specify first to create the feature structure." >&2 exit 1 fi diff --git a/.specify/templates/checklist-template.md b/.specify/templates/checklist-template.md index e64065d..0caeacf 100644 --- a/.specify/templates/checklist-template.md +++ b/.specify/templates/checklist-template.md @@ -4,13 +4,13 @@ **Created**: [DATE] **Feature**: [Link to spec.md or relevant documentation] -**Note**: This checklist is generated by the `/speckit-checklist` command based on feature context and requirements. +**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements. US2 Validate ----+ + | | + +------> US3 Activate ----+----> US4 Delete/Cleanup + | + v + Polish and full DoD +``` + +### Within Each User Story + +- Write the listed tests first and confirm they fail for the missing behavior +- Add boundary models before stores, stores before routes, and routes before frontend integration +- Never hold a database connection or row lock across provider or Storage I/O +- Always acquire the brand lock before a provider-key lock +- Preserve opaque ownership resolution before lifecycle/conflict errors +- Stop at each checkpoint and run that story's independent tests before continuing + +### Parallel Opportunities + +- T001, T002, and T005 can be authored in parallel before T003 because they touch separate test concerns +- T006 and T008 can proceed in parallel after the migration contract is understood +- T014-T016 can be authored in parallel; T017 can proceed separately before T018 consumes its models +- T024-T026 can be authored in parallel; T027 can proceed independently of the database lease implementation in T028 +- T035 and T036 can be authored in parallel after US1 +- T041-T044 can be authored in parallel before deletion/storage implementation starts +- T051 and T053 can proceed in parallel because they modify different frontend pages +- T057, T058, T060, and T061 can run in parallel after implementation stabilizes + +--- + +## Parallel Examples + +### User Story 1 + +```bash +Task: "Write list/add contract tests in backend/tests/contract/test_provider_keys.py" +Task: "Write Vault/idempotency integration tests in backend/tests/integration/test_provider_keys.py" +Task: "Write owner/RLS/Vault denial tests in backend/tests/integration/test_provider_key_rls.py" +``` + +### User Story 2 + +```bash +Task: "Write provider classification tests in backend/tests/unit/test_provider_validation.py" +Task: "Write validation API contract tests in backend/tests/contract/test_provider_keys.py" +Task: "Write validation lease tests in backend/tests/integration/test_provider_keys.py" +``` + +### User Story 3 + +```bash +Task: "Write activation API tests in backend/tests/contract/test_provider_keys.py" +Task: "Write activation concurrency tests in backend/tests/integration/test_provider_key_cleanup.py" +``` + +### User Story 4 + +```bash +Task: "Write individual cleanup contract tests in backend/tests/contract/test_provider_keys.py" +Task: "Write brand cleanup contract tests in backend/tests/contract/test_brands.py" +Task: "Write cleanup concurrency tests in backend/tests/integration/test_provider_key_cleanup.py" +``` + +--- + +## Implementation Strategy + +### MVP First + +1. Complete T001-T013 to establish the security and lifecycle foundation. +2. Complete T014-T023 for User Story 1. +3. Stop and independently validate atomic Vault storage, idempotent add, safe listing, + RLS, Vault denial, and browser opacity. +4. Deploy only if a brand owner can configure both providers without any reveal path. + +### Incremental Delivery + +1. Foundational: schema, privilege boundary, brand fence, deadline, and safe logs. +2. US1: add/list keys as the minimum useful BYOK setup. +3. US2: validate both providers without generation or false invalidation. +4. US3: rotate active credentials atomically under concurrency. +5. US4: complete individual and brand-wide retryable physical cleanup. +6. Polish: real-provider verification, full regression, secrecy audit, and DoD. + +## Notes + +- `[P]` means separate files or non-overlapping work with no dependency on an incomplete task. +- Raw keys may exist only in the add request, request-local backend memory/SQL bind, + Supabase Vault, and provider authentication header; never add them to fixtures that + can print on assertion failure. +- A cleanup-required row is a failed-operation retry anchor, not a successful soft delete. +- Missing Vault secrets are idempotent cleanup success; unknown Storage writes remain + blocked until definitive reconciliation and never expire based only on time. +- No Brand Kit, image generation, provider SDK, key reveal/edit, background cleanup + worker, pagination for key lists, or new runtime dependency belongs in this feature. diff --git a/specs/004-provider-keys/verification.md b/specs/004-provider-keys/verification.md new file mode 100644 index 0000000..d39e574 --- /dev/null +++ b/specs/004-provider-keys/verification.md @@ -0,0 +1,41 @@ +# Provider Keys Verification + +**Date**: 2026-07-28 + +## Automated Results + +- Backend suite: 226 passed with no skipped tests. +- Provider-key RLS, catalog, Data API, backend-table, and Vault checks: 9 passed. +- Supabase schema lint: no errors at warning level. +- Frontend ESLint, TypeScript, and production build: passed. +- Provider-key Playwright flow: 2 passed. +- Production `linux/amd64` container image build: passed. +- Focused secrecy suites after diagnostic-boundary hardening: 172 passed. + +The automated suites cover safe add/list behavior, deterministic OpenAI and Gemini +classification, ownership and RLS, Vault isolation, activation and validation races, +idempotent and ambiguous database outcomes, individual cleanup retry, token-owned +Storage operations, multi-page cleanup, brand hard deletion, and auth-user deletion +restrictions. + +## Environment Blocker + +Quickstart Scenario 2 steps 8-10 require user-supplied disposable OpenAI and Gemini +credentials entered through the UI. No disposable provider credentials were available +in this verification environment. Those checks were not simulated or treated as +passing: real official model-list acceptance and the associated live-log review remain +blocked. + +Because real-provider verification is applicable to this feature, Quickstart Scenario 2, +the complete six-scenario run, and the final constitutional Definition of Done remain +open until both providers return `valid` through the UI and the live logs pass review. + +## Applicability + +- Brand Kit zero-answer and completed-kit checks are not applicable because Brand Kit + does not exist and provider-key behavior does not read or depend on it. +- Generation lifecycle, platform preset, and PNG-output checks are not applicable + because image generation does not exist and provider-key operations perform no + generation. +- OpenAI and Gemini behavior is applicable because this feature introduces both + integrations; deterministic checks pass, but the real-provider gate above remains. diff --git a/supabase/migrations/00016_create_provider_keys.sql b/supabase/migrations/00016_create_provider_keys.sql new file mode 100644 index 0000000..a37734d --- /dev/null +++ b/supabase/migrations/00016_create_provider_keys.sql @@ -0,0 +1,237 @@ +CREATE EXTENSION IF NOT EXISTS supabase_vault CASCADE; + +CREATE TYPE provider_t AS ENUM ('openai', 'gemini'); +CREATE TYPE provider_key_lifecycle_t AS ENUM ('normal', 'cleanup_required'); +CREATE TYPE brand_deletion_state_t AS ENUM ('active', 'cleanup_required'); + +CREATE SCHEMA IF NOT EXISTS private; +REVOKE ALL ON SCHEMA private FROM PUBLIC, anon, authenticated; +GRANT USAGE ON SCHEMA private TO authenticated, service_role; + +CREATE FUNCTION private.is_brand_owner(p_brand_id UUID) +RETURNS BOOLEAN +LANGUAGE sql +STABLE +SECURITY DEFINER +SET search_path = '' +AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public.brands + WHERE id = p_brand_id + AND owner_user_id = (SELECT auth.uid()) + ); +$$; + +REVOKE ALL ON FUNCTION private.is_brand_owner(UUID) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION private.is_brand_owner(UUID) TO authenticated, service_role; + +ALTER TABLE brands + ADD COLUMN deletion_state brand_deletion_state_t NOT NULL DEFAULT 'active'; + +ALTER TABLE brands DROP CONSTRAINT brands_logo_path_check; +ALTER TABLE brands ADD CONSTRAINT brands_logo_path_check CHECK ( + logo_path IS NULL + OR logo_path ~ '^brands/[0-9a-f-]+/logo\.[A-Za-z0-9]+$' + OR logo_path ~ '^brands/[0-9a-f-]+/logos/[0-9a-f-]+\.[A-Za-z0-9]+$' +); + +ALTER TABLE brands DROP CONSTRAINT brands_owner_user_id_fkey; +ALTER TABLE brands ADD CONSTRAINT brands_owner_user_id_fkey + FOREIGN KEY (owner_user_id) REFERENCES auth.users(id) ON DELETE RESTRICT; + +CREATE FUNCTION reject_brand_cleanup_reversal() +RETURNS TRIGGER +LANGUAGE plpgsql +SET search_path = '' +AS $$ +BEGIN + IF OLD.deletion_state = 'cleanup_required' + AND NEW.deletion_state = 'active' THEN + RAISE EXCEPTION 'brand cleanup state is irreversible' + USING ERRCODE = '23514'; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER trg_brands_cleanup_irreversible + BEFORE UPDATE ON brands + FOR EACH ROW + EXECUTE FUNCTION reject_brand_cleanup_reversal(); + +CREATE TABLE provider_keys ( + id UUID PRIMARY KEY, + brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE RESTRICT, + provider provider_t NOT NULL, + vault_secret_id UUID NOT NULL, + label TEXT CHECK (label IS NULL OR char_length(label) <= 100), + key_hint TEXT NOT NULL CHECK (key_hint ~ '^\*\*\*[A-Za-z0-9_-]{4}$'), + lifecycle provider_key_lifecycle_t NOT NULL DEFAULT 'normal', + is_active BOOLEAN NOT NULL DEFAULT false, + is_valid BOOLEAN, + last_validated_at TIMESTAMPTZ, + last_validation_error TEXT, + validation_token UUID, + validation_lease_expires_at TIMESTAMPTZ, + last_used_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT provider_keys_cleanup_inactive CHECK ( + lifecycle <> 'cleanup_required' OR NOT is_active + ), + CONSTRAINT provider_keys_invalid_inactive CHECK ( + is_valid IS DISTINCT FROM false OR NOT is_active + ), + CONSTRAINT provider_keys_safe_validation_error CHECK ( + last_validation_error IS NULL + OR last_validation_error = 'INVALID_CREDENTIAL' + ), + CONSTRAINT provider_keys_validation_lease_pair CHECK ( + (validation_token IS NULL AND validation_lease_expires_at IS NULL) + OR (validation_token IS NOT NULL AND validation_lease_expires_at IS NOT NULL) + ), + CONSTRAINT provider_keys_cleanup_without_validation CHECK ( + lifecycle = 'normal' OR validation_token IS NULL + ), + CONSTRAINT provider_keys_validation_result CHECK ( + (is_valid IS NULL AND last_validated_at IS NULL AND last_validation_error IS NULL) + OR (is_valid IS TRUE AND last_validated_at IS NOT NULL AND last_validation_error IS NULL) + OR ( + is_valid IS FALSE + AND last_validated_at IS NOT NULL + AND last_validation_error = 'INVALID_CREDENTIAL' + ) + ) +); + +CREATE UNIQUE INDEX uq_provider_keys_vault_secret + ON provider_keys(vault_secret_id); +CREATE UNIQUE INDEX uq_provider_keys_one_active + ON provider_keys(brand_id, provider) + WHERE is_active; +CREATE INDEX idx_provider_keys_brand_provider_created + ON provider_keys(brand_id, provider, created_at DESC, id DESC); +CREATE INDEX idx_provider_keys_cleanup + ON provider_keys(brand_id, lifecycle) + WHERE lifecycle = 'cleanup_required'; + +CREATE TRIGGER trg_provider_keys_updated_at + BEFORE UPDATE ON provider_keys + FOR EACH ROW + EXECUTE FUNCTION set_updated_at(); + +CREATE FUNCTION reject_provider_key_cleanup_reversal() +RETURNS TRIGGER +LANGUAGE plpgsql +SET search_path = '' +AS $$ +BEGIN + IF OLD.lifecycle = 'cleanup_required' AND NEW.lifecycle = 'normal' THEN + RAISE EXCEPTION 'provider key cleanup state is irreversible' + USING ERRCODE = '23514'; + END IF; + RETURN NEW; +END; +$$; + +CREATE TRIGGER trg_provider_keys_cleanup_irreversible + BEFORE UPDATE ON provider_keys + FOR EACH ROW + EXECUTE FUNCTION reject_provider_key_cleanup_reversal(); + +CREATE TABLE provider_key_idempotency ( + id UUID PRIMARY KEY, + brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE CASCADE, + request_id UUID NOT NULL, + provider_key_id UUID REFERENCES provider_keys(id) ON DELETE SET NULL, + state TEXT NOT NULL CHECK (state IN ('active', 'deleted')), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT uq_provider_key_idempotency_brand_request UNIQUE (brand_id, request_id) +); + +CREATE TABLE brand_asset_operations ( + id UUID PRIMARY KEY, + brand_id UUID NOT NULL REFERENCES brands(id) ON DELETE RESTRICT, + operation TEXT NOT NULL CHECK (operation IN ('upload', 'remove')), + object_path TEXT, + previous_path TEXT, + state TEXT NOT NULL CHECK (state IN ('in_progress', 'cleanup_required')), + remote_status TEXT NOT NULL CHECK ( + remote_status IN ('pending', 'succeeded', 'failed', 'unknown') + ), + started_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT uq_brand_asset_operations_brand UNIQUE (brand_id) +); + +CREATE TRIGGER trg_brand_asset_operations_updated_at + BEFORE UPDATE ON brand_asset_operations + FOR EACH ROW + EXECUTE FUNCTION set_updated_at(); + +ALTER TABLE provider_keys ENABLE ROW LEVEL SECURITY; +ALTER TABLE provider_keys FORCE ROW LEVEL SECURITY; +ALTER TABLE provider_key_idempotency ENABLE ROW LEVEL SECURITY; +ALTER TABLE provider_key_idempotency FORCE ROW LEVEL SECURITY; +ALTER TABLE brand_asset_operations ENABLE ROW LEVEL SECURITY; +ALTER TABLE brand_asset_operations FORCE ROW LEVEL SECURITY; + +CREATE POLICY provider_keys_select ON provider_keys + FOR SELECT USING (private.is_brand_owner(brand_id)); +CREATE POLICY provider_keys_insert ON provider_keys + FOR INSERT WITH CHECK (private.is_brand_owner(brand_id)); +CREATE POLICY provider_keys_update ON provider_keys + FOR UPDATE + USING (private.is_brand_owner(brand_id)) + WITH CHECK (private.is_brand_owner(brand_id)); +CREATE POLICY provider_keys_delete ON provider_keys + FOR DELETE USING (private.is_brand_owner(brand_id)); + +CREATE POLICY provider_key_idempotency_owner ON provider_key_idempotency + FOR ALL + USING (private.is_brand_owner(brand_id)) + WITH CHECK (private.is_brand_owner(brand_id)); +CREATE POLICY brand_asset_operations_owner ON brand_asset_operations + FOR ALL + USING (private.is_brand_owner(brand_id)) + WITH CHECK (private.is_brand_owner(brand_id)); + +REVOKE ALL ON provider_keys FROM PUBLIC, anon, authenticated; +REVOKE ALL ON provider_key_idempotency FROM PUBLIC, anon, authenticated; +REVOKE ALL ON brand_asset_operations FROM PUBLIC, anon, authenticated; +REVOKE ALL ON brands FROM authenticated; + +GRANT SELECT, INSERT, UPDATE, DELETE ON provider_keys TO service_role; +GRANT SELECT, INSERT, UPDATE, DELETE ON provider_key_idempotency TO service_role; +GRANT SELECT, INSERT, UPDATE, DELETE ON brand_asset_operations TO service_role; +GRANT SELECT, INSERT, UPDATE, DELETE ON brands TO service_role; + +GRANT SELECT ( + id, provider, label, key_hint, lifecycle, is_active, is_valid, + last_validated_at, last_validation_error, created_at +) ON provider_keys TO authenticated; +GRANT SELECT ( + id, name, logo_path, deletion_state, created_at, updated_at +) ON brands TO authenticated; + +REVOKE ALL ON SCHEMA vault FROM PUBLIC, anon, authenticated; +REVOKE ALL ON ALL TABLES IN SCHEMA vault FROM PUBLIC, anon, authenticated; +REVOKE ALL ON FUNCTION vault.create_secret(TEXT, TEXT, TEXT, UUID) + FROM PUBLIC, anon, authenticated; +REVOKE ALL ON FUNCTION vault.update_secret(UUID, TEXT, TEXT, TEXT, UUID) + FROM PUBLIC, anon, authenticated; + +REVOKE ALL ON SCHEMA vault FROM service_role; +REVOKE ALL ON ALL TABLES IN SCHEMA vault FROM service_role; +REVOKE ALL ON FUNCTION vault.create_secret(TEXT, TEXT, TEXT, UUID) + FROM service_role; +REVOKE ALL ON FUNCTION vault.update_secret(UUID, TEXT, TEXT, TEXT, UUID) + FROM service_role; + +GRANT USAGE ON SCHEMA vault TO service_role; +GRANT EXECUTE ON FUNCTION vault.create_secret(TEXT, TEXT, TEXT, UUID) + TO service_role; +GRANT SELECT (id, decrypted_secret) ON vault.decrypted_secrets TO service_role; +GRANT SELECT (id) ON vault.secrets TO service_role; +GRANT DELETE ON vault.secrets TO service_role;