docs: plan autumn registration sync
This commit is contained in:
@@ -0,0 +1,657 @@
|
||||
# cfw-auth 注册同步 Autumn 实现计划
|
||||
|
||||
> **给 agent 执行者:** 必选子技能:使用 `superpowers:subagent-driven-development`(推荐)或 `superpowers:executing-plans` 按任务逐项实现本计划。步骤统一使用 checkbox(`- [ ]`)语法跟踪。
|
||||
|
||||
**目标:** 在 `cfw-auth` 用户注册成功后,异步创建或获取 Autumn customer,并自动启用配置的免费套餐。
|
||||
|
||||
**方案概览:** 新增 `src/autumn.ts` 作为唯一 Autumn 同步边界,用最小 `fetch` 调用 Autumn `POST /v1/customers.get_or_create`,避免为单个接口先引入 `autumn-js`。在 `src/auth.ts` 的 Better Auth `databaseHooks.user.create.after` 中触发同步;Worker 场景使用 `waitUntil`,测试或非 Worker 场景直接等待。同步失败 fail-open,只记录脱敏结构化日志。
|
||||
|
||||
**技术栈:** Cloudflare Workers、Hono、Better Auth、Vitest、TypeScript、Autumn HTTP API
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
- 新建:`src/autumn.ts`
|
||||
- Autumn 注册同步边界模块。负责 env 解析、payload 构造、HTTP 调用和脱敏错误日志。
|
||||
- 修改:`src/env.ts`
|
||||
- 增加 Autumn 相关 Worker env 类型。
|
||||
- 修改:`src/auth.ts`
|
||||
- 在 Better Auth 配置中挂载 `databaseHooks.user.create.after`,调用 Autumn 同步模块。
|
||||
- 新建:`tests/autumn.test.ts`
|
||||
- 覆盖 env、payload、fetch、fail-open、日志脱敏和关闭开关。
|
||||
- 修改:`tests/auth-config.test.ts`
|
||||
- 覆盖 `createAuth` 注册 hook 的 waitUntil 调度行为。
|
||||
- 修改:`TODO.md`
|
||||
- 增加生产环境 Autumn 变量说明。
|
||||
|
||||
## 任务 1:新增 Autumn 同步模块的失败测试
|
||||
|
||||
**文件:**
|
||||
- 新建:`tests/autumn.test.ts`
|
||||
- 后续实现:`src/autumn.ts`
|
||||
|
||||
- [ ] **步骤 1:写失败测试**
|
||||
|
||||
创建 `tests/autumn.test.ts`,完整内容如下:
|
||||
|
||||
```ts
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
AUTUMN_DEFAULT_API_URL,
|
||||
buildAutumnCustomerPayload,
|
||||
syncAutumnCustomerOnRegistration,
|
||||
} from "../src/autumn";
|
||||
import type { Env } from "../src/env";
|
||||
|
||||
const baseEnv: Env = {
|
||||
BETTER_AUTH_URL: "http://localhost:8788",
|
||||
TRUSTED_ORIGINS: "http://localhost:8787",
|
||||
AUTUMN_SECRET_KEY: "autumn-secret",
|
||||
AUTUMN_FREE_PLAN_ID: "free",
|
||||
AUTUMN_API_URL: "https://autumn.example.com",
|
||||
};
|
||||
|
||||
const user = {
|
||||
id: "user_123",
|
||||
email: "user@example.com",
|
||||
name: "Example User",
|
||||
};
|
||||
|
||||
let fetchMock: ReturnType<typeof vi.fn>;
|
||||
let errorLog: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ id: "user_123" }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
errorLog = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("Autumn registration sync", () => {
|
||||
it("builds a stable user-scoped customer payload", () => {
|
||||
expect(buildAutumnCustomerPayload(baseEnv, user)).toEqual({
|
||||
customer_id: "user_123",
|
||||
email: "user@example.com",
|
||||
name: "Example User",
|
||||
auto_enable_plan_id: "free",
|
||||
metadata: {
|
||||
source: "cfw-auth",
|
||||
auth_provider: "better-auth",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the Autumn default API URL when AUTUMN_API_URL is not configured", async () => {
|
||||
await syncAutumnCustomerOnRegistration(
|
||||
{
|
||||
...baseEnv,
|
||||
AUTUMN_API_URL: undefined,
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
`${AUTUMN_DEFAULT_API_URL}/v1/customers.get_or_create`,
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("posts get_or_create with the configured secret and free plan", async () => {
|
||||
await syncAutumnCustomerOnRegistration(baseEnv, user);
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(url).toBe("https://autumn.example.com/v1/customers.get_or_create");
|
||||
expect(init.method).toBe("POST");
|
||||
expect(init.headers).toMatchObject({
|
||||
Authorization: "Bearer autumn-secret",
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
expect(JSON.parse(String(init.body))).toEqual({
|
||||
customer_id: "user_123",
|
||||
email: "user@example.com",
|
||||
name: "Example User",
|
||||
auto_enable_plan_id: "free",
|
||||
metadata: {
|
||||
source: "cfw-auth",
|
||||
auth_provider: "better-auth",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not call Autumn when registration sync is disabled", async () => {
|
||||
await syncAutumnCustomerOnRegistration(
|
||||
{
|
||||
...baseEnv,
|
||||
AUTUMN_REGISTER_SYNC_ENABLED: "false",
|
||||
},
|
||||
user,
|
||||
);
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(errorLog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails open and logs a redacted config error when required env is missing", async () => {
|
||||
await expect(
|
||||
syncAutumnCustomerOnRegistration(
|
||||
{
|
||||
...baseEnv,
|
||||
AUTUMN_SECRET_KEY: undefined,
|
||||
},
|
||||
user,
|
||||
),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
expect(errorLog).toHaveBeenCalledTimes(1);
|
||||
const message = String(errorLog.mock.calls[0]?.[0]);
|
||||
expect(message).toContain("autumn_registration_sync_failed");
|
||||
expect(message).toContain("AUTUMN_SECRET_KEY");
|
||||
expect(message).not.toContain("autumn-secret");
|
||||
expect(message).not.toContain("Authorization");
|
||||
});
|
||||
|
||||
it("fails open and logs a redacted Autumn API error", async () => {
|
||||
fetchMock.mockResolvedValueOnce(
|
||||
new Response(JSON.stringify({ message: "upstream exploded" }), {
|
||||
status: 500,
|
||||
headers: { "content-type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(syncAutumnCustomerOnRegistration(baseEnv, user)).resolves.toBeUndefined();
|
||||
|
||||
expect(errorLog).toHaveBeenCalledTimes(1);
|
||||
const message = String(errorLog.mock.calls[0]?.[0]);
|
||||
expect(message).toContain("autumn_registration_sync_failed");
|
||||
expect(message).toContain("user_123");
|
||||
expect(message).toContain("500");
|
||||
expect(message).not.toContain("autumn-secret");
|
||||
expect(message).not.toContain("Bearer");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:运行测试,确认它先失败**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
pnpm vitest run tests/autumn.test.ts
|
||||
```
|
||||
|
||||
预期:FAIL,错误包含 `Cannot find module '../src/autumn'` 或 `Failed to resolve import "../src/autumn"`。
|
||||
|
||||
## 任务 2:实现 `src/autumn.ts`
|
||||
|
||||
**文件:**
|
||||
- 新建:`src/autumn.ts`
|
||||
- 修改:`src/env.ts`
|
||||
- 测试:`tests/autumn.test.ts`
|
||||
|
||||
- [ ] **步骤 1:先补充 `Env` 类型**
|
||||
|
||||
在 `src/env.ts` 的 `Env` interface 中,放在 `API_KEY_DEFER_UPDATES?: string;` 后追加:
|
||||
|
||||
```ts
|
||||
AUTUMN_SECRET_KEY?: string;
|
||||
AUTUMN_API_URL?: string;
|
||||
AUTUMN_FREE_PLAN_ID?: string;
|
||||
AUTUMN_REGISTER_SYNC_ENABLED?: string;
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:编写最小实现**
|
||||
|
||||
创建 `src/autumn.ts`,完整内容如下:
|
||||
|
||||
```ts
|
||||
import type { Env } from "./env";
|
||||
|
||||
export const AUTUMN_DEFAULT_API_URL = "https://api.useautumn.com";
|
||||
|
||||
export interface AutumnRegistrationUser {
|
||||
id: string;
|
||||
email?: string | null;
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
export interface AutumnCustomerPayload {
|
||||
customer_id: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
auto_enable_plan_id: string;
|
||||
metadata: {
|
||||
source: "cfw-auth";
|
||||
auth_provider: "better-auth";
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAutumnCustomerPayload(
|
||||
env: Env,
|
||||
user: AutumnRegistrationUser,
|
||||
): AutumnCustomerPayload {
|
||||
const autoEnablePlanId = requireAutumnEnv(env.AUTUMN_FREE_PLAN_ID, "AUTUMN_FREE_PLAN_ID");
|
||||
const payload: AutumnCustomerPayload = {
|
||||
customer_id: user.id,
|
||||
auto_enable_plan_id: autoEnablePlanId,
|
||||
metadata: {
|
||||
source: "cfw-auth",
|
||||
auth_provider: "better-auth",
|
||||
},
|
||||
};
|
||||
|
||||
if (user.email) {
|
||||
payload.email = user.email;
|
||||
}
|
||||
|
||||
if (user.name) {
|
||||
payload.name = user.name;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function syncAutumnCustomerOnRegistration(
|
||||
env: Env,
|
||||
user: AutumnRegistrationUser,
|
||||
): Promise<void> {
|
||||
if (!isAutumnRegistrationSyncEnabled(env)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const secretKey = requireAutumnEnv(env.AUTUMN_SECRET_KEY, "AUTUMN_SECRET_KEY");
|
||||
const payload = buildAutumnCustomerPayload(env, user);
|
||||
const response = await fetch(`${autumnApiUrl(env)}/v1/customers.get_or_create`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${secretKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new AutumnRegistrationSyncError("Autumn customer sync failed", response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
logAutumnRegistrationSyncError(env, user, error);
|
||||
}
|
||||
}
|
||||
|
||||
export function isAutumnRegistrationSyncEnabled(env: Env): boolean {
|
||||
return env.AUTUMN_REGISTER_SYNC_ENABLED !== "false";
|
||||
}
|
||||
|
||||
function autumnApiUrl(env: Env): string {
|
||||
return (env.AUTUMN_API_URL || AUTUMN_DEFAULT_API_URL).replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function requireAutumnEnv(value: string | undefined, key: string): string {
|
||||
if (!value) {
|
||||
throw new AutumnRegistrationSyncError(`Missing required env: ${key}`, 0, "config_error");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
class AutumnRegistrationSyncError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status: number,
|
||||
readonly code = "autumn_request_failed",
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
function logAutumnRegistrationSyncError(
|
||||
env: Env,
|
||||
user: AutumnRegistrationUser,
|
||||
error: unknown,
|
||||
): void {
|
||||
try {
|
||||
const knownError = error instanceof AutumnRegistrationSyncError ? error : undefined;
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
event: "autumn_registration_sync_failed",
|
||||
userId: user.id,
|
||||
status: knownError?.status ?? 0,
|
||||
code: knownError?.code ?? "autumn_request_failed",
|
||||
message: error instanceof Error ? error.message : "Autumn customer sync failed",
|
||||
autumnApiUrlConfigured: Boolean(env.AUTUMN_API_URL),
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
// Billing sync observability must not affect auth registration.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:运行 Autumn 单测,确认通过**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
pnpm vitest run tests/autumn.test.ts
|
||||
```
|
||||
|
||||
预期:PASS。
|
||||
|
||||
- [ ] **步骤 4:提交**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
git add src/autumn.ts src/env.ts tests/autumn.test.ts
|
||||
git commit -m "feat: add autumn registration sync client"
|
||||
```
|
||||
|
||||
预期:提交成功。
|
||||
|
||||
## 任务 3:补充 Autumn 配置文档
|
||||
|
||||
**文件:**
|
||||
- 修改:`TODO.md`
|
||||
- 测试:`tests/autumn.test.ts`
|
||||
|
||||
- [ ] **步骤 1:补一条 Env 形状测试**
|
||||
|
||||
在 `tests/autumn.test.ts` 的 `describe("Autumn registration sync", () => {` 内追加这个测试:
|
||||
|
||||
```ts
|
||||
it("accepts the Autumn registration sync env shape", () => {
|
||||
const env: Env = {
|
||||
BETTER_AUTH_URL: "http://localhost:8788",
|
||||
TRUSTED_ORIGINS: "http://localhost:8787",
|
||||
AUTUMN_SECRET_KEY: "secret",
|
||||
AUTUMN_FREE_PLAN_ID: "free",
|
||||
AUTUMN_API_URL: "https://autumn.example.com",
|
||||
AUTUMN_REGISTER_SYNC_ENABLED: "true",
|
||||
};
|
||||
|
||||
expect(env.AUTUMN_FREE_PLAN_ID).toBe("free");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:运行类型检查,确认通过**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
预期:PASS。
|
||||
|
||||
- [ ] **步骤 3:更新 `TODO.md` 的生产变量说明**
|
||||
|
||||
在 `TODO.md` 的 `Configure Only If Enabling The Feature` 表格末尾追加这四行:
|
||||
|
||||
```md
|
||||
| `AUTUMN_SECRET_KEY` | Enable registration-time Autumn customer sync. | Required when `AUTUMN_REGISTER_SYNC_ENABLED` is not `false`; store as a Worker secret. |
|
||||
| `AUTUMN_FREE_PLAN_ID` | Auto-enable the free Autumn plan during registration. | Required when Autumn registration sync is enabled; no code default. |
|
||||
| `AUTUMN_API_URL` | Override Autumn API URL. | Optional; omit to use `https://api.useautumn.com`. |
|
||||
| `AUTUMN_REGISTER_SYNC_ENABLED` | Disable registration sync when set to `false`. | Optional; defaults to enabled so missing required Autumn env is visible in logs. |
|
||||
```
|
||||
|
||||
- [ ] **步骤 4:运行测试和类型检查**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
pnpm vitest run tests/autumn.test.ts
|
||||
```
|
||||
|
||||
预期:两个命令都 PASS。
|
||||
|
||||
- [ ] **步骤 5:提交**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
git add TODO.md tests/autumn.test.ts
|
||||
git commit -m "chore: document autumn registration sync env"
|
||||
```
|
||||
|
||||
预期:提交成功。
|
||||
|
||||
## 任务 4:把 Autumn 同步接入 Better Auth 注册 hook
|
||||
|
||||
**文件:**
|
||||
- 修改:`src/auth.ts`
|
||||
- 修改:`tests/auth-config.test.ts`
|
||||
|
||||
- [ ] **步骤 1:写失败测试,确认注册 hook 会调度 waitUntil**
|
||||
|
||||
在 `tests/auth-config.test.ts` 的 import 区,把 `describe, expect, it` 改成:
|
||||
|
||||
```ts
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
```
|
||||
|
||||
在文件中 `describe("production auth config", () => {` 之前追加:
|
||||
|
||||
```ts
|
||||
describe("Autumn registration hook", () => {
|
||||
it("schedules Autumn sync after Better Auth creates a user", async () => {
|
||||
const waitUntil = vi.fn();
|
||||
const auth = createAuth(
|
||||
{
|
||||
...env,
|
||||
AUTUMN_SECRET_KEY: "autumn-secret",
|
||||
AUTUMN_FREE_PLAN_ID: "free",
|
||||
},
|
||||
{
|
||||
waitUntil,
|
||||
},
|
||||
);
|
||||
|
||||
const hook = (
|
||||
auth as unknown as {
|
||||
options: {
|
||||
databaseHooks?: {
|
||||
user?: {
|
||||
create?: {
|
||||
after?: (
|
||||
user: { id: string; email?: string | null; name?: string | null },
|
||||
context?: unknown,
|
||||
) => Promise<void>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
).options.databaseHooks?.user?.create?.after;
|
||||
expect(hook).toEqual(expect.any(Function));
|
||||
|
||||
await hook?.(
|
||||
{
|
||||
id: "user_123",
|
||||
email: "user@example.com",
|
||||
name: "Example User",
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(waitUntil).toHaveBeenCalledTimes(1);
|
||||
expect(waitUntil.mock.calls[0]?.[0]).toBeInstanceOf(Promise);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:运行测试,确认它先失败**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
pnpm vitest run tests/auth-config.test.ts --testNamePattern "Autumn registration hook"
|
||||
```
|
||||
|
||||
预期:FAIL,错误显示 `databaseHooks` 或 `user.create.after` 是 `undefined`。
|
||||
|
||||
- [ ] **步骤 3:修改 `src/auth.ts`**
|
||||
|
||||
在 import 区追加:
|
||||
|
||||
```ts
|
||||
import { syncAutumnCustomerOnRegistration } from "./autumn";
|
||||
```
|
||||
|
||||
在 `trustedOrigins: trustedOrigins(env, runtime.requestOrigin),` 后追加:
|
||||
|
||||
```ts
|
||||
databaseHooks: {
|
||||
user: {
|
||||
create: {
|
||||
after: async (user) => {
|
||||
const sync = syncAutumnCustomerOnRegistration(env, user);
|
||||
if (runtime.waitUntil) {
|
||||
runtime.waitUntil(sync);
|
||||
return;
|
||||
}
|
||||
|
||||
await sync;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **步骤 4:运行 hook 测试,确认通过**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
pnpm vitest run tests/auth-config.test.ts --testNamePattern "Autumn registration hook"
|
||||
```
|
||||
|
||||
预期:PASS。
|
||||
|
||||
- [ ] **步骤 5:运行相关测试和类型检查**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
pnpm vitest run tests/auth-config.test.ts tests/autumn.test.ts
|
||||
```
|
||||
|
||||
预期:两个命令都 PASS。
|
||||
|
||||
- [ ] **步骤 5:提交**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
git add src/auth.ts tests/auth-config.test.ts
|
||||
git commit -m "feat: sync autumn customer after signup"
|
||||
```
|
||||
|
||||
预期:提交成功。
|
||||
|
||||
## 任务 5:补 Worker 边界回归测试
|
||||
|
||||
**文件:**
|
||||
- 修改:`tests/auth-worker.test.ts`
|
||||
|
||||
- [ ] **步骤 1:写测试,确认公开 HTTP 边界没有扩大**
|
||||
|
||||
在 `tests/auth-worker.test.ts` 的 `describe("cfw-auth worker", () => {` 内、`does not expose a custom session wrapper` 测试后追加:
|
||||
|
||||
```ts
|
||||
it("does not expose a custom Autumn sync endpoint", async () => {
|
||||
const response = await worker.fetch(new Request("http://auth.local/internal/autumn/sync"), env);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:运行 Worker 测试**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
pnpm vitest run tests/auth-worker.test.ts
|
||||
```
|
||||
|
||||
预期:PASS。
|
||||
|
||||
- [ ] **步骤 3:提交**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
git add tests/auth-worker.test.ts
|
||||
git commit -m "test: preserve auth worker boundary"
|
||||
```
|
||||
|
||||
预期:提交成功。
|
||||
|
||||
## 任务 6:全量验证
|
||||
|
||||
**文件:**
|
||||
- 无新增代码,验证全仓库状态。
|
||||
|
||||
- [ ] **步骤 1:运行类型检查**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
预期:PASS。
|
||||
|
||||
- [ ] **步骤 2:运行测试**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
预期:PASS。
|
||||
|
||||
- [ ] **步骤 3:运行 ready gate**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
pnpm ready
|
||||
```
|
||||
|
||||
预期:PASS。该命令应包含 `db:check`、`typecheck` 和 `test`。
|
||||
|
||||
- [ ] **步骤 4:检查工作树**
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
预期:没有未提交文件。
|
||||
|
||||
## 自检映射
|
||||
|
||||
- 自动创建 Autumn customer:任务 1、2、4 覆盖。
|
||||
- 自动分配免费套餐:任务 1、2 通过 `auto_enable_plan_id` 覆盖。
|
||||
- 自动开通试用:由 Autumn plan/product 配置承担,任务 2 保持 `cfw-auth` 不硬编码试用。
|
||||
- 自动同步用户资料:任务 1、2 覆盖 `id/email/name` 映射。
|
||||
- fail-open:任务 1、2 覆盖缺 env、Autumn 500 和不抛出。
|
||||
- 不扩大公开 API:任务 5 覆盖。
|
||||
- 显式 env 和无假默认 plan/secret:任务 2、3 覆盖。
|
||||
- 验收命令:任务 6 覆盖。
|
||||
Reference in New Issue
Block a user