@ysk-kit/cli 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add-capability.js +2 -2
- package/dist/add-capability.js.map +1 -1
- package/dist/add-module.js +7 -7
- package/dist/add-module.js.map +1 -1
- package/dist/apply-capability.d.ts +1 -1
- package/dist/apply-capability.d.ts.map +1 -1
- package/dist/apply-capability.js +3 -3
- package/dist/apply-capability.js.map +1 -1
- package/dist/capability-catalog.d.ts +1 -1
- package/dist/capability-catalog.d.ts.map +1 -1
- package/dist/capability-catalog.js +11 -3
- package/dist/capability-catalog.js.map +1 -1
- package/dist/capability-patches.d.ts.map +1 -1
- package/dist/capability-patches.js +29 -46
- package/dist/capability-patches.js.map +1 -1
- package/dist/check-agent-guidance.d.ts.map +1 -1
- package/dist/check-agent-guidance.js +2 -1
- package/dist/check-agent-guidance.js.map +1 -1
- package/dist/check-agent.d.ts.map +1 -1
- package/dist/check-agent.js +2 -1
- package/dist/check-agent.js.map +1 -1
- package/dist/doctor.js +3 -3
- package/dist/doctor.js.map +1 -1
- package/dist/help.js +1 -1
- package/dist/help.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +111 -86
- package/dist/index.js.map +1 -1
- package/dist/upgrade.js +1 -1
- package/dist/upgrade.js.map +1 -1
- package/package.json +4 -4
- package/templates/agent/nested/api.md +1 -0
- package/templates/agent/nested/contracts.md +1 -0
- package/templates/agent/rules/api-modules.mdc +1 -1
- package/templates/agent/rules/contracts.mdc +1 -1
- package/templates/agent/rules/desktop.mdc +9 -0
- package/templates/agent/rules/security.mdc +9 -0
- package/templates/agent/rules/tests.mdc +1 -1
- package/templates/agent/skills/add-capability/SKILL.md +9 -0
- package/templates/agent/skills/add-module/SKILL.md +9 -0
- package/templates/agent/skills/contract-change/SKILL.md +19 -0
- package/templates/agent/skills/db-migration/SKILL.md +17 -0
- package/templates/agent/skills/debug-issue/SKILL.md +18 -0
- package/templates/agent/skills/desktop-electron/SKILL.md +20 -0
- package/templates/agent/skills/envelope-api/SKILL.md +9 -0
- package/templates/agent/skills/fix-layers/SKILL.md +9 -0
- package/templates/agent/skills/llm-feature/SKILL.md +19 -0
- package/templates/agent/skills/new-product/SKILL.md +9 -0
- package/templates/agent/skills/plan-feature/SKILL.md +9 -0
- package/templates/agent/skills/review-change/SKILL.md +20 -0
- package/templates/agent/skills/security-review/SKILL.md +20 -0
- package/templates/agent/skills/test-plan/SKILL.md +9 -0
- package/templates/agent/skills/ui-design/SKILL.md +9 -0
- package/templates/agent/skills/ui-review/SKILL.md +9 -0
- package/templates/agent/skills/verify-change/SKILL.md +9 -0
- package/templates/agent/skills/webhook-handling/SKILL.md +20 -0
- package/templates/agent/skills/write-tests/SKILL.md +9 -0
- package/templates/agent/ysk-kit.mdc +1 -1
- package/templates/capabilities/billing/apps/api/src/modules/billing/application/billing-service.test.ts +56 -0
- package/templates/capabilities/billing/apps/api/src/modules/billing/application/billing-service.ts +71 -18
- package/templates/capabilities/billing/apps/api/src/modules/billing/domain/stripe-webhook-event.ts +12 -0
- package/templates/capabilities/billing/apps/api/src/modules/billing/domain/webhook-event-repository.ts +19 -0
- package/templates/capabilities/billing/apps/api/src/modules/billing/infra/billing.app.test.ts +172 -0
- package/templates/capabilities/billing/apps/api/src/modules/billing/infra/memory-webhook-event-repository.ts +31 -0
- package/templates/capabilities/billing/apps/api/src/modules/billing/infra/prisma-webhook-event-repository.ts +39 -0
- package/templates/capabilities/billing/apps/api/src/modules/billing/infra/stripe-billing.test.ts +3 -0
- package/templates/capabilities/billing/apps/api/src/modules/billing/infra/stripe-billing.ts +34 -7
- package/templates/capabilities/billing/packages/contracts/src/api/billing.ts +2 -2
- package/templates/capabilities/billing/packages/contracts/src/dto/billing.ts +1 -1
- package/templates/capabilities/billing/packages/sdk/src/resources/billing.ts +1 -1
- package/templates/capabilities/llm/apps/api/src/modules/llm/application/llm-service.test.ts +58 -0
- package/templates/capabilities/llm/apps/api/src/modules/llm/application/llm-service.ts +63 -36
- package/templates/capabilities/llm/apps/api/src/modules/llm/domain/usage-repository.ts +1 -0
- package/templates/capabilities/llm/apps/api/src/modules/llm/infra/llm.app.test.ts +49 -0
- package/templates/capabilities/llm/apps/api/src/modules/llm/infra/memory-usage-repository.ts +6 -3
- package/templates/capabilities/llm/apps/api/src/modules/llm/infra/prisma-usage-repository.ts +5 -0
- package/templates/capabilities/llm/packages/contracts/src/api/llm.ts +2 -2
- package/templates/capabilities/llm/packages/contracts/src/dto/llm.ts +8 -2
- package/templates/capabilities/llm/packages/sdk/src/resources/llm.ts +2 -2
- package/templates/capabilities/push/packages/contracts/src/api/devices.ts +2 -2
- package/templates/capabilities/push/packages/sdk/src/resources/devices.ts +1 -1
- package/templates/capabilities/team/packages/contracts/src/api/organizations.ts +2 -2
- package/templates/capabilities/team/packages/contracts/src/dto/organization.ts +1 -1
- package/templates/capabilities/team/packages/sdk/src/resources/organizations.ts +1 -1
- package/templates/module/api.ts.tmpl +3 -3
- package/templates/module/dto.ts.tmpl +1 -1
- package/templates/module/sdk-resource.ts.tmpl +1 -1
|
@@ -3,6 +3,15 @@ name: add-module
|
|
|
3
3
|
description: >
|
|
4
4
|
Add a hexagonal HTTP module with ysk-kit add module (contract, DTO, Express+Fastify, SDK, web page).
|
|
5
5
|
Use when the user wants a new resource, new API route, ysk-kit add module, or /add-module.
|
|
6
|
+
中文:加模組、HTTP 切片、org 範圍、Express 與 Fastify 測試。
|
|
7
|
+
Do not use to mkdir apps/api/src/modules by hand or to restore llm/team/billing/push (add-capability).
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
Read `docs/skills/add-module.md`. Law: `AGENTS.md`.
|
|
11
|
+
|
|
12
|
+
1. `pnpm ysk-kit add module <kebab> --prisma --web`. Do not mkdir the slice.
|
|
13
|
+
2. Fill DTO then `application/` rules. Schema → db-migration. Org-scoped: requireBiller + negative test.
|
|
14
|
+
3. App tests on Express AND Fastify. Then verify-change.
|
|
15
|
+
Gotcha: no parallel tree; no TypeScript enum; clients use the SDK only.
|
|
16
|
+
Verify: `pnpm layers && pnpm typecheck && pnpm test && pnpm gen:openapi && pnpm ysk-kit check agent`
|
|
17
|
+
Full steps: `docs/skills/add-module.md`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: contract-change
|
|
3
|
+
description: >
|
|
4
|
+
Change YSK Kit contracts first: breaking-change table, gen:openapi diff
|
|
5
|
+
(optional oasdiff), deprecation, new error codes, lockstep changeset bump.
|
|
6
|
+
Use when editing packages/contracts, ts-rest paths, DTOs, error codes,
|
|
7
|
+
OpenAPI, or a client-visible field.
|
|
8
|
+
中文:合約、OpenAPI、破壞性變更、錯誤碼、changeset。
|
|
9
|
+
Do not use for application-only rules with no DTO/path change, or for Prisma SQL (use db-migration).
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Read `docs/skills/contract-change.md`. Law: `AGENTS.md`.
|
|
13
|
+
|
|
14
|
+
1. Classify additive vs breaking. Ask before dropping a published field.
|
|
15
|
+
2. Edit `@ysk-kit/contracts` first. `pnpm gen:openapi` then `git diff --exit-code docs/openapi.yaml`.
|
|
16
|
+
3. Lockstep changeset (all 26) + CHANGELOG. Optional `npx oasdiff`.
|
|
17
|
+
Gotcha: new error codes need ERROR_MESSAGE zh-HK+en and HTTP_STATUS.
|
|
18
|
+
Verify: `pnpm --filter @ysk-kit/contracts test && pnpm gen:openapi`
|
|
19
|
+
Full steps: `docs/skills/contract-change.md`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: db-migration
|
|
3
|
+
description: >
|
|
4
|
+
Apply Prisma 7.10 migrations in YSK Kit: review generated SQL, expand/contract
|
|
5
|
+
for breaking changes, backfills, never migrate reset without explicit consent.
|
|
6
|
+
Use when schema.prisma changes, after add-module --prisma or add <capability>
|
|
7
|
+
with Prisma, or the user says migrate / rename column / backfill / reset.
|
|
8
|
+
中文:資料庫遷移、改欄位、改名、回填、migrate reset。
|
|
9
|
+
Do not use for Prisma 8 commands or for application rules with no schema change.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Read `docs/skills/db-migration.md`. Law: `AGENTS.md`.
|
|
13
|
+
|
|
14
|
+
1. Pin Prisma 7.10: `migrate dev --create-only`, `migrate deploy`. No Prisma 8 CLI.
|
|
15
|
+
2. Read generated SQL. Breaking changes use expand/contract + backfill.
|
|
16
|
+
3. Update Prisma **and** memory repositories. Never `migrate reset` without this-turn consent.
|
|
17
|
+
Full steps: `docs/skills/db-migration.md`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: debug-issue
|
|
3
|
+
description: >
|
|
4
|
+
Debug a YSK Kit failure by building a red feedback loop first: memory-port
|
|
5
|
+
tests, then HTTP on Express AND Fastify, then SDK, then e2e, then git bisect.
|
|
6
|
+
Use when a test, CI job, or user report is red; a flake; or /debug-issue.
|
|
7
|
+
中文:除錯、紅燈迴圈、回歸測試、bisect、日誌遮罩。
|
|
8
|
+
Do not use for green-field features (plan-feature) or a security audit (security-review).
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Read `docs/skills/debug-issue.md`. Law: `AGENTS.md`.
|
|
12
|
+
|
|
13
|
+
1. Write or isolate a red test first (memory-port, then Express+Fastify HTTP).
|
|
14
|
+
2. Triage by error code. Fix the cause. Keep the regression test.
|
|
15
|
+
3. Climb to SDK / e2e / git bisect only if the lower loop cannot see it.
|
|
16
|
+
Gotcha: do not log OTP/sk_/whsec_. Do not set RATE_LIMIT_MAX=0 in production.
|
|
17
|
+
Verify: the named Vitest is green, then the five verify commands.
|
|
18
|
+
Full steps: `docs/skills/debug-issue.md`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: desktop-electron
|
|
3
|
+
description: >
|
|
4
|
+
Harden the YSK Kit Electron desktop app: contextIsolation, sandbox, CSP,
|
|
5
|
+
navigation and new-window restrictions, IPC sender validation, safeStorage
|
|
6
|
+
with no plaintext fallback, auto-update signing.
|
|
7
|
+
Use when changing apps/desktop/src/main or preload, adding an IPC channel,
|
|
8
|
+
opening external URLs, filesystem access, auto-update, or packaging.
|
|
9
|
+
中文:Electron 安全、CSP、IPC、safeStorage、自動更新。
|
|
10
|
+
Do not use for renderer-only UI or web/admin token storage with no main-process change.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Read `docs/skills/desktop-electron.md`. Law: `AGENTS.md`.
|
|
14
|
+
|
|
15
|
+
1. Keep contextIsolation, nodeIntegration false, sandbox, webSecurity, session CSP.
|
|
16
|
+
2. Deny unexpected navigation / window.open. Validate IPC `senderFrame`.
|
|
17
|
+
3. `safeStorage` only; memory if unavailable. No plaintext token file.
|
|
18
|
+
Gotcha: do not `openExternal` a renderer string without an allowlist.
|
|
19
|
+
Verify: `pnpm --filter @ysk-kit/desktop test`
|
|
20
|
+
Full steps: `docs/skills/desktop-electron.md`.
|
|
@@ -3,6 +3,15 @@ name: envelope-api
|
|
|
3
3
|
description: >
|
|
4
4
|
Keep YSK Kit JSON routes on { ok, data } / { ok, error } unless the change is one of the four exceptions.
|
|
5
5
|
Use when adding a route, SSE, PDF, error shape, or /envelope-api.
|
|
6
|
+
中文:envelope、錯誤碼、AppError、webhook JSON。
|
|
7
|
+
Do not use to invent a parallel error JSON or a fifth envelope exception without asking.
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
Read `docs/skills/envelope-api.md`. Law: `AGENTS.md`.
|
|
11
|
+
|
|
12
|
+
1. DTO + OkSchema/ErrSchema first. `throw new AppError('<CODE>')`.
|
|
13
|
+
2. JSON stays envelope. Exceptions: LLM SSE, invoice PDF 302, GET /docs, GET /openapi.json.
|
|
14
|
+
3. Webhooks: still envelope, raw body, off ts-rest. Sync ERROR_MESSAGE zh-HK and en.
|
|
15
|
+
Gotcha: ask before a fifth exception. New codes need HTTP_STATUS too.
|
|
16
|
+
Verify: fixtures show `{ ok, data }` / `{ ok, error }`.
|
|
17
|
+
Full steps: `docs/skills/envelope-api.md`.
|
|
@@ -3,6 +3,15 @@ name: fix-layers
|
|
|
3
3
|
description: >
|
|
4
4
|
Repair a pnpm layers (dependency-cruiser) failure without disabling hexagonal rules.
|
|
5
5
|
Use when pnpm layers fails, a client imported Prisma, or /fix-layers.
|
|
6
|
+
中文:分層、dependency-cruiser、客戶端 Prisma。
|
|
7
|
+
Do not use to edit .dependency-cruiser.cjs without explicit approval.
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
Read `docs/skills/fix-layers.md`. Law: `AGENTS.md`.
|
|
11
|
+
|
|
12
|
+
1. Read the cruiser rule name (`clients-no-server-infra`, `domain-no-infra`, …).
|
|
13
|
+
2. Move the import (SDK / port in infra / type in contracts). Do not disable the rule.
|
|
14
|
+
3. Re-run `pnpm layers`, then verify-change.
|
|
15
|
+
Gotcha: never edit `.dependency-cruiser.cjs` without explicit this-turn approval.
|
|
16
|
+
Verify: `pnpm layers`
|
|
17
|
+
Full steps: `docs/skills/fix-layers.md`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: llm-feature
|
|
3
|
+
description: >
|
|
4
|
+
Build a YSK Kit LLM feature with a server-owned prompt, Zod-validated
|
|
5
|
+
output, untrusted-content delimiting, quota, and createFakeLlm evals.
|
|
6
|
+
Use when adding or changing /v1/llm, a product prompt, streaming, or ysk-kit add llm.
|
|
7
|
+
中文:LLM、system prompt、配額、createFakeLlm、評測。
|
|
8
|
+
Do not use for generic chat UI with no server prompt, or for a security
|
|
9
|
+
audit of non-LLM code (security-review).
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Read `docs/skills/llm-feature.md`. Law: `AGENTS.md`.
|
|
13
|
+
|
|
14
|
+
1. Client roles are user/assistant only (`LlmClientRoleSchema`). Server prepends `LLM_SYSTEM_PROMPT`.
|
|
15
|
+
2. Fence untrusted user text. Parse structured output with Zod in application/.
|
|
16
|
+
3. Quota `LLM_QUOTA_*` → RATE_LIMITED. Evals: ≥5 normal + ≥3 adversarial `createFakeLlm`. No live provider in CI.
|
|
17
|
+
Gotcha: v1.2.2 already removed client `system` and added quota — describe current code, do not list those as gaps.
|
|
18
|
+
Verify: `pnpm --filter @ysk-kit/api exec vitest run src/modules/llm`
|
|
19
|
+
Full steps: `docs/skills/llm-feature.md`.
|
|
@@ -3,6 +3,15 @@ name: new-product
|
|
|
3
3
|
description: >
|
|
4
4
|
Scaffold a product from YSK Kit with create-ysk-app (flavor, preset, database).
|
|
5
5
|
Use when the user wants a new product, create-ysk-app, or /new-product.
|
|
6
|
+
中文:開新產品、create-ysk-app、flavor、doctor。
|
|
7
|
+
Do not use to add a module inside this kit (add-module) or to invent a parallel monorepo.
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
Read `docs/skills/new-product.md`. Law: `AGENTS.md`.
|
|
11
|
+
|
|
12
|
+
1. Pick flavor (saas default; desktop / gateway / php-bridge / trading / static-web3).
|
|
13
|
+
2. `pnpm create @ysk-kit/app <name> --preset thin --db mysql --flavor saas --yes`.
|
|
14
|
+
3. migrate/seed when the flavor has an API. Run `pnpm ysk-kit doctor`.
|
|
15
|
+
Gotcha: leave `eas.json` / `app.config.ts` projectId as `replace-me` for a human.
|
|
16
|
+
Verify: `pnpm ysk-kit doctor`
|
|
17
|
+
Full steps: `docs/skills/new-product.md`.
|
|
@@ -3,6 +3,15 @@ name: plan-feature
|
|
|
3
3
|
description: >
|
|
4
4
|
Write a YSK Kit feature plan to docs/plans/<yyyy-mm-dd>-<slug>.md before contracts and code.
|
|
5
5
|
Use when the user wants a plan, plan.md, design, or a multi-surface change.
|
|
6
|
+
中文:計劃、plan.md、合約之前。
|
|
7
|
+
Do not use for typo-only or one-file mechanical renames.
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
Read `docs/skills/plan-feature.md`. Law: `AGENTS.md`.
|
|
11
|
+
|
|
12
|
+
1. Confirm a plan is required. Explore the tree (modules, contracts, SDK) first.
|
|
13
|
+
2. `pnpm ysk-kit plan <slug>` and fill every template heading.
|
|
14
|
+
3. `pnpm ysk-kit plan --check` both language files. Wait for approval.
|
|
15
|
+
Gotcha: do not implement first and backfill. If rejected as too short, expand.
|
|
16
|
+
Verify: `pnpm ysk-kit plan --check docs/plans/<date>-<slug>.md`
|
|
17
|
+
Full steps: `docs/skills/plan-feature.md`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: review-change
|
|
3
|
+
description: >
|
|
4
|
+
Review a YSK Kit diff on two axes: Law (AGENTS.md hard rules, definition of
|
|
5
|
+
done, Do-not list) and Spec (the dated plan's scope and checklist). Flag
|
|
6
|
+
high-risk items for a human.
|
|
7
|
+
Use when asked to review a PR, a branch vs main, or uncommitted work; or /review-change.
|
|
8
|
+
中文:審查、法律軸、計劃軸、高風險人工、合理化表。
|
|
9
|
+
Do not use for implementing the change (plan-feature) or a dedicated
|
|
10
|
+
security/Prisma deep dive (security-review, db-migration).
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Read `docs/skills/review-change.md`. Law: `AGENTS.md`.
|
|
14
|
+
|
|
15
|
+
1. Diff is the report. Score Law (hard rules/DoD/Do-not) and Spec (dated plan).
|
|
16
|
+
2. Flag authz, webhooks, SQL DROP, secrets, LLM, breaking contracts for a human.
|
|
17
|
+
3. Run verify-change. Fill the rationalization table (no empty reasons).
|
|
18
|
+
Gotcha: do not self-approve high-risk; link security-review / db-migration.
|
|
19
|
+
Verify: `pnpm layers && pnpm typecheck && pnpm test && pnpm gen:openapi && pnpm ysk-kit check agent`
|
|
20
|
+
Full steps: `docs/skills/review-change.md`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-review
|
|
3
|
+
description: >
|
|
4
|
+
Review YSK Kit authz, tenant isolation, secrets, logging redaction, token
|
|
5
|
+
storage, webhook signatures, and LLM input against hexagonal modules and
|
|
6
|
+
the {ok,data}/{ok,error} envelope. Optional lightweight threat-model mode.
|
|
7
|
+
Use when the user asks for a security review or audit; the diff touches
|
|
8
|
+
auth, apikey, organizations, billing, files, crypto, webhooks, env/secrets,
|
|
9
|
+
or a new capability; or the plan's security section is not "none".
|
|
10
|
+
中文:安全審查、權限、跨租戶、密鑰、日誌遮罩、webhook 簽名。
|
|
11
|
+
Do not use for pure UI styling, docs typos, or generic dependency bumps.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Read `docs/skills/security-review.md`. Law: `AGENTS.md`.
|
|
15
|
+
|
|
16
|
+
1. Diff is the report scope; the repo is research. HIGH confidence only.
|
|
17
|
+
2. AuthZ in `application/` (membership + role). Cross-tenant memory tests.
|
|
18
|
+
3. No secrets in logs. Webhook signatures. Client token stores.
|
|
19
|
+
4. Threat-model sub-mode only when the user asks or a new trust boundary lands.
|
|
20
|
+
Full steps: `docs/skills/security-review.md`.
|
|
@@ -5,6 +5,15 @@ description: >
|
|
|
5
5
|
mapped to contract, memory-port, API, SDK, client, and Playwright layers.
|
|
6
6
|
Use when the user asks how to test a feature, what cases to cover, or for a QA
|
|
7
7
|
checklist before writing tests or shipping.
|
|
8
|
+
中文:測試計劃、風險、given/when/then。
|
|
9
|
+
Do not use for comment-only edits or to write the tests themselves (write-tests).
|
|
8
10
|
---
|
|
9
11
|
|
|
10
12
|
Read `docs/skills/test-plan.md`. Law: `AGENTS.md`.
|
|
13
|
+
|
|
14
|
+
1. Restate the change. Rank data loss, auth, money, tenancy, secrets first.
|
|
15
|
+
2. Map each risk to a layer and file. Write given/when/then cases.
|
|
16
|
+
3. Name in-memory fixtures and explicit out of scope. Then write-tests.
|
|
17
|
+
Gotcha: do not start Redis or Stripe to plan tests. E2E stays thin.
|
|
18
|
+
Verify: every case has layer + file; out of scope is written.
|
|
19
|
+
Full steps: `docs/skills/test-plan.md`.
|
|
@@ -4,6 +4,15 @@ description: >
|
|
|
4
4
|
Design and implement YSK Kit UI with existing tokens, @ysk-kit/ui, WCAG 2.2 AA,
|
|
5
5
|
forms mapped from the API envelope, and no generic AI-slop chrome.
|
|
6
6
|
Use when adding or changing web, admin, mobile, or desktop screens.
|
|
7
|
+
中文:畫面、zinc、表單、可及性。
|
|
8
|
+
Do not use for API-only changes or to invent a second component library.
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
Read `docs/skills/ui-design.md`. Law: `AGENTS.md`.
|
|
12
|
+
|
|
13
|
+
1. Reuse `@ysk-kit/ui` zinc tokens (`rounded-md`, `h-9`). No new palette.
|
|
14
|
+
2. Loading / empty / error / disabled. Forms: FormField + contracts Zod.
|
|
15
|
+
3. WCAG 2.2 AA, `zh-HK` length, no raw fetch. Then ui-review.
|
|
16
|
+
Gotcha: no purple gradients, no placeholder-as-label, no `div` buttons.
|
|
17
|
+
Verify: ui-review checklist, then verify-change.
|
|
18
|
+
Full steps: `docs/skills/ui-design.md`.
|
|
@@ -4,6 +4,15 @@ description: >
|
|
|
4
4
|
QA checklist before declaring YSK Kit UI work done: visual, a11y, responsive,
|
|
5
5
|
states, copy, performance — with Playwright, keyboard, and axe verification.
|
|
6
6
|
Use when finishing a screen or when the user asks to review UI / UX / a11y.
|
|
7
|
+
中文:UI 審查、鍵盤、375 視埠。
|
|
8
|
+
Do not use to design the screen (ui-design) or to treat one screenshot as proof.
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
Read `docs/skills/ui-review.md`. Law: `AGENTS.md`.
|
|
12
|
+
|
|
13
|
+
1. Walk visual, a11y, states, copy. Fail if any required box is unchecked.
|
|
14
|
+
2. Keyboard-only on web/admin/desktop. Check 375 / 768 / 1280.
|
|
15
|
+
3. Exercise the flow (click, type, submit). Screenshot is not proof.
|
|
16
|
+
Gotcha: do not add axe or screenshot diffs to CI unless asked.
|
|
17
|
+
Verify: checklist + `pnpm e2e` when login/shell changed.
|
|
18
|
+
Full steps: `docs/skills/ui-review.md`.
|
|
@@ -3,6 +3,15 @@ name: verify-change
|
|
|
3
3
|
description: >
|
|
4
4
|
Run the YSK Kit verification chain after every feature (layers, typecheck, test, OpenAPI, check agent).
|
|
5
5
|
Use when finishing a change, after add module/capability, or /verify-change.
|
|
6
|
+
中文:驗證、layers、typecheck、OpenAPI、check agent。
|
|
7
|
+
Do not use to skip commands and claim done, or to start Redis/Stripe in unit tests.
|
|
6
8
|
---
|
|
7
9
|
|
|
8
10
|
Read `docs/skills/verify-change.md`. Law: `AGENTS.md`.
|
|
11
|
+
|
|
12
|
+
1. Run `pnpm layers && pnpm typecheck && pnpm test && pnpm gen:openapi && pnpm ysk-kit check agent`.
|
|
13
|
+
2. `git diff --exit-code docs/openapi.yaml` (commit YAML if HTTP changed).
|
|
14
|
+
3. Conditional: lint, check:links, zh pair, lockstep changeset, e2e.
|
|
15
|
+
Gotcha: never claim done without running the five commands in this turn.
|
|
16
|
+
Verify: emit the verification report in the skill output format.
|
|
17
|
+
Full steps: `docs/skills/verify-change.md`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webhook-handling
|
|
3
|
+
description: >
|
|
4
|
+
Handle inbound webhooks in YSK Kit: verify signature on the raw body, ack
|
|
5
|
+
fast, idempotency by event id, out-of-order events, retries, replay tests.
|
|
6
|
+
Stripe is the main example; the same rules apply to other providers.
|
|
7
|
+
Use when adding or changing /v1/*/webhook, adding billing, or debugging
|
|
8
|
+
"paid but not activated" / duplicate fulfilments.
|
|
9
|
+
中文:webhook、驗簽、冪等、Stripe 回調、重送。
|
|
10
|
+
Do not use for outbound HTTP or a general security audit with no webhook route.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Read `docs/skills/webhook-handling.md`. Law: `AGENTS.md`.
|
|
14
|
+
|
|
15
|
+
1. Mount raw body before JSON parsers on Express and Fastify.
|
|
16
|
+
2. Verify HMAC; persist Stripe `event.id`; 200 envelope; skip duplicates.
|
|
17
|
+
3. Replay tests in-memory. Do not call Stripe in CI.
|
|
18
|
+
Gotcha: 401 on bad signature. Fulfilment should queue, not block the ack.
|
|
19
|
+
Verify: `pnpm --filter @ysk-kit/api test`
|
|
20
|
+
Full steps: `docs/skills/webhook-handling.md`.
|
|
@@ -4,6 +4,15 @@ description: >
|
|
|
4
4
|
Write professional YSK Kit tests: trophy per layer, in-memory ports, given/when/then,
|
|
5
5
|
deterministic fixtures, envelope and authz cases, exact Vitest/Playwright commands.
|
|
6
6
|
Use when adding tests, fixing a bug, or after test-plan.
|
|
7
|
+
中文:寫測試、Vitest、Playwright、回歸。
|
|
8
|
+
Do not use to plan cases (test-plan) or to start Redis/Stripe in CI.
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
Read `docs/skills/write-tests.md`. Law: `AGENTS.md`.
|
|
12
|
+
|
|
13
|
+
1. Put the case at the lowest layer that can fail it. Extend the generator test file.
|
|
14
|
+
2. given/when/then; envelope `ok` / `error.code`; other-tenant negative when owned.
|
|
15
|
+
3. Bug fixes: red regression test first. Express and Fastify for HTTP.
|
|
16
|
+
Gotcha: in-memory ports only. Never mock Prisma from a client.
|
|
17
|
+
Verify: `pnpm --filter @ysk-kit/<pkg> exec vitest run <file>`
|
|
18
|
+
Full steps: `docs/skills/write-tests.md`.
|
|
@@ -3,4 +3,4 @@ description: YSK Kit agent law — follow root AGENTS.md
|
|
|
3
3
|
alwaysApply: true
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Follow `AGENTS.md` at the repo root (Chinese: `AGENTS.zh.md`). Do not copy that file here. Procedures: `docs/skills/`. Plan first when required: `pnpm ysk-kit plan <slug>` → `docs/plans/<yyyy-mm-dd>-<slug>.md`. New HTTP features start with `pnpm ysk-kit add module <kebab-name> --prisma --web`. Verify with `pnpm layers && pnpm typecheck && pnpm test && pnpm gen:openapi && pnpm ysk-kit check agent`.
|
|
6
|
+
Follow `AGENTS.md` at the repo root (Chinese: `AGENTS.zh.md`). Do not copy that file here. Procedures: `docs/skills/`. Plan first when required: `pnpm ysk-kit plan <slug>` → `docs/plans/<yyyy-mm-dd>-<slug>.md`. New HTTP features start with `pnpm ysk-kit add module <kebab-name> --prisma --web`. Verify with `pnpm layers && pnpm typecheck && pnpm test && pnpm gen:openapi && pnpm ysk-kit check agent`. Daily loop: `contract-change`, `debug-issue`, `review-change`, `llm-feature`. Security / Prisma / webhooks / Electron: `docs/skills/security-review.md`, `db-migration.md`, `webhook-handling.md`, `desktop-electron.md`.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createMemoryOrganizationRepository } from '../../organizations/infra/memory-organization-repository';
|
|
3
|
+
import { createLogBilling } from '../infra/log-billing';
|
|
4
|
+
import { createMemorySubscriptionRepository } from '../infra/memory-subscription-repository';
|
|
5
|
+
import { createMemoryWebhookEventRepository } from '../infra/memory-webhook-event-repository';
|
|
6
|
+
import { createBillingService } from './billing-service';
|
|
7
|
+
|
|
8
|
+
describe('createBillingService webhooks', () => {
|
|
9
|
+
it('activates once and skips a replay plus a stale event', async () => {
|
|
10
|
+
const orgs = createMemoryOrganizationRepository();
|
|
11
|
+
const subscriptions = createMemorySubscriptionRepository();
|
|
12
|
+
const service = createBillingService({
|
|
13
|
+
subscriptions,
|
|
14
|
+
billing: createLogBilling(),
|
|
15
|
+
orgs,
|
|
16
|
+
webhookEvents: createMemoryWebhookEventRepository(),
|
|
17
|
+
});
|
|
18
|
+
const owner = await orgs.createWithOwner({ name: 'Pay', ownerUserId: 'user-1' });
|
|
19
|
+
await service.handleStripeEvent({
|
|
20
|
+
id: 'evt_new',
|
|
21
|
+
type: 'checkout.session.completed',
|
|
22
|
+
created: 100,
|
|
23
|
+
data: {
|
|
24
|
+
object: {
|
|
25
|
+
customer: 'cus_1',
|
|
26
|
+
metadata: { organizationId: owner.id, planCode: 'pro', seatCount: '4' },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
await service.handleStripeEvent({
|
|
31
|
+
id: 'evt_new',
|
|
32
|
+
type: 'checkout.session.completed',
|
|
33
|
+
created: 100,
|
|
34
|
+
data: {
|
|
35
|
+
object: {
|
|
36
|
+
customer: 'cus_1',
|
|
37
|
+
metadata: { organizationId: owner.id, planCode: 'pro', seatCount: '4' },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
await service.handleStripeEvent({
|
|
42
|
+
id: 'evt_old',
|
|
43
|
+
type: 'checkout.session.completed',
|
|
44
|
+
created: 50,
|
|
45
|
+
data: {
|
|
46
|
+
object: {
|
|
47
|
+
customer: 'cus_1',
|
|
48
|
+
metadata: { organizationId: owner.id, planCode: 'pro', seatCount: '1' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const row = await subscriptions.findByOrganizationId(owner.id);
|
|
53
|
+
expect(row?.seatCount).toBe(4);
|
|
54
|
+
expect(row?.status).toBe('active');
|
|
55
|
+
});
|
|
56
|
+
});
|
package/templates/capabilities/billing/apps/api/src/modules/billing/application/billing-service.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { BILLING_PLANS, orgRoleCan } from '@ysk-kit/contracts';
|
|
|
3
3
|
import { AppError } from '@ysk-kit/domain-kernel';
|
|
4
4
|
import type { IOrganizationRepository } from '../../organizations/domain/organization-repository';
|
|
5
5
|
import type { IBillingPort } from '../domain/billing-port';
|
|
6
|
+
import type { StripeWebhookEvent } from '../domain/stripe-webhook-event';
|
|
6
7
|
import type { ISubscriptionRepository } from '../domain/subscription-repository';
|
|
8
|
+
import type { IProcessedWebhookRepository } from '../domain/webhook-event-repository';
|
|
7
9
|
|
|
8
10
|
const PERIOD_MS = 1000 * 60 * 60 * 24 * 30;
|
|
9
11
|
|
|
@@ -30,6 +32,7 @@ export const createBillingService = (opts: {
|
|
|
30
32
|
subscriptions: ISubscriptionRepository;
|
|
31
33
|
billing: IBillingPort;
|
|
32
34
|
orgs: IOrganizationRepository;
|
|
35
|
+
webhookEvents: IProcessedWebhookRepository;
|
|
33
36
|
now?: () => Date;
|
|
34
37
|
}) => {
|
|
35
38
|
const now = () => opts.now?.() ?? new Date();
|
|
@@ -44,6 +47,25 @@ export const createBillingService = (opts: {
|
|
|
44
47
|
return org;
|
|
45
48
|
};
|
|
46
49
|
|
|
50
|
+
const activate = async (
|
|
51
|
+
organizationId: string,
|
|
52
|
+
planCode: SubscriptionDto['planCode'],
|
|
53
|
+
customerId?: string,
|
|
54
|
+
seatCount = 1,
|
|
55
|
+
) => {
|
|
56
|
+
if (planCode === 'free') return;
|
|
57
|
+
const org = await opts.orgs.findById(organizationId);
|
|
58
|
+
if (!org) throw new AppError('NOT_FOUND');
|
|
59
|
+
await opts.subscriptions.upsert({
|
|
60
|
+
organizationId,
|
|
61
|
+
planCode,
|
|
62
|
+
status: 'active',
|
|
63
|
+
currentPeriodEnd: new Date(now().getTime() + PERIOD_MS),
|
|
64
|
+
seatCount,
|
|
65
|
+
});
|
|
66
|
+
if (customerId) await opts.orgs.setStripeCustomerId(organizationId, customerId);
|
|
67
|
+
};
|
|
68
|
+
|
|
47
69
|
return {
|
|
48
70
|
plans: () => BILLING_PLANS,
|
|
49
71
|
subscription: async (userId: string, organizationId: string): Promise<SubscriptionDto> => {
|
|
@@ -64,24 +86,7 @@ export const createBillingService = (opts: {
|
|
|
64
86
|
invoiceId,
|
|
65
87
|
});
|
|
66
88
|
},
|
|
67
|
-
activate
|
|
68
|
-
organizationId: string,
|
|
69
|
-
planCode: SubscriptionDto['planCode'],
|
|
70
|
-
customerId?: string,
|
|
71
|
-
seatCount = 1,
|
|
72
|
-
) => {
|
|
73
|
-
if (planCode === 'free') return;
|
|
74
|
-
const org = await opts.orgs.findById(organizationId);
|
|
75
|
-
if (!org) throw new AppError('NOT_FOUND');
|
|
76
|
-
await opts.subscriptions.upsert({
|
|
77
|
-
organizationId,
|
|
78
|
-
planCode,
|
|
79
|
-
status: 'active',
|
|
80
|
-
currentPeriodEnd: new Date(now().getTime() + PERIOD_MS),
|
|
81
|
-
seatCount,
|
|
82
|
-
});
|
|
83
|
-
if (customerId) await opts.orgs.setStripeCustomerId(organizationId, customerId);
|
|
84
|
-
},
|
|
89
|
+
activate,
|
|
85
90
|
portal: async (userId: string, organizationId: string, returnUrl: string) => {
|
|
86
91
|
const org = await requireBiller(userId, organizationId);
|
|
87
92
|
const customerId = org.stripeCustomerId;
|
|
@@ -120,6 +125,54 @@ export const createBillingService = (opts: {
|
|
|
120
125
|
}
|
|
121
126
|
return { url: result.url };
|
|
122
127
|
},
|
|
128
|
+
handleStripeEvent: async (event: StripeWebhookEvent) => {
|
|
129
|
+
if (!event.id) throw new AppError('VALIDATION_FAILED', 'Stripe event id required');
|
|
130
|
+
const organizationId = event.data.object.metadata?.organizationId;
|
|
131
|
+
const eventCreatedAt = new Date((Number.isFinite(event.created) ? event.created : 0) * 1000);
|
|
132
|
+
const claim = await opts.webhookEvents.tryClaim({
|
|
133
|
+
provider: 'stripe',
|
|
134
|
+
eventId: event.id,
|
|
135
|
+
eventType: event.type,
|
|
136
|
+
eventCreatedAt,
|
|
137
|
+
...(organizationId ? { organizationId } : {}),
|
|
138
|
+
});
|
|
139
|
+
if (claim === 'duplicate') return { received: true as const };
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
if (organizationId) {
|
|
143
|
+
const latest = await opts.webhookEvents.maxEventCreatedAt(
|
|
144
|
+
'stripe',
|
|
145
|
+
organizationId,
|
|
146
|
+
event.id,
|
|
147
|
+
);
|
|
148
|
+
if (latest && eventCreatedAt < latest) return { received: true as const };
|
|
149
|
+
}
|
|
150
|
+
if (event.type === 'checkout.session.completed') {
|
|
151
|
+
const planCode = event.data.object.metadata?.planCode;
|
|
152
|
+
const seatRaw = Number(event.data.object.metadata?.seatCount ?? '1');
|
|
153
|
+
const seatCount = Number.isFinite(seatRaw) && seatRaw >= 1 ? Math.floor(seatRaw) : 1;
|
|
154
|
+
if (organizationId && (planCode === 'pro' || planCode === 'free')) {
|
|
155
|
+
const customer = event.data.object.customer;
|
|
156
|
+
await activate(
|
|
157
|
+
organizationId,
|
|
158
|
+
planCode,
|
|
159
|
+
typeof customer === 'string' ? customer : undefined,
|
|
160
|
+
seatCount,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { received: true as const };
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (
|
|
167
|
+
error instanceof AppError &&
|
|
168
|
+
(error.code === 'NOT_FOUND' || error.code === 'VALIDATION_FAILED')
|
|
169
|
+
) {
|
|
170
|
+
return { received: true as const };
|
|
171
|
+
}
|
|
172
|
+
await opts.webhookEvents.release('stripe', event.id);
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
123
176
|
cancel: async (userId: string, organizationId: string) => {
|
|
124
177
|
await requireBiller(userId, organizationId);
|
|
125
178
|
const row = await opts.subscriptions.findByOrganizationId(organizationId);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ProcessedWebhookClaim = 'claimed' | 'duplicate';
|
|
2
|
+
|
|
3
|
+
export type ProcessedWebhookRecord = {
|
|
4
|
+
provider: string;
|
|
5
|
+
eventId: string;
|
|
6
|
+
eventType: string;
|
|
7
|
+
eventCreatedAt: Date;
|
|
8
|
+
organizationId?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface IProcessedWebhookRepository {
|
|
12
|
+
tryClaim(input: ProcessedWebhookRecord): Promise<ProcessedWebhookClaim>;
|
|
13
|
+
maxEventCreatedAt(
|
|
14
|
+
provider: string,
|
|
15
|
+
organizationId: string,
|
|
16
|
+
exceptEventId: string,
|
|
17
|
+
): Promise<Date | null>;
|
|
18
|
+
release(provider: string, eventId: string): Promise<void>;
|
|
19
|
+
}
|