create-dwy 0.25.3 → 0.27.0

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +1 -1
  3. package/src/sync-all.js +46 -2
  4. package/src/sync-plan.js +9 -1
  5. package/templates/ai-tools/CHANGELOG.md +17 -0
  6. package/templates/ai-tools/VERSION +1 -1
  7. package/templates/ai-tools/skills/Apple/dwy-apple-appkit/SKILL.md +17 -0
  8. package/templates/ai-tools/skills/Apple/dwy-apple-architecture/SKILL.md +12 -0
  9. package/templates/ai-tools/skills/Apple/dwy-apple-architecture/references/extraction.md +9 -0
  10. package/templates/ai-tools/{rules/iOS/dwy-apple-layering.md → skills/Apple/dwy-apple-architecture/references/layering.md} +2 -10
  11. package/templates/ai-tools/skills/Apple/dwy-apple-architecture/references/pitfalls.md +3 -0
  12. package/templates/ai-tools/skills/Apple/dwy-ios-development/SKILL.md +14 -0
  13. package/templates/ai-tools/skills/Apple/dwy-ios-development/references/appearance.md +6 -0
  14. package/templates/ai-tools/{rules/iOS/dwy-ios-layout-adaptation.md → skills/Apple/dwy-ios-development/references/layout.md} +0 -6
  15. package/templates/ai-tools/{rules/iOS/dwy-ios-pitfalls.md → skills/Apple/dwy-ios-development/references/pitfalls.md} +1 -9
  16. package/templates/ai-tools/skills/Apple/dwy-ios-development/references/presentations.md +11 -0
  17. package/templates/ai-tools/skills/Apple/dwy-macos-development/SKILL.md +14 -0
  18. package/templates/ai-tools/skills/Apple/dwy-macos-development/references/pitfalls.md +9 -0
  19. package/templates/ai-tools/skills/Apple/dwy-macos-development/references/presentations.md +5 -0
  20. package/templates/ai-tools/skills/Apple/dwy-macos-development/references/ui-adaptation.md +6 -0
  21. package/templates/ai-tools/skills/Apple/dwy-swift-development/SKILL.md +16 -0
  22. package/templates/ai-tools/{rules/iOS/dwy-swift-core.md → skills/Apple/dwy-swift-development/references/conventions.md} +2 -8
  23. package/templates/ai-tools/skills/Apple/dwy-swift-development/references/pitfalls.md +5 -0
  24. package/templates/ai-tools/skills/Apple/dwy-swift-development/references/presentations.md +8 -0
  25. package/templates/ai-tools/skills/Python/dwy-apple-notify/SKILL.md +24 -0
  26. package/templates/ai-tools/skills/Python/dwy-apple-notify/references/v0/0.1.6.md +81 -0
  27. package/templates/ai-tools/skills/Python/dwy-auth/SKILL.md +24 -0
  28. package/templates/ai-tools/skills/Python/dwy-auth/references/v0/0.2.0.md +163 -0
  29. package/templates/ai-tools/skills/Python/dwy-device-fingerprint/SKILL.md +19 -0
  30. package/templates/ai-tools/skills/Python/dwy-device-fingerprint/references/v0/0.1.1.md +90 -0
  31. package/templates/ai-tools/skills/Python/dwy-eapi/SKILL.md +1 -1
  32. package/templates/ai-tools/skills/Python/dwy-eapi/versions.yml +3 -3
  33. package/templates/ai-tools/skills/Python/dwy-email-notify/SKILL.md +19 -0
  34. package/templates/ai-tools/skills/Python/dwy-email-notify/references/v0/0.1.0.md +31 -0
  35. package/templates/ai-tools/skills/Python/dwy-notify/SKILL.md +24 -0
  36. package/templates/ai-tools/skills/Python/dwy-notify/references/v0/0.3.2.md +128 -0
  37. package/templates/ai-tools/skills/Python/dwy-payment/SKILL.md +19 -0
  38. package/templates/ai-tools/skills/Python/dwy-payment/references/v0/0.3.0.md +90 -0
  39. package/templates/ai-tools/skills/Python/dwy-python-api/SKILL.md +8 -0
  40. package/templates/ai-tools/skills/Python/dwy-python-architecture/SKILL.md +8 -0
  41. package/templates/ai-tools/skills/Python/dwy-python-development/SKILL.md +17 -0
  42. package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-development/references}/dwy-python-base-lib.md +3 -3
  43. package/templates/ai-tools/skills/Python/dwy-webhooks/SKILL.md +24 -0
  44. package/templates/ai-tools/skills/Python/dwy-webhooks/references/v0/0.6.2.md +238 -0
  45. package/templates/ai-tools/skills/Python/dwy-wechatpay-notify/SKILL.md +19 -0
  46. package/templates/ai-tools/skills/Python/dwy-wechatpay-notify/references/v0/0.1.1.md +101 -0
  47. package/templates/ai-tools/skills/Vue/dwy-admin-kit/SKILL.md +12 -0
  48. package/templates/ai-tools/skills/Vue/dwy-admin-kit/references/v0/0.3.0.md +176 -0
  49. package/templates/ai-tools/skills/Vue/dwy-auth-web/SKILL.md +10 -0
  50. package/templates/ai-tools/skills/Vue/dwy-ekit/SKILL.md +2 -3
  51. package/templates/ai-tools/skills/Vue/dwy-eui/SKILL.md +1 -1
  52. package/templates/ai-tools/skills/Vue/dwy-eui/versions.yml +2 -2
  53. package/templates/ai-tools/skills/Vue/dwy-vue-architecture/SKILL.md +8 -0
  54. package/templates/ai-tools/skills/Vue/dwy-vue-development/SKILL.md +16 -0
  55. package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-development/references}/dwy-vue-base-lib.md +3 -3
  56. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-architecture/SKILL.md +6 -6
  57. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-commercial-sdk/SKILL.md +20 -0
  58. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-github-action-publish/SKILL.md +1 -1
  59. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/SKILL.md +6 -2
  60. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/android.md +2 -2
  61. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/backend-deploy.md +1 -1
  62. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/frontend-deploy.md +1 -1
  63. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/generic-release.md +2 -2
  64. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/harmony.md +2 -2
  65. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-publish/references/ios.md +2 -2
  66. package/templates/ai-tools/skills//351/200/232/347/224/250/dwy-dependency-choice/SKILL.md +2 -2
  67. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/SKILL.md +0 -244
  68. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/assets/files-whitelist-snippet.json +0 -22
  69. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/assets/js-commercial.npmignore +0 -76
  70. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/assets/python-commercial-MANIFEST.in +0 -37
  71. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/assets/python-commercial.gitignore +0 -62
  72. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/references/js-commercial-guide.md +0 -133
  73. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/references/publish-blacklist.md +0 -106
  74. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/references/python-commercial-guide.md +0 -147
  75. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/references/sensitive-patterns.md +0 -112
  76. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/scripts/check_comments.py +0 -260
  77. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/scripts/check_js_publish.sh +0 -110
  78. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/scripts/check_python_publish.sh +0 -171
  79. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/scripts/detect_sdk.sh +0 -71
  80. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-sdk-spec/scripts/init_config.sh +0 -91
  81. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/SKILL.md +0 -136
  82. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/references/semver-spec.md +0 -70
  83. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-semver/scripts/bump.py +0 -137
  84. /package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-api/references}/dwy-python-backend.md +0 -0
  85. /package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-architecture/references}/dwy-python-layering.md +0 -0
  86. /package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-development/references}/dwy-python-core.md +0 -0
  87. /package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-development/references}/dwy-python-pitfalls.md +0 -0
  88. /package/templates/ai-tools/{rules/Python → skills/Python/dwy-python-development/references}/dwy-python-testing.md +0 -0
  89. /package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-architecture/references}/dwy-vue-layering.md +0 -0
  90. /package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-development/references}/dwy-vue-core.md +0 -0
  91. /package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-development/references}/dwy-vue-pinia.md +0 -0
  92. /package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-development/references}/dwy-vue-pitfalls.md +0 -0
  93. /package/templates/ai-tools/{rules/Vue → skills/Vue/dwy-vue-development/references}/dwy-vue-testing.md +0 -0
@@ -0,0 +1,163 @@
1
+ <!-- 来源:dwy-auth@0.2.0 的 backend/packages/auth/README.md;文中相对链接相对原仓库路径。 -->
2
+
3
+ # dwy-auth
4
+
5
+ New integrations use `AuthRuntime` with application-owned users and a replaceable `SessionBackend`. See [runtime integration](../../../docs/auth-runtime-integration.md). The authentication core is independent of EAPI and FastAPI; the legacy service, HTTP and administrator interfaces below require EAPI when used.
6
+
7
+ Python distribution `dwy-auth==0.2.0`, import `dwy_auth`. Install the independent core from public PyPI with `pip install "dwy-auth==0.2.0"`. For EAPI integration and compatibility interfaces, install `pip install "dwyeapi[email]==0.12.1" "dwy-auth==0.2.0"`; the older `dwyeapi[auth]==0.12.1` aggregate extra pins authentication to 0.1.1. Source is available in the public repository; registry releases use `.github/workflows/publish-auth.yml` with GitHub Actions OIDC. The Google user entry and administrator Cookie entry are separate. No dependency on app-cloud or cloud-kernel; no environment variables or global database are read by this package.
8
+
9
+ Upgrading runtime hosts from 0.1.1 requires a host-owned Alembic revision: add the `auth_runtime_credential_versions` table from `build_runtime_tables`, plus nullable `subject` and `credential_version` columns on `auth_runtime_email_challenges`. Snapshot these definitions in the revision; do not import a future builder inside historical migrations. Existing accounts and sessions remain intact; application admission remains opt-in.
10
+
11
+ When upgrading a preview integration, update both the dependency name and Python imports. The new wheel exports only `dwy_auth`; no legacy namespace alias is shipped. The source package is `backend/packages/auth` and release tags use `dwy-auth@VERSION`.
12
+
13
+ User authentication uses the existing Google subject identity, app-scoped SQL tables, 15-minute opaque access tokens, rotating refresh families, device revocation and transactional account deletion. The application owns its engine/session factory, metadata and Alembic history. No migrations are created or run automatically. Google configuration accepts one explicit Web OAuth audience; a missing configuration closes login.
14
+
15
+ ```python
16
+ from fastapi import FastAPI
17
+ from dwyeapi.exceptions import register_exception_handlers
18
+ from dwy_auth import AuthService, GoogleConfig, OfficialGoogleVerifier, build_auth_tables, install_auth_routes
19
+
20
+ # metadata and sessions are owned by this application.
21
+ service = AuthService("myapp", sessions, build_auth_tables(metadata), OfficialGoogleVerifier(
22
+ GoogleConfig(client_id=google_client_id)
23
+ ))
24
+ app = FastAPI()
25
+ register_exception_handlers(app)
26
+ install_auth_routes(app, service, authorization_limit=8192)
27
+ ```
28
+
29
+ `dwyeapi.identity.Principal` and `IdentityVerifier` are the canonical identity contract. `create_identity_dependency(app_id, verifier, authorization_limit=...)` binds business routes to an explicitly chosen application/verifier. `install_auth_routes` uses its AuthService by default and accepts an explicitly injected trusted `identity_verifier` for existing host adapters. The host must apply request-body limits before parsing (the Google token schema separately limits token length to 16384 characters). Validation errors installed by this package omit submitted credentials.
30
+
31
+ ```python
32
+ from dwy_auth.admin import AdminAuthConfig, install_admin_auth
33
+
34
+ admin = install_admin_auth(app, AdminAuthConfig(
35
+ username=admin_username,
36
+ password=admin_password,
37
+ secure_cookie=True,
38
+ origin="https://admin.example.com",
39
+ ))
40
+ # Protect host-owned business routes with Depends(admin.require_admin).
41
+ # Also add Depends(admin.same_origin) to host-owned mutation routes.
42
+ ```
43
+
44
+ Admin paths remain `/api/admin/auth/login`, `/me`, `/logout`. Cookies remain HttpOnly, SameSite=Strict, scoped to `/api/admin`; production callers explicitly enable Secure and enforce their deployment password policy. A configured origin is matched exactly. Without one, an Origin header must match the request scheme/host; forwarded headers are not trusted. Requests without Origin preserve the existing API contract. Standalone admin installation bounds auth request bodies at 4096 bytes and forbids caching. A host that already protects every admin path can pass `install_security=False` and retain that middleware.
45
+
46
+ Admin state is in memory: restarting revokes every session, with one host worker required for this existing implementation. Sessions, rate limits and credentials are independent of user access/refresh tokens. The binding grants no user Principal and contains no business queries or configuration storage.
47
+
48
+ Run `pytest tests` from this directory with the dev extra installed. `tests/packaging_smoke.py` additionally runs against installed wheels in a clean environment and asserts the host, kernel and business packages are absent. PostgreSQL concurrency cases need an explicitly provided `APP_CLOUD_TEST_POSTGRES_URL`.
49
+
50
+ ## Email/password authentication
51
+
52
+ Email/password is opt-in. Email accounts have stable random UUIDs and an independent unique `(app_id,email)` credential; email normalization only trims and lowercases. A Google account with the same email is never merged. New/reset passwords require at least 15 Unicode code points and at most 72 UTF-8 bytes; login accepts nonempty existing passwords up to 72 bytes. Passwords are never trimmed or truncated. EAPI bcrypt executes in a bounded worker pool (4 concurrent, at most 32 queued/running per service); cancellation keeps running work charged to this budget.
53
+
54
+ ```python
55
+ from dwy_auth import EmailAuthConfig, EapiEmailCodeSender, SessionPolicy
56
+ from dwyeapi.providers.email import make_email_provider
57
+
58
+ # email_settings and challenge_secret come from the host's validated settings.
59
+ # Every worker must share the same secret (at least 32 bytes). Never commit it.
60
+ provider = make_email_provider(email_settings)
61
+ service = AuthService(
62
+ "myapp", sessions, build_auth_tables(metadata), google_verifier,
63
+ email_sender=EapiEmailCodeSender(provider),
64
+ email_config=EmailAuthConfig(challenge_secret=challenge_secret),
65
+ session_policy=SessionPolicy(refresh_ttl=180 * 86400, rolling_refresh=True),
66
+ )
67
+ install_auth_routes(app, service)
68
+ ```
69
+
70
+ Use the matching EAPI version containing `EmailProviderBase.send_verification_code`. This public transport-only method reuses existing Resend templates/domain policy and does not access Redis. Match the provider template TTL with `EmailAuthConfig.code_ttl`. The host owns the email sender/provider lifecycle. Without email configuration all email routes fail closed with `EMAIL_NOT_CONFIGURED`.
71
+
72
+ Both authentication HTTP installers accept an optional `Accept-Language` header (maximum 512 characters) for email codes. Any valid `zh`/`zh-*` range with nonzero quality selects Chinese; missing, malformed, excluded (`zh;q=0`), or other languages select English. The subject, HTML, and plain-text body use the same language. The current Apple client explicitly sends `zh` for a Chinese first preferred language and `en` otherwise. Language is passed per call and never changes code identity or resend limits.
73
+
74
+ Custom senders must accept `send(email, code, purpose, *, language="en")`; custom EAPI providers must accept `_send(target, code, *, language="en")`. Programmatic calls may pass `send_email_code(..., language="zh")`; only `zh` and `en` are supported. Configure `resend.subject` for Chinese and `resend.subject_en` for English. English brand taglines/slogans use `brand_tagline_en`/`brand_slogan_en` (empty by default), without falling back to Chinese promotional copy. Brand names remain as configured.
75
+
76
+ | POST path | JSON input | SUCCESS data |
77
+ |---|---|---|
78
+ | `/api/v1/auth/email/code` | `email,purpose` (`register` or `password_reset`) | `challenge_id,expires_in,resend_after,code_length` |
79
+ | `/api/v1/auth/register` | `email,password,code,challenge_id,device_id` | Existing `TokenResponse`; automatically logged in |
80
+ | `/api/v1/auth/login` | `email,password,device_id` | Existing `TokenResponse` |
81
+ | `/api/v1/auth/password/reset` | `email,password,code,challenge_id` | `{"status":"completed"}`; all existing user sessions revoked |
82
+
83
+ Codes are six digits, expire after 300 seconds and have a 60-second resend cooldown by default. Response `expires_in` and `resend_after` report actual remaining database deadlines after delivery; a delivery that outlasts the challenge fails and removes that challenge. Only a successful delivery activates a challenge; a new delivered challenge invalidates older challenges for the same app/email/purpose. The database stores an HMAC bound to app/email/purpose/challenge, never the code. Conditional updates make consumption single-use; failed-attempt counters commit independently so business rollback cannot replenish attempts. Each challenge allows at most five attempts. Atomic SQL budgets apply per app, peer IP, purpose/email and action; send limits are 50/IP/hour, 1000/app/hour, 10/email/purpose/hour; verification is 100/IP/15min and 20/email/purpose/15min; login is 100/IP/15min and 10/email/15min. Expired counters/challenges are pruned on use. Configure trusted proxy middleware at the host if deployed behind a proxy; request forwarding headers are not read directly. Continue enforcing host request-body and ingress limits.
84
+
85
+ Errors never echo submitted secrets. Public codes are `EMAIL_INVALID`, `PASSWORD_INVALID`, `EMAIL_PURPOSE_INVALID`, `EMAIL_CODE_INVALID` (wrong/expired/used/exhausted), `EMAIL_ALREADY_REGISTERED`, `CREDENTIALS_INVALID`, `EMAIL_RATE_LIMITED`, `EMAIL_NOT_CONFIGURED`, `EMAIL_DELIVERY_FAILED`. Schema validation uses the existing `VALIDATION_ERROR` envelope. Failed delivery remains charged to resend budgets to prevent abusive retries. Delivery to existing and unknown accounts follows the same public send contract.
86
+
87
+ The default `SessionPolicy()` retains the existing fixed 30-day refresh deadline and 15-minute access lifetime. Explicit rolling policy extends refresh validity to 180 days on successful refresh. Old refresh digests remain replay evidence for at most `replay_retention` (default 30 days) and expire independently, so normal 15-minute refreshes do not accumulate a lifetime 4096 cap. The 4096 limit still bounds *unexpired* evidence (normal 30-day use is 2880); an abusive burst gets 429 without invalidating the current token. Refresh replay, device revocation and account deletion remain enforced. Account deletion removes email credentials/challenges transactionally; it preserves the anonymized UUID for independent purchase ledgers.
88
+
89
+ ### Host Alembic migration
90
+
91
+ Fresh applications include all six auth tables via `build_auth_tables(metadata)` in their own migration. Existing Google-only databases must migrate before enabling the new package; `metadata.create_all` does not alter the old non-nullable subject column. Add a revision to the **host** Alembic history:
92
+
93
+ ```python
94
+ from alembic import op
95
+ from dwy_auth.migrations import upgrade_email_auth
96
+
97
+ # revision/down_revision belong to the host migration history.
98
+ def upgrade():
99
+ upgrade_email_auth(op)
100
+ ```
101
+
102
+ The helper makes `auth_users.google_subject` nullable and creates `auth_password_credentials`, `auth_email_challenges`, `auth_rate_limits`; existing UUIDs, sessions and Google subjects remain untouched. PostgreSQL alters the column in place. SQLite uses Alembic batch mode; hosts with foreign keys enabled must follow Alembic's SQLite foreign-key PRAGMA procedure outside the transaction. Downgrade is intentionally host-owned: email-only users have null Google subjects, so restoring NOT NULL requires an explicit data-retention decision. Do not run this helper at application startup. `tests/test_email_auth.py` migrates a populated legacy Google schema and verifies both existing sessions and new email registration afterward.
103
+
104
+ ## Apple 原生登录(dwy-auth 0.1.1 / dwyeapi 0.12.1)
105
+
106
+ 已通过 GitHub Actions OIDC 发布到公开 PyPI;独立配置与迁移步骤见 [Apple 后端接入](../../../docs/apple-auth-integration.md)。
107
+
108
+ 使用 `dwy_auth.apple.AppleSignInConfig`、`AppleGateway`、`dwy_auth.apple_auth.AppleAuthService` 和 `dwy_auth.apple_storage.build_apple_tables(metadata)`。多步骤 Apple 授权使用独立挑战/登录接口;不能把 Apple ID token 塞进通用 external credential 接口绕过 nonce 和授权码验证。
109
+
110
+ 宿主提供自己的 AuthRuntime、业务主体、会话撤销回调及 Alembic 迁移,并在现有 `install_runtime_auth` 后安装 `dwyeapi.auth_apple.install_apple_auth`。把 service 注册为 runtime 的 apple provider,驱动 `process_revocations()`,在业务删号事务内调用 `prepare_delete(db, subject)`;其返回值为撤销任务 ID,可用于区分本地删除完成与上游撤销 pending。签名 Key 与 Fernet token_encryption_key 只保留服务端,不能复用内购配置或按邮箱合并账号。
111
+
112
+ 原生 Swift 接入使用共享 AuthCore 与 AuthAppleIOS,说明见仓库根 `ios/Docs/APPLE_AUTH.md`。每个应用独立配置 client_id、密钥、数据库与业务 UUID,代码共享不代表跨产品 SSO。
113
+
114
+ ## 已登录账号的邮箱与登录方式管理
115
+
116
+ `AuthRuntime.send_email_update_code(principal, email, *, reauthentication, remote_key="local", language="en")`
117
+ 消费 `purpose="email_update"` 的 `VerifiedIdentity`,将验证码绑定当前 app、subject、规范化目标邮箱和凭据代次。
118
+ `AuthRuntime.update_email(principal, email, code, challenge_id, *, new_password=None, remote_key="local")`
119
+ 在宿主事务中消费挑战、更新邮箱凭据与已验证主邮箱、撤销所有旧会话并签发同一 subject 的新会话。
120
+ 首次添加密码必须提供符合共享密码政策的新密码;已有密码可保留。Google issuer/sub 不变,邮箱不用于自动合并。
121
+ 公开 `send_email_code` 不接受 `email_update`,因此注册/重置验证码不能绕过重新认证。
122
+
123
+ 启用变更的宿主适配器实现 `EmailSubjectAdapter.update_email(db, subject, email)`,与挑战/凭据/会话共同提交,不能自行提交或更换 subject。
124
+ 未实现此接口的宿主在发码前返回 `EMAIL_UPDATE_NOT_CONFIGURED`,既有注册/登录不受影响。
125
+ 主邮箱经用户验证后,宿主不得用 Google profile 邮箱覆盖它。
126
+
127
+ EAPI `install_runtime_auth` 额外提供:
128
+
129
+ - POST `/api/v1/auth/email/update/code`:`email` 与 `reauthentication`,需要当前 Bearer。
130
+ - POST `/api/v1/auth/email/update`:`email/code/challenge_id/new_password?`,需要当前 Bearer,返回既有 token/user 信封。
131
+ - GET `/api/v1/auth/identities`:当前主体可用入口的 `LoginIdentity` 数组,仅含 `kind/value/provider/issuer`;密码、版本摘要与 token 不进入响应。
132
+
133
+ 重新认证 `method` 仍为 `email/username/external`;Google 使用 `method=external, identifier=google, credential=<ID token>`。
134
+ 已有外部绑定/解绑接口继续执行主体证明、唯一归属与最后一种可用登录方式保护。
135
+
136
+ 宿主 Alembic revision 在启用前升级 `build_runtime_tables` 使用的实际前缀:
137
+
138
+ - 已有 `email_challenges` 增加可空 `subject VARCHAR(255)`、`credential_version VARCHAR(64)`。
139
+ - 新建 `credential_versions`,`app_id VARCHAR(32)` 与 `subject VARCHAR(255)` 为联合主键,`generation VARCHAR(36) NOT NULL`。
140
+
141
+ 旧挑战两列保持 NULL,不补认旧验证码。代次由首次凭据变更创建;改邮箱、改密、重置、绑定和解绑都会同事务轮换代次并核销未完成挑战。
142
+ 凭据内容恢复成旧值也不会恢复旧挑战,发信中的旧快照同样失效。共享包不在启动时执行迁移。
143
+
144
+
145
+ ## 可选应用准入
146
+
147
+ - `AuthRuntime(..., admission=...)` 接受 `AuthAdmission`:`prepare(proof)` 在数据库事务外验证本次请求证明,`admit(db, subject, prepared)` 在主体、凭据、会话共用事务内验证或写入业务归属。准入不得自行提交事务。仅配置准入时,在 `prepare` 外网工作前复用独立 `admission-ip` 预算(每个可信来源 900 秒最多 100 次);refresh 使用与登录相同的来源解析。默认不配置,不强制业务资格。
148
+ - 对外登录调用 `runtime.login(method, ..., admission_proof=...)`;注册调用 `runtime.register_email_session(...)`。配置准入后,独立 `register_email`、登录用途 `authenticate`、`issue` 拒绝绕过;内部 `_db` 参数只供受信运行时事务组合,不能暴露给请求。
149
+ - `refresh(..., admission_proof=...)` 必须提供本次证明。会话后端需实现可选 `before_lock(db, principal)` 纯主体锁检查(CAS 前,保持 account→session 锁序)和 `before_rotate(db, principal)` 准入写入回调,并在原有刷新事务内、CAS 与配额检查成功后且生成新 token 前调用,准入失败必须回滚尚未生效的轮换。未配置准入仍兼容原会话实现;自定义后端启用准入前必须实现这个合同。
150
+ - EAPI 认证请求总预算为 163840 字节;`admission_proof` 最多 131072 个 UTF-8 字节,日志和模型表示不包含原文。邮箱发码、忘记密码不签发会话,不要求准入证明;变更邮箱会签发新会话,因此同样要求证明。
151
+
152
+ Apple 专用 `AppleAuthService.login(..., admission_proof=...)` 使用同一准入配置:先验证证明,再消费 Apple 挑战与授权码;准入归属、账号、授权与会话共同提交,拒绝时回滚并持久化上游撤销任务。
153
+
154
+ ### Apple-only 账号的管理重新认证
155
+
156
+ 先用当前 Bearer 向 `/api/v1/auth/apple/challenges` 提交 `purpose=reauth_email_update`、`reauth_bind` 或 `reauth_unlink`;服务端将挑战绑定当前 subject 与具体操作。原生授权使用服务端原始 nonce。
157
+ 管理请求的 `reauthentication` 使用 `method=external, identifier=apple`,其 `credential` 为包含 `challenge_id`、`identity_token`、`authorization_code` 的 JSON 字符串(最多 32768 个 UTF-8 字节)。EAPI 通过 `AppleAuthService.reauthenticate` 兑换一次性 code 并生成当前主体、限用途、单次消费的 proof;跨主体、跨操作或重放均拒绝。通用 Apple 登录仍拒绝单凭据路径,不能恢复旧 `AppleIdentityProvider` 或客户端自建 nonce 方案。
158
+
159
+ ## JWT 会话滑动有效期
160
+
161
+ `JWTSessionBackend(..., rolling_refresh=True)` 在每次刷新成功时,把会话期限延到当时起 `refresh_ttl` 秒(默认 30 天);第 25 天刷新后到期日为第 55 天。默认 `False` 保持签发起固定期限,不影响已有调用方。普通业务请求不会续期;已经过期或撤销的会话不能恢复。客户端继续使用现有刷新接口和返回的轮换凭据,无需修改协议或数据库结构。
162
+
163
+ 滑动模式下,每个已消费 refresh 摘要从消费时起再保留 `refresh_ttl` 秒用于重放撤销,刷新成功时只清理同应用、同 family 的到期摘要。4096 条上限约束有效证据窗口,不再累计终身刷新次数;短时间超额仍返回 `REFRESH_QUOTA_EXCEEDED` 并撤销当前 family,需重新登录。CAS 轮换、期限更新与准入写入共同提交,失败回滚不会消费旧凭据或延长期限。
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: dwy-device-fingerprint
3
+ description: 使用或排查 dwy-device-fingerprint 设备指纹能力时使用;先核实项目安装版本及公开 API。
4
+ ---
5
+
6
+ # dwy-device-fingerprint
7
+
8
+ 设备信息采集和状态;检查目标平台支持范围、权限和失败结果,不假定采集值始终存在。
9
+
10
+ ## 查找与使用
11
+
12
+ 项目使用 0.1.1 时可阅读 [0.1.1 接入和示例](references/v0/0.1.1.md);其他版本先查该版本的资料和代码,不把此快照当作其他版本的合同。
13
+
14
+ 1. 检查项目锁文件和当前 Python 环境;用 `importlib.metadata.version("dwy-device-fingerprint")` 查询实际发行版本,依赖范围不等于实际安装版本。未安装时不得假设可用或自动安装。
15
+ 2. 在实际环境中检查 `dwy_device` 的公开接口、类型及项目已有调用;editable 安装以实际源码为准。说明、实现或安装版本不一致时以运行代码为准。
16
+ 3. 本索引基于仓库 0.1.1,能力与签名可能随小版本变化。精确版本源码位于仓库 `backend/packages/device-fingerprint`,文档为该目录的 `README.md`;只在确认其版本与消费项目一致时使用。缺少安装源码时查对应 `dwy-device-fingerprint@版本` 标签,不能用默认分支冒充。
17
+ 4. 在已有调用和该版本文档中核对配置、必需依赖、输入校验、失败处理及生命周期后接入。没有足够证据时说明缺口,不猜 API。
18
+
19
+ 不要因为发现此库就改动项目依赖;新增依赖遵循项目选型规则。关联 FastAPI 或其他自有包时同时参考可用的对应 Skill。
@@ -0,0 +1,90 @@
1
+ <!-- 来源:dwy-device-fingerprint@0.1.1 的 backend/packages/device-fingerprint/README.md;文中相对链接相对原仓库路径。 -->
2
+
3
+ # dwy-device-fingerprint
4
+
5
+ eapi 体系下独立的跨平台设备指纹核心,Python 3.9+,零运行时依赖。只采集并返回结果,不联网、不记录设备日志、不执行限制策略。
6
+
7
+ ## 安装与使用
8
+
9
+ 源码仓库根目录安装:
10
+
11
+ ```bash
12
+ uv pip install ./backend/packages/device-fingerprint
13
+ ```
14
+
15
+ 安装公开 PyPI 已发布版本:
16
+
17
+ ```bash
18
+ python -m pip install dwy-device-fingerprint
19
+ ```
20
+
21
+ 完整示例只打印采集状态,不把设备指纹写入日志:
22
+
23
+ ```python
24
+ from dwy_device import collect_device
25
+
26
+
27
+ def device_headers() -> dict[str, str]:
28
+ """客户端初始化时采集一次,后续请求复用结果。"""
29
+ info = collect_device("example.app", command_timeout=2.0)
30
+ if info.fingerprint is None:
31
+ return {}
32
+ return {"X-Device-Id": info.fingerprint}
33
+
34
+
35
+ if __name__ == "__main__":
36
+ headers = device_headers()
37
+ print("available" if headers else "unavailable")
38
+ ```
39
+
40
+ `namespace` 必须为 1 至 128 个小写 ASCII 字母、数字、点、下划线或连字符,产品与其 SDK/桌面端使用同一固定值。`command_timeout` 为有限正数,最大 30 秒;非法调用参数抛 ValueError。
41
+
42
+ ### 返回结果
43
+
44
+ `DeviceInfo` 与 `SystemInfo` 是本库不可变 dataclass,状态、来源和错误使用本库 str Enum。
45
+
46
+ | 字段 | 内容 |
47
+ | --- | --- |
48
+ | fingerprint | 可用时 64 位小写十六进制,否则 None |
49
+ | scheme_version | 1 |
50
+ | source | Windows MachineGuid、macOS IOPlatformUUID 或 Linux machine-id 的明确来源 |
51
+ | status | available、unavailable、unsupported |
52
+ | error_code | 无错误为 None;否则为缺失、权限、超时、无效值、命令失败或不支持平台 |
53
+ | system | system_name、system_release、system_version、architecture |
54
+
55
+ 系统信息来自 platform 标准库,Darwin/Linux 的 release 为内核发行信息,不代表产品营销版本。原始机器身份不包含在结果中。
56
+
57
+ ### 稳定性
58
+
59
+ - Windows 使用 64 位注册表 MachineGuid;macOS 读取 ioreg 的 IOPlatformUUID;Linux 优先 /etc/machine-id,仅缺失时读取 /var/lib/dbus/machine-id。
60
+ - 规范化机器身份作为 HMAC-SHA256 key,`dwy.device.v1:` 加 namespace 作为 message。不同产品域隔离,同产品域跨账号配置、进程、项目和 venv 保持一致。
61
+ - OS 元数据和 SDK 版本不参与派生。原始系统身份、namespace 或方案版本改变会影响结果。
62
+ - 无可靠来源时返回结构化失败,不降级到随机值、MAC、hostname 或安装文件;不自动重试、不缓存到磁盘。需要缓存时由客户端实例保存结果。
63
+ - 容器、虚拟机、WSL 只识别当前系统环境,不能保证物理宿主唯一。克隆镜像、重装系统或修改客户端可以绕过,指纹不用于身份认证。
64
+ - 本接口包含同步系统 I/O;异步应用在初始化或工作线程调用,不在每个请求中运行。
65
+
66
+ ## eapi 可选门面
67
+
68
+ Python 3.11+ 且已安装匹配核心的 eapi 应用可以从 `dwyeapi.device` 导入同一入口和类型。eapi 根导入不强制加载设备模块。
69
+
70
+ EAPI 的公开版本提供 `device` extra:
71
+
72
+ ```bash
73
+ python -m pip install 'dwyeapi[device]'
74
+ ```
75
+
76
+ 源码仓库保持私有,安装包通过 `.github/workflows/publish-device-fingerprint.yml` 公开发布。SDK 可以依赖固定版本的公开包,不需要源码仓库权限,也无需维护第二份实现。
77
+
78
+ ## 测试与构建
79
+
80
+ 在仓库根目录运行:
81
+
82
+ ```bash
83
+ uv run --directory backend/packages/device-fingerprint --extra dev --frozen pytest
84
+ uv run --directory backend/packages/device-fingerprint --extra dev --frozen ruff check src tests
85
+ uv build --project backend/packages/device-fingerprint --default-index https://pypi.org/simple
86
+ ```
87
+
88
+ macOS/Windows/Linux 原生读取测试需要设置 `DWY_DEVICE_NATIVE_TEST=1`;常规测试使用受控系统边界模拟,不打印真实原始身份。
89
+
90
+ 运行时仅使用标准库。开源参考和许可见 THIRD_PARTY_NOTICES.md 与 LICENSE。
@@ -16,7 +16,7 @@ FastAPI 基础设施包。本文件只做**路由**:探测安装版本 → 选
16
16
  python -c "import dwyeapi; print(dwyeapi.__version__)"
17
17
  ```
18
18
  失败则读项目 `pyproject.toml` / `uv.lock` 里 `dwyeapi` 约束。
19
- 仍失败:按 `v0` 给脚手架,并声明「未探测到安装版本」。
19
+ 声明的依赖范围不代表实际安装版本。仍无法确定时说明缺口,不按任一版本猜测 API。
20
20
 
21
21
  2. **选线**:读同目录 `versions.yml`。`0.y.z` → `v0/`,`1.y.z` → `v1/`,`2.y.z` → `v2/`。目录不存在(例如装了 1.x 但还没有 `v1/`)→ 不要用 `v0` 冒充;按下面「读源码」拿该版本代码,并说明文档线未建。
22
22
 
@@ -5,9 +5,9 @@ tag_prefix: "dwyeapi@"
5
5
  sparse:
6
6
  - backend/src/dwyeapi
7
7
  lines:
8
- - match: ">=2.0.0"
8
+ - match: ">=2.0.0 <3.0.0"
9
9
  dir: v2
10
- - match: ">=1.0.0"
10
+ - match: ">=1.0.0 <2.0.0"
11
11
  dir: v1
12
- - match: ">=0.0.0"
12
+ - match: ">=0.0.0 <1.0.0"
13
13
  dir: v0
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: dwy-email-notify
3
+ description: 使用或排查 dwy-email-notify 邮件事件通知能力时使用;先核实项目安装版本及公开 API。
4
+ ---
5
+
6
+ # dwy-email-notify
7
+
8
+ 邮件平台事件和通知,核实供应商配置、Webhook 验证与宿主处理。
9
+
10
+ ## 查找与使用
11
+
12
+ 项目使用 0.1.0 时可阅读 [0.1.0 接入和示例](references/v0/0.1.0.md);其他版本先查该版本的资料和代码,不把此快照当作其他版本的合同。
13
+
14
+ 1. 检查项目锁文件和当前 Python 环境;用 `importlib.metadata.version("dwy-email-notify")` 查询实际发行版本,依赖范围不等于实际安装版本。未安装时不得假设可用或自动安装。
15
+ 2. 在实际环境中检查 `dwy_email_notify` 的公开接口、类型及项目已有调用;editable 安装以实际源码为准。说明、实现或安装版本不一致时以运行代码为准。
16
+ 3. 本索引基于仓库 0.1.0,能力与签名可能随小版本变化。精确版本源码位于仓库 `backend/packages/email-notify`,文档为该目录的 `README.md`;只在确认其版本与消费项目一致时使用。缺少安装源码时查对应 `dwy-email-notify@版本` 标签,不能用默认分支冒充。
17
+ 4. 在已有调用和该版本文档中核对配置、必需依赖、输入校验、失败处理及生命周期后接入。没有足够证据时说明缺口,不猜 API。
18
+
19
+ 不要因为发现此库就改动项目依赖;新增依赖遵循项目选型规则。关联 FastAPI 或其他自有包时同时参考可用的对应 Skill。
@@ -0,0 +1,31 @@
1
+ <!-- 来源:dwy-email-notify@0.1.0 的 backend/packages/email-notify/README.md;文中相对链接相对原仓库路径。 -->
2
+
3
+ # dwy-email-notify
4
+
5
+ 邮件平台官方 Webhook 事件接入与通知转发。当前提供 Resend,未来平台各自添加 provider 和 extra;本包不提供业务发信、验证码或邮件正文读取功能。
6
+
7
+ ```bash
8
+ pip install 'dwy-email-notify[resend]==0.1.0'
9
+ ```
10
+
11
+ ```python
12
+ from fastapi import FastAPI
13
+ from dwy_notify import NotificationConfig
14
+ from dwy_email_notify import EmailNotifyConfig, ResendWebhookConfig, install_email_notifications
15
+
16
+ def configure(app: FastAPI, signing_secret: str, notification_url: str) -> None:
17
+ install_email_notifications(app, EmailNotifyConfig(
18
+ notifications=NotificationConfig(targets={"operations": notification_url}),
19
+ resend=ResendWebhookConfig(secret=signing_secret),
20
+ ))
21
+ ```
22
+
23
+ 配置由宿主从安全配置中显式注入,本包不读取环境或 dotenv。Resend 只需要独立的 Webhook signing secret,无需发信 API key。默认接收路径为 `/webhooks/resend`,可由配置覆盖;只有配置来源后才加载对应 SDK。
24
+
25
+ 事件经过原始载荷大小限制、官方 SDK 验签及事件筛选后才交付。仅转发事件类型、邮件编号和通知编号,不读取或转发主题、正文、收件人。默认转发使用“邮件通知”名称及固定 GitHub PNG 头像,应用可以通过 `NotificationConfig.appearance` 覆盖名称或显式清除头像。最终发送者名称必填;本包提供默认名称。
26
+
27
+ 默认内存去重只覆盖当前进程,只有成功发送才登记。需要重启后仍可靠接收的宿主,应通过 `sink` 注入自己的持久化接收函数;返回 `True` 表示新事件被接受,`False` 表示已接受的重复事件,失败必须抛出异常,不能提前向 Resend 确认成功。
28
+
29
+ 安装器返回 `EmailNotifyRuntime`,只持有去重状态,不接管宿主数据库、异常处理器或 lifespan。同一 FastAPI 应用只安装一次。`providers.resend.webhooks.create_resend_router` 可用于自行组合路由。
30
+
31
+ 通知头像保持已有固定公开资产引用,不打包图片或拉取浮动分支:`https://raw.githubusercontent.com/danweiyuancircle/dwy-ai-experience/091ad4fea18ed3840fec83dc833f18b759256ca7/backend/packages/webhooks/assets/email.png`。
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: dwy-notify
3
+ description: 使用或排查 dwy-notify 多目标通知能力时使用;先核实项目安装版本及公开 API。
4
+ ---
5
+
6
+ # dwy-notify
7
+
8
+ 多目标通知配置、字段与发送结果;核实目标配置、失败与实际发送行为。
9
+
10
+ ## 查找与使用
11
+
12
+ 项目使用 0.3.2 时可阅读 [0.3.2 接入和示例](references/v0/0.3.2.md);其他版本先查该版本的资料和代码,不把此快照当作其他版本的合同。
13
+
14
+ 1. 检查项目锁文件和当前 Python 环境;用 `importlib.metadata.version("dwy-notify")` 查询实际发行版本,依赖范围不等于实际安装版本。未安装时不得假设可用或自动安装。
15
+ 2. 在实际环境中检查 `dwy_notify` 的公开接口、类型及项目已有调用;editable 安装以实际源码为准。说明、实现或安装版本不一致时以运行代码为准。
16
+ 3. 本索引基于仓库 0.3.2,能力与签名可能随小版本变化。精确版本源码位于仓库 `backend/packages/notify`,文档为该目录的 `README.md`;只在确认其版本与消费项目一致时使用。缺少安装源码时查对应 `dwy-notify@版本` 标签,不能用默认分支冒充。
17
+ 4. 在已有调用和该版本文档中核对配置、必需依赖、输入校验、失败处理及生命周期后接入。没有足够证据时说明缺口,不猜 API。
18
+
19
+ ## 0.x 已核实差异
20
+
21
+ - 0.2.0 起发送者名称必须有明确来源;0.2.1 提供来源无关事件合同与可选持久队列。
22
+ - 0.3.0 增加与宿主事务共用的入队和指定失败目标重放;0.3.1 增加通知字段渲染;0.3.2 调整 dwyeapi 兼容范围。较早版本不要假设这些能力存在。
23
+
24
+ 不要因为发现此库就改动项目依赖;新增依赖遵循项目选型规则。关联 FastAPI 或其他自有包时同时参考可用的对应 Skill。
@@ -0,0 +1,128 @@
1
+ <!-- 来源:dwy-notify@0.3.2 的 backend/packages/notify/README.md;文中相对链接相对原仓库路径。 -->
2
+
3
+ # dwy-notify
4
+
5
+ 独立多目标异步通知包,基于 `apprise==2.1.0`,公共配置和结果类型由本包拥有。依赖 `dwyeapi==0.12.1` 基础规范,不要求运行 Webhook 服务。
6
+
7
+ 来源无关的通知集成入口:
8
+
9
+ - `dwy_notify.events.NotificationEvent`、`EventSink`:已验证事件的接收合同,不包含原始签名或邮件正文。
10
+ - `dwy_notify.relay.NotificationRelay`:单次转发和有界成功去重;调用方通过 `appearance` 注入模块名称与可选头像,不按来源猜测业务预设。
11
+ - `dwy_notify.delivery`:`DeliveryPolicy`、`NotificationDeliveryError`、`deliver_notifications`、`delivery_failure`,复用逐目标检查点和安全退避;不创建数据库。
12
+ - `dwy_notify.sqlalchemy`:通过 `dwy-notify[sqlalchemy]` 安装的 `build_delivery_table`、`SQLAlchemyDeliveryQueue`;会话、表、生命周期、迁移均归宿主,队列显式接收 `appearance`。
13
+ - `dwy_notify.integration`:`NotificationSettings`、`configured_notifier` 等目标选择与历史 Discord 配置适配,保留旧 URL 指纹;新集成优先使用 `NotificationConfig`。
14
+
15
+ 基础发送器不依赖 Apple、邮件或 Webhook 业务包。新模块使用自己的展示预设;历史兼容字段的旧名称仅为保证已有队列目标指纹稳定,不作为新业务的默认名称。
16
+
17
+ 实际 wheel 的发布验收在仓库外执行 `tests/delivery_wheel_smoke.py EXPECTED_VERSION`;需安装 `sqlalchemy` extra 与测试 SQLite 驱动。它核对安装版本、基础导入不加载 ORM/FastAPI/旧业务包,以及队列持久化显式预设和重启去重;不向外部服务发消息。
18
+
19
+ ```bash
20
+ pip install dwy-notify==0.3.0
21
+ ```
22
+
23
+ ```python
24
+ import asyncio
25
+ import os
26
+
27
+ from dwy_notify import NotificationAppearance, NotificationConfig, Notifier
28
+
29
+
30
+ async def main() -> None:
31
+ notifier = Notifier(NotificationConfig(
32
+ targets={
33
+ "discord": os.environ["DISCORD_NOTIFY_URL"],
34
+ "wecom": os.environ["WECOM_NOTIFY_URL"],
35
+ },
36
+ appearance=NotificationAppearance(username="业务通知"),
37
+ ))
38
+ result = await notifier.send("任务完成", title="每日汇总")
39
+ for delivery in result.deliveries:
40
+ if not delivery.success:
41
+ print(delivery.target, delivery.error_code, delivery.retry_after, delivery.permanent)
42
+
43
+
44
+ asyncio.run(main())
45
+ ```
46
+
47
+ `DISCORD_NOTIFY_URL` 来自宿主环境,可用 `discord://WEBHOOK_ID/WEBHOOK_TOKEN` 或 `https://discord.com/api/webhooks/WEBHOOK_ID/WEBHOOK_TOKEN`。`WECOM_NOTIFY_URL` 使用 `wecombot://WEBHOOK_KEY`。凭据应来自环境变量或秘密存储,不写入版本库。配置使用 `SecretStr` 隐藏 URL;调用方不要解密后记录。
48
+
49
+ `NotificationConfig(targets=..., timeout_seconds=10)` 为 frozen Pydantic 模型,目标映射非空,超时必须为有限正数且不超过 30 秒,不接受布尔值。`Notifier` 在构造时固定目标快照,不读取环境变量。`targets` 返回目标名元组,`target_fingerprint(name)` 返回 URL 的 SHA-256 摘要,供调用方检测配置变更;摘要不包含明文 URL。
50
+
51
+ `await notifier.send(body, title="", targets=None)` 默认发送全部目标,返回 `NotificationResult`。`success` 仅在至少一个目标且全部成功时为真,`succeeded` / `failed` 是目标名元组,详情位于 `deliveries`。每条 `NotificationDelivery` 包含 `target`、`success`、`message_id`、`error_code`、`retry_after`、`permanent`。正文必须为非空白字符串,标题必须为字符串,显式 targets 必须非空且不重复;无效输入或未知目标在发请求前抛出固定 `ValueError`。调用方应遵守 `retry_after` 并排除永久失败,再用失败目标列表重试。
52
+
53
+ 安全边界:
54
+
55
+ - HTTP 渠道强制 TLS 校验、禁重定向、禁 SDK retry 和 optional 成功;覆盖 URL 中相反选项。普通渠道沿用 Apprise 的 HTTPS 协议和安全参数,不使用全局 monkeypatch。
56
+ - Discord 复用上游解析、消息构造和分段,仅适配单次 HTTP 发送:`allowed_mentions={"parse": []}`、始终设置 flags 的 `4` 位,并用 `wait=true` 确认真实消息 ID;响应 ID 必须为 1 至 128 位 ASCII 数字。需要分段时使用 Apprise 的 `?overflow=split`,分段遇失败停止后续请求。企业微信仅在 JSON `errcode` 为整数 `0` 时成功。
57
+ - Discord / 企业微信返回 HTTP 状态对应的安全错误码,429 的有效秒数放入 `retry_after`,408 与 5xx 可重试。普通渠道以 SDK 返回值判断成功,失败统一 `send_failed`,不暴露原始响应或异常。插件日志通过本包子类隔离;urllib3 原始 HTTP 日志仅在通知工作线程的发送区间被过滤,宿主其它线程、普通 HTTP 调用与应用日志保留。
58
+ - 支持 Apprise 基于 `requests` 的 HTTPS 渠道,例如 Discord、企业微信、钉钉、飞书和 `jsons` 自定义 HTTPS;不支持 SMTP、本地命令等非 HTTP 协议或明文协议,也拒绝固定版本中自带内部重试的 Fluxer、Revolt、Matrix、SparkPost、SendPulse,构造时返回 `unsupported_transport`。可选渠道的额外依赖仍需由宿主安装。
59
+ - 同一目标串行,不同目标可并发。取消等待会传播 `CancelledError`;已开始的同步请求在线程中继续,由连接/读取超时结束,同目标锁直到请求结束才释放。超时是 socket 超时,不是整条通知的硬截止时间;上游分段、多接收人或节流可能增加总耗时。
60
+ - `Notifier` 不提供持久化、调度或自动重试。分段通知中已经成功的部分可能在调用方整体重试时重复;HTTP 接收后丢失响应也无法保证恰好一次。
61
+
62
+ 仅 Discord、企业微信、钉钉和飞书通过模拟 HTTP 验证;未进行真实渠道投递验收,也未逐一验收全部 Apprise 插件。发布必须通过 GitHub Actions OIDC,不使用长期发布 token。
63
+
64
+ ## 发送者名称与可选头像
65
+
66
+ `NotificationAppearance(username=..., avatar_url=...)` 只描述平台基础展示,不包含购买、审核等业务预设。每个目标最终必须具有发送者名称;所有配置层都未提供名称时,`send` 在任何目标发出网络请求前抛出 `ValueError("missing_username")`。正文 `title` 不能代替发送者名称。头像可省略,沿用平台默认头像,不注入 Apprise Logo。
67
+
68
+ 应用在 `NotificationConfig.appearance` 设置默认展示;单次 `send(..., appearance=...)` 可覆盖;具体业务库通过 `send(..., default_appearance=...)` 提供低优先级预设。按字段选择:单次设置 > 应用配置 > 目标 URL 的显式名称/头像 > 业务预设。未设置字段继承;名称不能显式设为 `None` 或空白,头像可通过 `avatar_url=None` 清除预设、恢复平台默认。仅覆盖头像时,名称继续从应用或具体模块继承。
69
+
70
+ 名称限制为 1–80 字符且不包含控制字符;图片 URL 限长 2048,要求 HTTPS 且不携带账号密码。图片须由平台匿名访问,不接受本地路径,不在应用发送进程中下载图片。当前 Discord 适配使用这些字段,其它渠道保留自身展示行为。展示不参与目标 URL 指纹,不改变接收方去重或发送重试。
71
+
72
+ 兼容层若把隐式默认名称编码进旧 URL,构造 `Notifier(..., url_appearance_is_default=True)` 可让这些展示字段退到业务预设之后,同时保留原目标 URL 与指纹;真实用户显式设置的 URL 保持默认优先级。
73
+
74
+ ## 追加公开展示字段
75
+
76
+ `NotificationField` 与 `render_notification_fields` 从 `dwy_notify` 根导出,用于给正文追加少量业务标签。宿主先从用户、订单等对象中筛选公开字符串,再构造字段;不要传完整对象、凭据或个人敏感信息。它们不读取数据库或网络,不修改官方回调数据,也不改变 `send` 和队列接口。
77
+
78
+ ```python
79
+ from dwy_notify import NotificationField, render_notification_fields
80
+
81
+ body = "支付成功"
82
+ fields = (
83
+ NotificationField(label="来源", value="网站"),
84
+ NotificationField(label="套餐", value="专业版"),
85
+ )
86
+ # 2048 是宿主选定的正文总预算,额外预留一个拼接换行的 UTF-8 字节。
87
+ extra = render_notification_fields(fields, max_bytes=2048 - len(body.encode("utf-8")) - 1)
88
+ content = f"{body}\n{extra}" if extra else body
89
+ ```
90
+
91
+ 模型冻结、拒绝额外属性,`label` / `value` 必须为严格字符串,分别为 1–32 / 1–256 个字符,不能全为空白,也不能包含控制字符、格式控制字符(含双向文本控制和零宽连接符)、孤立代理项或 Unicode 行分隔符。校验错误的文本不回显输入;调用方记录 Pydantic 结构化错误时仍须移除 `input`。
92
+
93
+ 渲染接收最多八个 `NotificationField` 的元组,保留顺序,每行采用「标签:值」。`max_bytes` 必须是整数(不接受布尔),表示本段剩余 UTF-8 字节预算;正文、标题、分隔符由宿主另外预留。值截短后加 `…`,其余字段放不下时加 `[已省略]`;标签不会截短,提示也放不下时返回空串,零或负预算同样返回空串。字段数量、类型或预算类型无效时抛出固定原因码 `ValueError`,不会回显内容。
94
+
95
+ 输出沿用当前传输:企业微信发送 `text.content`,不添加 `mentioned_list` / `mentioned_mobile_list`,Markdown 和 `@all` 为普通文本;Discord 的 `allowed_mentions={"parse": []}` 继续禁用提醒。渲染器不添加 Markdown 转义,不提供跨所有 Apprise 渠道的富文本安全保证。
96
+
97
+ ## 与业务共用事务及失败重放
98
+
99
+ `SQLAlchemyDeliveryQueue` 通过 `dwy-notify[sqlalchemy]` 使用 PostgreSQL 或 SQLite。`enqueue` 继续创建并提交独立事务;需要与支付、订单等业务状态同时提交时,调用 `enqueue_in_session(db, app_id, event_id, event_type, content)`。`db` 是宿主的 `AsyncSession`,队列不会 begin、commit、rollback 或 close 它。返回 `True` 表示新入队,`False` 表示同一 `(app_id, event_id)` 已存在;冲突忽略不会使外层事务失效。
100
+
101
+ ```python
102
+ from dwy_notify.sqlalchemy import SQLAlchemyDeliveryQueue
103
+ from sqlalchemy.ext.asyncio import AsyncSession
104
+
105
+
106
+ async def enqueue_payment_notice(
107
+ db: AsyncSession, queue: SQLAlchemyDeliveryQueue, order_id: str, content: str,
108
+ ) -> bool:
109
+ """由宿主把业务更新和本次入队一同提交,异常时一同回滚。"""
110
+ return await queue.enqueue_in_session(db, "payments", order_id, "payment.paid", content)
111
+
112
+
113
+ async def replay_failed_notice(
114
+ db: AsyncSession, queue: SQLAlchemyDeliveryQueue, order_id: str,
115
+ ) -> bool:
116
+ """宿主完成操作者鉴权与审计后,仅重放账本中失败的指定目标。"""
117
+ return await queue.requeue_failed_in_session(db, "payments", order_id, ("wecom",))
118
+ ```
119
+
120
+ scope 非空白且不超过注入表 `app_id` 的长度(默认 128);`event_id`、`event_type` 非空白且不超过 128 字符;正文非空白且最多 65536 字符。内容完整保留,超限拒绝,不截断。`ValueError` 仅包含固定原因码,不回显标识、正文或目标 URL。
121
+
122
+ `allowed_scopes` 限制消费者领取范围:`None` 领取全部 scope,空集合不领取;显式集合只处理其中的 scope。同表其它 scope 的积压正文、状态和租约保持原样,不会因当前消费者的配置被清除。
123
+
124
+ `requeue_failed_in_session` 要求队列注入 `notifier`,`targets` 为非空、不重复的目标名元组。只把指定的 `dead` / `retry` 目标重置为 `pending`、尝试次数 0、当前时刻可发;父记录重新聚合调度与失败状态。正文、展示快照、原目标指纹及其它目标的状态保持不变;当前配置新增的目标不会收到历史消息。无记录、尚未初始化目标快照或选中目标没有失败状态时返回 `False`。
125
+
126
+ 成功目标抛出 `notification_target_sent`;快照中不存在的目标抛出 `notification_target_not_in_snapshot`;当前已移除或指纹变化的目标分别抛出 `notification_target_missing` / `notification_target_changed`;活跃父租约抛出 `notification_active_lease`。先验证全部选择再重置,混合选择不会部分生效。重放自身不发送消息,由既有消费者在事务提交后处理;外层回滚会撤销重放。该接口持有数据库写锁直到宿主提交或回滚,宿主应及时结束事务;SQLite 需要支持 `RETURNING` 的 3.35 及以上版本。
127
+
128
+ 过期租约的 token 会在重放时清除,旧消费者无法覆盖新状态。网络已接收但响应丢失的失败记录仍可能在重放后产生重复消息,沿用原投递语义。
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: dwy-payment
3
+ description: 使用或排查 dwy-payment 服务端支付能力时使用;先核实项目安装版本及公开 API。
4
+ ---
5
+
6
+ # dwy-payment
7
+
8
+ 支付网关、订单和回调;HTTP、认证与权益由宿主处理,核实回调验签、幂等和持久化边界。
9
+
10
+ ## 查找与使用
11
+
12
+ 项目使用 0.3.0 时可阅读 [0.3.0 接入和示例](references/v0/0.3.0.md);其他版本先查该版本的资料和代码,不把此快照当作其他版本的合同。
13
+
14
+ 1. 检查项目锁文件和当前 Python 环境;用 `importlib.metadata.version("dwy-payment")` 查询实际发行版本,依赖范围不等于实际安装版本。未安装时不得假设可用或自动安装。
15
+ 2. 在实际环境中检查 `dwy_payment` 的公开接口、类型及项目已有调用;editable 安装以实际源码为准。说明、实现或安装版本不一致时以运行代码为准。
16
+ 3. 本索引基于仓库 0.3.0,能力与签名可能随小版本变化。精确版本源码位于仓库 `backend/packages/payment`,文档为该目录的 `README.md`;只在确认其版本与消费项目一致时使用。缺少安装源码时查对应 `dwy-payment@版本` 标签,不能用默认分支冒充。
17
+ 4. 在已有调用和该版本文档中核对配置、必需依赖、输入校验、失败处理及生命周期后接入。没有足够证据时说明缺口,不猜 API。
18
+
19
+ 不要因为发现此库就改动项目依赖;新增依赖遵循项目选型规则。关联 FastAPI 或其他自有包时同时参考可用的对应 Skill。