@pinnadash/account-sdk 0.1.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.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @pinnadash/account-sdk
2
+
3
+ Pinnadash 账户体系客户端 SDK(单一来源):PKCE 登录、深链回调、token 刷新、离线宽限、会员权益、付费墙。
4
+ 零依赖,Web Crypto + fetch,浏览器 / Workers / Tauri webview 通用。
5
+
6
+ > 对接文档见 [INTEGRATION.md](./INTEGRATION.md)。本 README 只讲「单一来源分发」机制。
7
+
8
+ ## 单一来源规则(2026-09-18 起)
9
+
10
+ 全公司**只有本目录一份实现**(`packages/sdk/src/`,4 文件)。五个产品仓的拷贝一律由脚本分发,**禁止手改 vendor 目录**:
11
+
12
+ ```bash
13
+ # 在 account_center 仓:
14
+ node scripts/sync-account-sdk.mjs # 全量分发到 5 产品仓
15
+ node scripts/sync-account-sdk.mjs --only easy_clipboard
16
+ node scripts/sync-account-sdk.mjs --check # 对账(可挂 CI/rulecheck),漂移退出码 1
17
+ ```
18
+
19
+ 产品侧消费:tsconfig `paths` + vite `resolve.alias` 把 `@pinnadash/account-sdk`(及 `/desktop` `/paywall` `/login-ui` 子路径)映射到 `src/vendor/account-sdk/`。
20
+
21
+ ## 为什么还不是 npm 包
22
+
23
+ npmjs 发布凭据未配。包名已按 `@pinnadash/account-sdk` 规范占位(业务 import 不含路径细节),凭据就位后:
24
+
25
+ ```bash
26
+ npm publish # packages/sdk 内;业务侧删 alias、加依赖即完成切换,import 零改动
27
+ ```
28
+
29
+ ## 改动流程
30
+
31
+ 1. 改 `packages/sdk/src/`(业务改动走各文件 region 或常规提交,测试在 `packages/sdk/test/`);
32
+ 2. `npx vitest run` + `npx tsc -p tsconfig.json --noEmit` 全绿;
33
+ 3. 跑同步脚本分发;各产品仓 `npm run build` 验证。
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Desktop (Tauri) helpers: storage adapter, offline grace logic (design.md §5.3),
3
+ * deep-link callback parsing and display helpers shared by desktop products.
4
+ *
5
+ * Desktop differences from web:
6
+ * - storage: Tauri app_data dir (file), NOT localStorage — survives reinstalls of webviews
7
+ * - signed snapshot: the last valid session is re-verified against JWKS before use
8
+ * - offline grace: after access token expiry, member access continues for GRACE_DAYS
9
+ * as long as the locally-verified snapshot says member=true; past that, require login
10
+ *
11
+ * The grace logic is pure and tested here; the Tauri file IO is injected via StorageAdapter.
12
+ */
13
+ import type { Session, StorageAdapter, Entitlements } from "./index.js";
14
+ export declare const GRACE_DAYS = 7;
15
+ /**
16
+ * Entitlement resolution for offline desktop:
17
+ * 1. valid session + member -> member
18
+ * 2. expired access token but within grace of `obtained_at + expires_in + grace`
19
+ * AND snapshot says member -> member (degraded)
20
+ * 3. otherwise -> free
21
+ *
22
+ * Trial state is orthogonal to the member grace window: it is judged purely by
23
+ * absolute time from the snapshot (trials[product] > now), so going offline
24
+ * never extends a trial, and an expired token never strips one. Pass the
25
+ * product slug as trialProduct to have trialActive computed; otherwise false.
26
+ *
27
+ * nowSeconds is injectable for tests.
28
+ */
29
+ export declare function resolveOfflineEntitlements(session: Session | null, nowSeconds: number, trialProduct?: string): Entitlements & {
30
+ degraded: boolean;
31
+ trialActive: boolean;
32
+ };
33
+ /**
34
+ * Tauri storage adapter factory. The app reads its snapshot file once at startup
35
+ * (createTauriStorageSync) and passes the seed value; all adapter ops stay
36
+ * synchronous, matching StorageAdapter. Writes flush asynchronously (fire-and-forget)
37
+ * through the io callbacks.
38
+ */
39
+ export declare function createTauriStorage(io: {
40
+ seed: string | null;
41
+ write: (value: string) => Promise<void>;
42
+ }): StorageAdapter;
43
+ /** Session tamper check: structural validation (crypto verification of JWT happens
44
+ * in Account Center's /v1/me and at exchange time; the snapshot only survives if
45
+ * it was issued by the server — a forged JSON cannot pass refresh). */
46
+ export declare function isPlausibleSession(raw: string): boolean;
47
+ /**
48
+ * Parse a custom-scheme login callback URL, accepting only `<scheme>://auth/callback?code=***`.
49
+ * WHATWG URL parses the first segment of a custom scheme as the host
50
+ * (`scheme://auth/callback` → host="auth", pathname="/callback"; the single-slash form
51
+ * yields pathname="/auth/callback") — both shapes are normalized before matching,
52
+ * otherwise the callback never matches and login fails silently (recorded in the
53
+ * lessons ledger; do not "simplify" this back to a pathname comparison).
54
+ */
55
+ export declare function parseDeepLinkCallback(rawUrl: string, scheme: string): URL | null;
56
+ /**
57
+ * Extract the first deep-link URL from process argv. Windows single-instance
58
+ * callbacks arrive via argv / second-instance commandLine (same shape); null if absent.
59
+ */
60
+ export declare function extractDeepLinkFromArgv(argv: string[], scheme: string): string | null;
61
+ /**
62
+ * Display-only: decode the email claim from an access token (JWT) payload, used to
63
+ * show the account name offline. No signature verification (that happens server-side);
64
+ * returns "" on any failure so callers can omit the name instead of erroring.
65
+ */
66
+ export declare function emailFromAccessToken(token: string | null | undefined): string;
67
+ //# sourceMappingURL=desktop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"desktop.d.ts","sourceRoot":"","sources":["../src/desktop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAGxE,eAAO,MAAM,UAAU,IAAI,CAAC;AAG5B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,OAAO,GAAG,IAAI,EACvB,UAAU,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,GACpB,YAAY,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAgB5D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE;IACrC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC,GAAG,cAAc,CA8BjB;AAED;;wEAEwE;AACxE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAcvD;AAID;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,GAAG,GAAG,IAAI,CAShF;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIrF;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAY7E"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Desktop (Tauri) helpers: storage adapter, offline grace logic (design.md §5.3),
3
+ * deep-link callback parsing and display helpers shared by desktop products.
4
+ *
5
+ * Desktop differences from web:
6
+ * - storage: Tauri app_data dir (file), NOT localStorage — survives reinstalls of webviews
7
+ * - signed snapshot: the last valid session is re-verified against JWKS before use
8
+ * - offline grace: after access token expiry, member access continues for GRACE_DAYS
9
+ * as long as the locally-verified snapshot says member=true; past that, require login
10
+ *
11
+ * The grace logic is pure and tested here; the Tauri file IO is injected via StorageAdapter.
12
+ */
13
+ import { trialActive as computeTrialActive } from "./index.js";
14
+ export const GRACE_DAYS = 7;
15
+ const GRACE_SECONDS = GRACE_DAYS * 24 * 3600;
16
+ /**
17
+ * Entitlement resolution for offline desktop:
18
+ * 1. valid session + member -> member
19
+ * 2. expired access token but within grace of `obtained_at + expires_in + grace`
20
+ * AND snapshot says member -> member (degraded)
21
+ * 3. otherwise -> free
22
+ *
23
+ * Trial state is orthogonal to the member grace window: it is judged purely by
24
+ * absolute time from the snapshot (trials[product] > now), so going offline
25
+ * never extends a trial, and an expired token never strips one. Pass the
26
+ * product slug as trialProduct to have trialActive computed; otherwise false.
27
+ *
28
+ * nowSeconds is injectable for tests.
29
+ */
30
+ export function resolveOfflineEntitlements(session, nowSeconds, trialProduct) {
31
+ if (!session) {
32
+ return { member: false, plan: "free", scope: "all", expiresAt: null, startedAt: null, degraded: false, trialActive: false };
33
+ }
34
+ const tokenExpiresAt = session.obtained_at + session.expires_in;
35
+ const member = session.entitlements.member;
36
+ const trialActive = trialProduct ? computeTrialActive(session.entitlements, trialProduct, nowSeconds) : false;
37
+ if (nowSeconds < tokenExpiresAt) {
38
+ return { ...session.entitlements, degraded: false, trialActive };
39
+ }
40
+ // token expired: grace window applies to members only
41
+ if (member && nowSeconds < tokenExpiresAt + GRACE_SECONDS) {
42
+ return { ...session.entitlements, degraded: true, trialActive };
43
+ }
44
+ return { member: false, plan: "free", scope: "all", expiresAt: null, startedAt: null, degraded: member, trialActive };
45
+ }
46
+ /**
47
+ * Tauri storage adapter factory. The app reads its snapshot file once at startup
48
+ * (createTauriStorageSync) and passes the seed value; all adapter ops stay
49
+ * synchronous, matching StorageAdapter. Writes flush asynchronously (fire-and-forget)
50
+ * through the io callbacks.
51
+ */
52
+ export function createTauriStorage(io) {
53
+ const cache = new Map();
54
+ if (io.seed) {
55
+ try {
56
+ const obj = JSON.parse(io.seed);
57
+ for (const [k, v] of Object.entries(obj))
58
+ cache.set(k, v);
59
+ }
60
+ catch {
61
+ /* corrupted snapshot: start clean */
62
+ }
63
+ }
64
+ function persist() {
65
+ const obj = {};
66
+ cache.forEach((v, k) => (obj[k] = v));
67
+ void io.write(JSON.stringify(obj));
68
+ }
69
+ return {
70
+ get(key) {
71
+ return cache.get(key) ?? null;
72
+ },
73
+ set(key, value) {
74
+ cache.set(key, value);
75
+ persist();
76
+ },
77
+ remove(key) {
78
+ cache.delete(key);
79
+ persist();
80
+ },
81
+ };
82
+ }
83
+ /** Session tamper check: structural validation (crypto verification of JWT happens
84
+ * in Account Center's /v1/me and at exchange time; the snapshot only survives if
85
+ * it was issued by the server — a forged JSON cannot pass refresh). */
86
+ export function isPlausibleSession(raw) {
87
+ try {
88
+ const s = JSON.parse(raw);
89
+ return (typeof s.access_token === "string" &&
90
+ s.access_token.split(".").length === 3 && // JWT shape
91
+ typeof s.refresh_token === "string" &&
92
+ typeof s.expires_in === "number" &&
93
+ typeof s.obtained_at === "number" &&
94
+ typeof s.entitlements === "object");
95
+ }
96
+ catch {
97
+ return false;
98
+ }
99
+ }
100
+ // ---------- deep-link callback parsing (shared by desktop shells) ----------
101
+ /**
102
+ * Parse a custom-scheme login callback URL, accepting only `<scheme>://auth/callback?code=***`.
103
+ * WHATWG URL parses the first segment of a custom scheme as the host
104
+ * (`scheme://auth/callback` → host="auth", pathname="/callback"; the single-slash form
105
+ * yields pathname="/auth/callback") — both shapes are normalized before matching,
106
+ * otherwise the callback never matches and login fails silently (recorded in the
107
+ * lessons ledger; do not "simplify" this back to a pathname comparison).
108
+ */
109
+ export function parseDeepLinkCallback(rawUrl, scheme) {
110
+ try {
111
+ const parsed = new URL(rawUrl);
112
+ if (parsed.protocol !== `${scheme}:`)
113
+ return null;
114
+ const tail = parsed.hostname ? `/${parsed.hostname}${parsed.pathname}` : parsed.pathname;
115
+ return tail === "/auth/callback" ? parsed : null;
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ /**
122
+ * Extract the first deep-link URL from process argv. Windows single-instance
123
+ * callbacks arrive via argv / second-instance commandLine (same shape); null if absent.
124
+ */
125
+ export function extractDeepLinkFromArgv(argv, scheme) {
126
+ const prefix = `${scheme}:`;
127
+ const hit = argv.find((arg) => arg.startsWith(prefix));
128
+ return hit ?? null;
129
+ }
130
+ /**
131
+ * Display-only: decode the email claim from an access token (JWT) payload, used to
132
+ * show the account name offline. No signature verification (that happens server-side);
133
+ * returns "" on any failure so callers can omit the name instead of erroring.
134
+ */
135
+ export function emailFromAccessToken(token) {
136
+ if (!token)
137
+ return "";
138
+ const parts = token.split(".");
139
+ if (parts.length !== 3)
140
+ return "";
141
+ try {
142
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
143
+ const json = atob(b64 + "=".repeat((4 - (b64.length % 4)) % 4));
144
+ const email = JSON.parse(json).email;
145
+ return typeof email === "string" ? email : "";
146
+ }
147
+ catch {
148
+ return "";
149
+ }
150
+ }
151
+ //# sourceMappingURL=desktop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"desktop.js","sourceRoot":"","sources":["../src/desktop.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @pinnadash/account-sdk — client SDK for Account Center.
3
+ *
4
+ * One account, every product. The SDK handles:
5
+ * - login() → redirect to Account Center (magic link / Google), with PKCE
6
+ * - handleCallback() → exchange ?code=*** for tokens (PKCE)
7
+ * - getEntitlements() → { member, plan, scope, expiresAt }
8
+ * - refreshIfDue() → rotate refresh token before access token expiry
9
+ * - logout() → revoke refresh token + clear local session
10
+ *
11
+ * Storage is pluggable: localStorage (web) by default, custom storage for
12
+ * Tauri/desktop (see README §desktop). Zero dependencies; uses Web Crypto +
13
+ * fetch only, so it runs in browsers, Workers and Tauri webviews.
14
+ */
15
+ export interface Entitlements {
16
+ member: boolean;
17
+ plan: string;
18
+ scope: "all";
19
+ expiresAt: number | null;
20
+ /** 当前订阅创建时间(unix 秒);老 session 快照无此字段 */
21
+ startedAt?: number | null;
22
+ /** Pro 7 天试用(design.md §6.5):付费优先;老 session 快照无此字段 */
23
+ is_trial?: boolean;
24
+ /** 最近开启的活跃试用产品(client_id);多产品场景真源是 trials */
25
+ trial_product?: string | null;
26
+ /** 最近开启的活跃试用到期时间(unix 秒) */
27
+ trial_ends_at?: number | null;
28
+ /** 全量活跃试用映射 product → ends_at(判定本产品试用态用 trials[productSlug]) */
29
+ trials?: Record<string, number>;
30
+ }
31
+ /**
32
+ * 本产品试用结束时间(unix 秒);无试用记录返回 undefined。
33
+ * trials 映射是真源;老快照(无 trials 键)回退 is_trial/trial_product/trial_ends_at。
34
+ */
35
+ export declare function trialEndsAt(ent: Entitlements, product: string): number | undefined;
36
+ /** 试用按绝对时间判定(ends_at > now),不因离线/token 宽限延长 */
37
+ export declare function trialActive(ent: Entitlements, product: string, now: number): boolean;
38
+ export interface Session {
39
+ access_token: string;
40
+ refresh_token: string;
41
+ token_type: "Bearer";
42
+ expires_in: number;
43
+ entitlements: Entitlements;
44
+ obtained_at: number;
45
+ }
46
+ export interface AccountClientOptions {
47
+ /** Account Center base URL, e.g. https://account.example.com */
48
+ centerUrl: string;
49
+ /** Product slug = client_id registered in Account Center (src/lib/auth/redirects.ts) */
50
+ productSlug: string;
51
+ /** Where Account Center redirects back after login. Defaults to `${origin}/auth/callback`. */
52
+ redirectUri?: string;
53
+ /** Storage adapter. Defaults to localStorage; desktop passes its own. */
54
+ storage?: StorageAdapter;
55
+ /** Clock override for tests. */
56
+ now?: () => number;
57
+ }
58
+ export interface StorageAdapter {
59
+ get(key: string): string | null;
60
+ set(key: string, value: string): void;
61
+ remove(key: string): void;
62
+ }
63
+ export declare class AccountError extends Error {
64
+ readonly code: string;
65
+ readonly status?: number | undefined;
66
+ constructor(code: string, message: string, status?: number | undefined);
67
+ }
68
+ export declare function createAccountClient(opts: AccountClientOptions): {
69
+ /** True if a session exists locally (not necessarily valid — call getEntitlements). */
70
+ hasSession(): boolean;
71
+ /** Read the locally persisted session (desktop offline entitlement resolution). */
72
+ getSession(): Session | null;
73
+ /** Build the login URL without navigating (desktop opens it via system browser). */
74
+ createLoginUrl: () => Promise<string>;
75
+ /**
76
+ * Start login: navigate the browser to Account Center.
77
+ * PKCE verifier is stashed in storage and consumed by handleCallback().
78
+ */
79
+ login(): Promise<void>;
80
+ /**
81
+ * In-product email login: send the magic-link email without leaving the product.
82
+ * The email's verify link lands on this client's redirect_uri with ?code=***,
83
+ * then handleCallback() finishes the PKCE exchange as usual.
84
+ *
85
+ * Dev note: when the Account Center runs without RESEND_API_KEY (dev mode) the
86
+ * response includes login_token + verify_url so flows can be tested without mail.
87
+ */
88
+ requestMagicLink(email: string, turnstileToken?: string): Promise<{
89
+ ok: true;
90
+ message: string;
91
+ dev?: {
92
+ login_token: string;
93
+ verify_url: string;
94
+ };
95
+ }>;
96
+ /**
97
+ * Finish login on the callback page: read ?code=***, exchange with the stashed
98
+ * verifier. Returns the session. Safe to call on any page — returns null when
99
+ * no code param is present.
100
+ */
101
+ handleCallback(searchParams?: URLSearchParams | string): Promise<Session | null>;
102
+ /**
103
+ * Membership state. Force-refreshes from /v1/me when stale (or force=true),
104
+ * otherwise serves from the session's cached entitlements.
105
+ */
106
+ getEntitlements(force?: boolean): Promise<Entitlements>;
107
+ /**
108
+ * Rotate tokens if the access token is at/near expiry (or force=true).
109
+ * Returns the fresh session, or null when logged out / refresh rejected.
110
+ */
111
+ refreshIfDue(force?: boolean): Promise<Session | null>;
112
+ /**
113
+ * Start the 7-day Pro trial for this product (POST /v1/trial/start).
114
+ * Idempotent and once-per-account-per-product server-side; outcome maps
115
+ * HTTP 409 variants so callers can message without parsing errors.
116
+ */
117
+ startTrial(product: string): Promise<{
118
+ outcome: "started" | "active" | "already_member" | "trial_used";
119
+ trial?: {
120
+ product: string;
121
+ starts_at: number;
122
+ ends_at: number;
123
+ };
124
+ }>;
125
+ /** Revoke the refresh token server-side and clear local state. */
126
+ logout(): Promise<void>;
127
+ };
128
+ export type AccountClient = ReturnType<typeof createAccountClient>;
129
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6CAA6C;IAC7C,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,4BAA4B;IAC5B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIlF;AAED,+CAA+C;AAC/C,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAGpF;AAED,MAAM,WAAW,OAAO;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,QAAQ,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,YAAY,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,gEAAgE;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,WAAW,EAAE,MAAM,CAAC;IACpB,8FAA8F;IAC9F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,gCAAgC;IAChC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAChC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACtC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAoCD,qBAAa,YAAa,SAAQ,KAAK;aAEnB,IAAI,EAAE,MAAM;aAEZ,MAAM,CAAC,EAAE,MAAM;IAHjC,YACkB,IAAI,EAAE,MAAM,EAC5B,OAAO,EAAE,MAAM,EACC,MAAM,CAAC,EAAE,MAAM,YAAA,EAIhC;CACF;AAkCD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB;IA4C1D,uFAAuF;kBACzE,OAAO;IAIrB,mFAAmF;kBACrE,OAAO,GAAG,IAAI;IAI5B,oFAAoF;0BArBrD,OAAO,CAAC,MAAM,CAAC;IAwB9C;;;OAGG;aACY,OAAO,CAAC,IAAI,CAAC;IAS5B;;;;;;;OAOG;4BAEM,MAAM,mBACI,MAAM,GACtB,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE;YAAE,WAAW,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IA8B5F;;;;OAIG;kCACiC,eAAe,GAAG,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAwBtF;;;OAGG;sCACmC,OAAO,CAAC,YAAY,CAAC;IA0B3D;;;OAGG;mCACgC,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAmB1D;;;;OAIG;wBAEQ,MAAM,GACd,OAAO,CAAC;QACT,OAAO,EAAE,SAAS,GAAG,QAAQ,GAAG,gBAAgB,GAAG,YAAY,CAAC;QAChE,KAAK,CAAC,EAAE;YAAE,OAAO,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC;KACjE,CAAC;IAqBF,kEAAkE;cAClD,OAAO,CAAC,IAAI,CAAC;EAYhC;AAED,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,314 @@
1
+ /**
2
+ * @pinnadash/account-sdk — client SDK for Account Center.
3
+ *
4
+ * One account, every product. The SDK handles:
5
+ * - login() → redirect to Account Center (magic link / Google), with PKCE
6
+ * - handleCallback() → exchange ?code=*** for tokens (PKCE)
7
+ * - getEntitlements() → { member, plan, scope, expiresAt }
8
+ * - refreshIfDue() → rotate refresh token before access token expiry
9
+ * - logout() → revoke refresh token + clear local session
10
+ *
11
+ * Storage is pluggable: localStorage (web) by default, custom storage for
12
+ * Tauri/desktop (see README §desktop). Zero dependencies; uses Web Crypto +
13
+ * fetch only, so it runs in browsers, Workers and Tauri webviews.
14
+ */
15
+ /**
16
+ * 本产品试用结束时间(unix 秒);无试用记录返回 undefined。
17
+ * trials 映射是真源;老快照(无 trials 键)回退 is_trial/trial_product/trial_ends_at。
18
+ */
19
+ export function trialEndsAt(ent, product) {
20
+ if (ent.trials)
21
+ return ent.trials[product];
22
+ if (ent.is_trial && ent.trial_product === product)
23
+ return ent.trial_ends_at ?? undefined;
24
+ return undefined;
25
+ }
26
+ /** 试用按绝对时间判定(ends_at > now),不因离线/token 宽限延长 */
27
+ export function trialActive(ent, product, now) {
28
+ const endsAt = trialEndsAt(ent, product);
29
+ return endsAt !== undefined && endsAt > now;
30
+ }
31
+ // ---------- default web storage ----------
32
+ const localStorageAdapter = {
33
+ get(key) {
34
+ try {
35
+ return localStorage.getItem(key);
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ },
41
+ set(key, value) {
42
+ try {
43
+ localStorage.setItem(key, value);
44
+ }
45
+ catch {
46
+ /* private mode: session stays memory-only */
47
+ }
48
+ },
49
+ remove(key) {
50
+ try {
51
+ localStorage.removeItem(key);
52
+ }
53
+ catch {
54
+ /* noop */
55
+ }
56
+ },
57
+ };
58
+ // ---------- constants ----------
59
+ const SESSION_KEY = "account.session";
60
+ const VERIFIER_KEY = "account.pkce.verifier";
61
+ const REFRESH_AHEAD = 5 * 60; // refresh 5 min before expiry
62
+ // ---------- errors ----------
63
+ export class AccountError extends Error {
64
+ code;
65
+ status;
66
+ constructor(code, message, status) {
67
+ super(message);
68
+ this.code = code;
69
+ this.status = status;
70
+ this.name = "AccountError";
71
+ }
72
+ }
73
+ // ---------- helpers ----------
74
+ function b64url(bytes) {
75
+ const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
76
+ let bin = "";
77
+ arr.forEach((b) => (bin += String.fromCharCode(b)));
78
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
79
+ }
80
+ async function pkcePair() {
81
+ const arr = new Uint8Array(32);
82
+ crypto.getRandomValues(arr);
83
+ const verifier = b64url(arr);
84
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
85
+ return { verifier, challenge: b64url(digest) };
86
+ }
87
+ async function parseJsonError(res) {
88
+ let code = "request_failed";
89
+ let message = `HTTP ${res.status}`;
90
+ try {
91
+ const body = (await res.json());
92
+ if (body.error)
93
+ code = body.error;
94
+ if (body.message)
95
+ message = body.message;
96
+ }
97
+ catch {
98
+ /* non-JSON error body */
99
+ }
100
+ throw new AccountError(code, message, res.status);
101
+ }
102
+ // ---------- client ----------
103
+ export function createAccountClient(opts) {
104
+ const centerUrl = opts.centerUrl.replace(/\/+$/, "");
105
+ const storage = opts.storage ?? localStorageAdapter;
106
+ const now = opts.now ?? (() => Math.floor(Date.now() / 1000));
107
+ const redirectUri = opts.redirectUri ?? `${globalThis.location?.origin ?? ""}/auth/callback`;
108
+ function readSession() {
109
+ const raw = storage.get(SESSION_KEY);
110
+ if (!raw)
111
+ return null;
112
+ try {
113
+ return JSON.parse(raw);
114
+ }
115
+ catch {
116
+ storage.remove(SESSION_KEY);
117
+ return null;
118
+ }
119
+ }
120
+ function writeSession(s) {
121
+ const persisted = { ...s, obtained_at: now() };
122
+ storage.set(SESSION_KEY, JSON.stringify(persisted));
123
+ return persisted;
124
+ }
125
+ async function api(path, init) {
126
+ return fetch(`${centerUrl}${path}`, {
127
+ ...init,
128
+ headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
129
+ });
130
+ }
131
+ /** 构造授权 URL 并持久化 PKCE verifier,但不导航。
132
+ * 桌面壳调用它后用系统浏览器打开 URL(回调经自定义 scheme 的 redirect_uri 回到应用)。 */
133
+ async function createLoginUrl() {
134
+ const { verifier, challenge } = await pkcePair();
135
+ storage.set(VERIFIER_KEY, JSON.stringify({ verifier, at: now() }));
136
+ return (`${centerUrl}/login/?client_id=${encodeURIComponent(opts.productSlug)}` +
137
+ `&redirect_uri=${encodeURIComponent(redirectUri)}` +
138
+ `&code_challenge=${encodeURIComponent(challenge)}`);
139
+ }
140
+ return {
141
+ /** True if a session exists locally (not necessarily valid — call getEntitlements). */
142
+ hasSession() {
143
+ return readSession() !== null;
144
+ },
145
+ /** Read the locally persisted session (desktop offline entitlement resolution). */
146
+ getSession() {
147
+ return readSession();
148
+ },
149
+ /** Build the login URL without navigating (desktop opens it via system browser). */
150
+ createLoginUrl,
151
+ /**
152
+ * Start login: navigate the browser to Account Center.
153
+ * PKCE verifier is stashed in storage and consumed by handleCallback().
154
+ */
155
+ async login() {
156
+ const url = await createLoginUrl();
157
+ if (globalThis.location) {
158
+ globalThis.location.assign(url);
159
+ }
160
+ else {
161
+ throw new AccountError("no_browser", "login() requires a browser context; desktop should open the URL via shell opener");
162
+ }
163
+ },
164
+ /**
165
+ * In-product email login: send the magic-link email without leaving the product.
166
+ * The email's verify link lands on this client's redirect_uri with ?code=***,
167
+ * then handleCallback() finishes the PKCE exchange as usual.
168
+ *
169
+ * Dev note: when the Account Center runs without RESEND_API_KEY (dev mode) the
170
+ * response includes login_token + verify_url so flows can be tested without mail.
171
+ */
172
+ async requestMagicLink(email, turnstileToken) {
173
+ const { verifier, challenge } = await pkcePair();
174
+ storage.set(VERIFIER_KEY, JSON.stringify({ verifier, at: now() }));
175
+ const res = await api("/v1/auth/magic-link", {
176
+ method: "POST",
177
+ body: JSON.stringify({
178
+ email,
179
+ client_id: opts.productSlug,
180
+ redirect_uri: redirectUri,
181
+ code_challenge: challenge,
182
+ ...(turnstileToken ? { turnstile_token: turnstileToken } : {}),
183
+ }),
184
+ });
185
+ if (!res.ok)
186
+ await parseJsonError(res);
187
+ const data = (await res.json());
188
+ return {
189
+ ok: true,
190
+ message: data.message,
191
+ ...(data.login_token && data.verify_url
192
+ ? { dev: { login_token: data.login_token, verify_url: data.verify_url } }
193
+ : {}),
194
+ };
195
+ },
196
+ /**
197
+ * Finish login on the callback page: read ?code=***, exchange with the stashed
198
+ * verifier. Returns the session. Safe to call on any page — returns null when
199
+ * no code param is present.
200
+ */
201
+ async handleCallback(searchParams) {
202
+ const params = typeof searchParams === "string"
203
+ ? new URLSearchParams(searchParams)
204
+ : (searchParams ?? new URLSearchParams(globalThis.location?.search ?? ""));
205
+ const code = params.get("code");
206
+ if (!code)
207
+ return null;
208
+ const rawVerifier = storage.get(VERIFIER_KEY);
209
+ if (!rawVerifier) {
210
+ throw new AccountError("pkce_missing", "No pending PKCE verifier — call login() first");
211
+ }
212
+ const { verifier } = JSON.parse(rawVerifier);
213
+ storage.remove(VERIFIER_KEY);
214
+ const res = await api("/v1/auth/token", {
215
+ method: "POST",
216
+ body: JSON.stringify({ code, code_verifier: verifier, client_id: opts.productSlug }),
217
+ });
218
+ if (!res.ok)
219
+ await parseJsonError(res);
220
+ const session = (await res.json());
221
+ return writeSession(session);
222
+ },
223
+ /**
224
+ * Membership state. Force-refreshes from /v1/me when stale (or force=true),
225
+ * otherwise serves from the session's cached entitlements.
226
+ */
227
+ async getEntitlements(force = false) {
228
+ const session = readSession();
229
+ if (!session)
230
+ return { member: false, plan: "free", scope: "all", expiresAt: null, startedAt: null };
231
+ const fresh = force || now() >= session.obtained_at + REFRESH_AHEAD;
232
+ if (!fresh)
233
+ return session.entitlements;
234
+ // /v1/me both validates the token and returns live entitlements
235
+ const res = await fetch(`${centerUrl}/v1/me`, {
236
+ headers: { Authorization: `Bearer ${session.access_token}` },
237
+ });
238
+ if (res.status === 401) {
239
+ // access token expired → try refresh once, then retry me
240
+ const refreshed = await this.refreshIfDue(true);
241
+ if (!refreshed) {
242
+ storage.remove(SESSION_KEY);
243
+ return { member: false, plan: "free", scope: "all", expiresAt: null, startedAt: null };
244
+ }
245
+ return this.getEntitlements(false);
246
+ }
247
+ if (!res.ok)
248
+ await parseJsonError(res);
249
+ const me = (await res.json());
250
+ writeSession({ ...session, entitlements: me.entitlements });
251
+ return me.entitlements;
252
+ },
253
+ /**
254
+ * Rotate tokens if the access token is at/near expiry (or force=true).
255
+ * Returns the fresh session, or null when logged out / refresh rejected.
256
+ */
257
+ async refreshIfDue(force = false) {
258
+ const session = readSession();
259
+ if (!session)
260
+ return null;
261
+ const expiresAt = session.obtained_at + session.expires_in;
262
+ if (!force && now() < expiresAt - REFRESH_AHEAD)
263
+ return session;
264
+ const res = await api("/v1/auth/refresh", {
265
+ method: "POST",
266
+ body: JSON.stringify({ refresh_token: session.refresh_token }),
267
+ });
268
+ if (res.status === 401) {
269
+ storage.remove(SESSION_KEY);
270
+ return null;
271
+ }
272
+ if (!res.ok)
273
+ await parseJsonError(res);
274
+ const fresh = (await res.json());
275
+ return writeSession(fresh);
276
+ },
277
+ /**
278
+ * Start the 7-day Pro trial for this product (POST /v1/trial/start).
279
+ * Idempotent and once-per-account-per-product server-side; outcome maps
280
+ * HTTP 409 variants so callers can message without parsing errors.
281
+ */
282
+ async startTrial(product) {
283
+ const session = readSession();
284
+ if (!session)
285
+ throw new AccountError("unauthorized", "Login required before starting a trial");
286
+ const res = await api("/v1/trial/start", {
287
+ method: "POST",
288
+ headers: { Authorization: `Bearer ${session.access_token}` },
289
+ body: JSON.stringify({ product }),
290
+ });
291
+ if (res.status === 409) {
292
+ const body = (await res.json().catch(() => ({})));
293
+ return { outcome: body.error === "already_member" ? "already_member" : "trial_used" };
294
+ }
295
+ if (!res.ok)
296
+ await parseJsonError(res);
297
+ const data = (await res.json());
298
+ return { outcome: data.already ? "active" : "started", trial: data.trial };
299
+ },
300
+ /** Revoke the refresh token server-side and clear local state. */
301
+ async logout() {
302
+ const session = readSession();
303
+ if (session?.refresh_token) {
304
+ await api("/v1/auth/logout", {
305
+ method: "POST",
306
+ body: JSON.stringify({ refresh_token: session.refresh_token }),
307
+ }).catch(() => undefined); // best-effort; local logout always succeeds
308
+ }
309
+ storage.remove(SESSION_KEY);
310
+ storage.remove(VERIFIER_KEY);
311
+ },
312
+ };
313
+ }
314
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @pinnadash/account-sdk/login-ui — ready-made login UI for products.
3
+ *
4
+ * 产品拿去就能用的登录界面:自带样式(scoped CSS,不污染宿主)、
5
+ * 内置 zh-CN / en-US 双语文案、Turnstile 预留。产品自行决定承载方式:
6
+ *
7
+ * import { mountLoginPanel, openLoginModal } from "@pinnadash/account-sdk/login-ui";
8
+ *
9
+ * // 嵌入页面任意容器(页面式)
10
+ * mountLoginPanel("#login-root", account);
11
+ *
12
+ * // 弹窗式
13
+ * const modal = openLoginModal(account);
14
+ * modal.close();
15
+ *
16
+ * 纯 DOM 实现(零依赖),与 SDK 其余部分一样只依赖 Web 标准。
17
+ * 登录完成的后续(?code= 回调)仍由产品的 handleCallback 页处理。
18
+ */
19
+ import type { AccountClient } from "./index.js";
20
+ export interface LoginPanelOptions {
21
+ /** 界面语言。缺省自动检测浏览器语言,检测失败回退 zh-CN。 */
22
+ locale?: string;
23
+ /** Turnstile site key(生产环境防机器人)。传入即渲染 Turnstile widget。 */
24
+ turnstileSiteKey?: string;
25
+ /** 邮件发送成功后的回调(用于埋点等)。 */
26
+ onLinkSent?: (email: string) => void;
27
+ }
28
+ export interface LoginPanelHandle {
29
+ /** 移除面板及其注入的样式引用(弹窗关闭时使用)。 */
30
+ destroy(): void;
31
+ }
32
+ /** 把登录面板挂载到容器(selector 或元素)。页面式与弹窗式共用。 */
33
+ export declare function mountLoginPanel(container: string | HTMLElement, client: AccountClient, opts?: LoginPanelOptions): LoginPanelHandle;
34
+ /** 弹窗式登录:创建全屏遮罩 + 面板;返回 close() 与 destroy()。 */
35
+ export declare function openLoginModal(client: AccountClient, opts?: LoginPanelOptions): LoginPanelHandle & {
36
+ close(): void;
37
+ };
38
+ //# sourceMappingURL=login-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-ui.d.ts","sourceRoot":"","sources":["../src/login-ui.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAuDhD,MAAM,WAAW,iBAAiB;IAChC,qCAAqC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,yBAAyB;IACzB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,8BAA8B;IAC9B,OAAO,IAAI,IAAI,CAAC;CACjB;AAoMD,0CAA0C;AAC1C,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,GAAG,WAAW,EAC/B,MAAM,EAAE,aAAa,EACrB,IAAI,GAAE,iBAAsB,GAC3B,gBAAgB,CAqBlB;AAED,gDAAgD;AAChD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,IAAI,GAAE,iBAAsB,GAC3B,gBAAgB,GAAG;IAAE,KAAK,IAAI,IAAI,CAAA;CAAE,CA8BtC"}
@@ -0,0 +1,278 @@
1
+ /**
2
+ * @pinnadash/account-sdk/login-ui — ready-made login UI for products.
3
+ *
4
+ * 产品拿去就能用的登录界面:自带样式(scoped CSS,不污染宿主)、
5
+ * 内置 zh-CN / en-US 双语文案、Turnstile 预留。产品自行决定承载方式:
6
+ *
7
+ * import { mountLoginPanel, openLoginModal } from "@pinnadash/account-sdk/login-ui";
8
+ *
9
+ * // 嵌入页面任意容器(页面式)
10
+ * mountLoginPanel("#login-root", account);
11
+ *
12
+ * // 弹窗式
13
+ * const modal = openLoginModal(account);
14
+ * modal.close();
15
+ *
16
+ * 纯 DOM 实现(零依赖),与 SDK 其余部分一样只依赖 Web 标准。
17
+ * 登录完成的后续(?code= 回调)仍由产品的 handleCallback 页处理。
18
+ */
19
+ const MESSAGES = {
20
+ "zh-CN": {
21
+ title: "登录",
22
+ subtitle: "输入邮箱,我们会发送一次性登录链接。",
23
+ emailLabel: "邮箱地址",
24
+ emailPlaceholder: "name@example.com",
25
+ submit: "发送登录链接",
26
+ submitting: "正在发送…",
27
+ sentTitle: "请查收邮件",
28
+ sentBody: "登录链接已发送到 {email},10 分钟内有效,点击邮件中的按钮即可完成登录。",
29
+ resend: "重新发送",
30
+ resendIn: "{s} 秒后可重新发送",
31
+ errorInvalidEmail: "请输入有效的邮箱地址。",
32
+ errorFailed: "发送失败:{message}",
33
+ devHint: "开发模式:点击链接完成登录",
34
+ footer: "登录即表示同意本产品的服务条款与隐私政策。",
35
+ },
36
+ "en-US": {
37
+ title: "Sign in",
38
+ subtitle: "Enter your email and we will send a one-time sign-in link.",
39
+ emailLabel: "Email address",
40
+ emailPlaceholder: "name@example.com",
41
+ submit: "Send sign-in link",
42
+ submitting: "Sending…",
43
+ sentTitle: "Check your inbox",
44
+ sentBody: "We sent a sign-in link to {email}. It expires in 10 minutes — click the button in the email to finish.",
45
+ resend: "Resend",
46
+ resendIn: "Resend available in {s}s",
47
+ errorInvalidEmail: "Please enter a valid email address.",
48
+ errorFailed: "Failed to send: {message}",
49
+ devHint: "Dev mode: open the link to sign in",
50
+ footer: "By signing in you agree to this product's terms and privacy policy.",
51
+ },
52
+ };
53
+ function detectLocale(pref) {
54
+ if (pref === "zh-CN" || pref === "en-US")
55
+ return pref;
56
+ const langs = pref ? [pref] : (typeof navigator !== "undefined" ? [...navigator.languages] : []);
57
+ for (const l of langs) {
58
+ if (/^zh/i.test(l))
59
+ return "zh-CN";
60
+ if (/^en/i.test(l))
61
+ return "en-US";
62
+ }
63
+ return "zh-CN";
64
+ }
65
+ // ---------- styles(scoped,前缀 acx- 防冲突) ----------
66
+ const STYLE_ID = "acx-login-style";
67
+ const CSS = `
68
+ .acx-root { font-family: inherit; color: inherit; }
69
+ .acx-card { max-width: 380px; margin: 0 auto; }
70
+ .acx-title { margin: 0 0 4px; font-size: 20px; font-weight: 700; }
71
+ .acx-subtitle { margin: 0 0 20px; font-size: 14px; opacity: .72; }
72
+ .acx-field { display: grid; gap: 6px; margin-bottom: 14px; }
73
+ .acx-label { font-size: 13px; font-weight: 600; }
74
+ .acx-input { min-height: 42px; padding: 0 12px; font: inherit; border: 1px solid rgba(0,0,0,.18); border-radius: 8px; background: #fff; color: #111; }
75
+ .acx-input:focus { outline: 2px solid #1f5fbf; outline-offset: 1px; }
76
+ .acx-button { min-height: 42px; padding: 0 18px; font: inherit; font-weight: 600; color: #fff; background: #1f5fbf; border: 1px solid #1f5fbf; border-radius: 8px; cursor: pointer; }
77
+ .acx-button:hover { background: #1a51a3; }
78
+ .acx-button:disabled { opacity: .56; cursor: default; }
79
+ .acx-status { margin: 12px 0 0; font-size: 14px; }
80
+ .acx-status.is-error { color: #a3302f; }
81
+ .acx-devlink { margin: 12px 0 0; font-size: 13px; word-break: break-all; }
82
+ .acx-footer { margin: 18px 0 0; font-size: 12px; opacity: .6; }
83
+ .acx-turnstile { margin-bottom: 14px; }
84
+ .acx-overlay { position: fixed; inset: 0; z-index: 9999; display: flex; align-items: center; justify-content: center; background: rgba(10,16,28,.55); padding: 16px; }
85
+ .acx-modal { position: relative; background: #fff; color: #17233b; border-radius: 12px; padding: 28px 24px; box-shadow: 0 12px 40px rgba(0,0,0,.25); }
86
+ .acx-close { position: absolute; top: 10px; right: 10px; width: 32px; height: 32px; border: none; background: transparent; font-size: 18px; line-height: 1; cursor: pointer; color: inherit; border-radius: 6px; }
87
+ .acx-close:hover { background: rgba(0,0,0,.06); }
88
+ `;
89
+ function ensureStyles(doc) {
90
+ if (doc.getElementById(STYLE_ID))
91
+ return;
92
+ const style = doc.createElement("style");
93
+ style.id = STYLE_ID;
94
+ style.textContent = CSS;
95
+ doc.head.appendChild(style);
96
+ }
97
+ function escapeHtml(text) {
98
+ return text
99
+ .replace(/&/g, "&amp;")
100
+ .replace(/</g, "&lt;")
101
+ .replace(/>/g, "&gt;")
102
+ .replace(/"/g, "&quot;");
103
+ }
104
+ // ---------- Turnstile (预留:传 site key 即启用) ----------
105
+ const TURNSTILE_SCRIPT_ID = "acx-turnstile-script";
106
+ function renderTurnstile(doc, container, siteKey) {
107
+ return new Promise((resolve) => {
108
+ const w = doc.defaultView;
109
+ const finish = () => {
110
+ if (!w?.turnstile)
111
+ return resolve(null);
112
+ try {
113
+ const widgetId = w.turnstile.render(container, {
114
+ sitekey: siteKey,
115
+ callback: (token) => resolve(token),
116
+ });
117
+ if (!widgetId)
118
+ resolve(null);
119
+ }
120
+ catch {
121
+ resolve(null);
122
+ }
123
+ };
124
+ if (w?.turnstile)
125
+ return finish();
126
+ if (!doc.getElementById(TURNSTILE_SCRIPT_ID)) {
127
+ const s = doc.createElement("script");
128
+ s.id = TURNSTILE_SCRIPT_ID;
129
+ s.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
130
+ s.async = true;
131
+ s.onload = finish;
132
+ s.onerror = () => resolve(null);
133
+ doc.head.appendChild(s);
134
+ }
135
+ });
136
+ }
137
+ function buildPanel(root, t) {
138
+ root.classList.add("acx-root", "acx-card");
139
+ root.innerHTML = `
140
+ <h2 class="acx-title">${escapeHtml(t.title)}</h2>
141
+ <p class="acx-subtitle">${escapeHtml(t.subtitle)}</p>
142
+ <form class="acx-form" novalidate>
143
+ <div class="acx-field">
144
+ <label class="acx-label" for="acx-email">${escapeHtml(t.emailLabel)}</label>
145
+ <input class="acx-input" id="acx-email" name="email" type="email" autocomplete="email"
146
+ placeholder="${escapeHtml(t.emailPlaceholder)}" required />
147
+ </div>
148
+ <div class="acx-turnstile" hidden></div>
149
+ <button class="acx-button" type="submit">${escapeHtml(t.submit)}</button>
150
+ </form>
151
+ <p class="acx-status" role="status" aria-live="polite"></p>
152
+ <p class="acx-footer">${escapeHtml(t.footer)}</p>
153
+ `;
154
+ const form = root.querySelector(".acx-form");
155
+ const email = root.querySelector("#acx-email");
156
+ const submit = root.querySelector(".acx-button");
157
+ const status = root.querySelector(".acx-status");
158
+ const turnstileBox = root.querySelector(".acx-turnstile");
159
+ return { form, email, submit, status, turnstileBox };
160
+ }
161
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
162
+ function setStatus(status, text, isError) {
163
+ status.textContent = text;
164
+ status.classList.toggle("is-error", isError);
165
+ }
166
+ function wirePanel(doc, els, client, opts, t) {
167
+ let busy = false;
168
+ let resendTimer;
169
+ const onSubmit = async (event) => {
170
+ event.preventDefault();
171
+ if (busy)
172
+ return;
173
+ const value = els.email.value.trim();
174
+ if (!EMAIL_RE.test(value)) {
175
+ setStatus(els.status, t.errorInvalidEmail, true);
176
+ return;
177
+ }
178
+ busy = true;
179
+ els.submit.disabled = true;
180
+ setStatus(els.status, t.submitting, false);
181
+ try {
182
+ let turnstileToken;
183
+ if (opts.turnstileSiteKey && !els.turnstileBox.hasChildNodes()) {
184
+ els.turnstileBox.hidden = false;
185
+ turnstileToken = (await renderTurnstile(doc, els.turnstileBox, opts.turnstileSiteKey)) ?? undefined;
186
+ }
187
+ const result = await client.requestMagicLink(value, turnstileToken);
188
+ setStatus(els.status, t.sentBody.replace("{email}", value), false);
189
+ // sent 面板:标题化提示 + 60s 重新发送倒计时(与服务端限频一致)
190
+ els.submit.disabled = true;
191
+ let wait = 60;
192
+ const tick = () => {
193
+ els.submit.textContent = wait > 0 ? t.resendIn.replace("{s}", String(wait)) : t.resend;
194
+ if (wait <= 0) {
195
+ els.submit.disabled = false;
196
+ busy = false;
197
+ return;
198
+ }
199
+ wait -= 1;
200
+ resendTimer = doc.defaultView.setTimeout(tick, 1000);
201
+ };
202
+ tick();
203
+ if (result.dev?.verify_url) {
204
+ const dev = doc.createElement("p");
205
+ dev.className = "acx-devlink";
206
+ const a = doc.createElement("a");
207
+ a.href = result.dev.verify_url;
208
+ a.textContent = `${t.devHint} →`;
209
+ dev.appendChild(a);
210
+ els.status.after(dev);
211
+ }
212
+ opts.onLinkSent?.(value);
213
+ }
214
+ catch (error) {
215
+ const message = error instanceof Error ? error.message : String(error);
216
+ setStatus(els.status, t.errorFailed.replace("{message}", message), true);
217
+ els.submit.disabled = false;
218
+ busy = false;
219
+ }
220
+ };
221
+ els.form.addEventListener("submit", onSubmit);
222
+ return () => {
223
+ els.form.removeEventListener("submit", onSubmit);
224
+ if (resendTimer !== undefined)
225
+ doc.defaultView?.clearTimeout(resendTimer);
226
+ };
227
+ }
228
+ /** 把登录面板挂载到容器(selector 或元素)。页面式与弹窗式共用。 */
229
+ export function mountLoginPanel(container, client, opts = {}) {
230
+ const doc = container instanceof HTMLElement ? container.ownerDocument : globalThis.document;
231
+ const root = typeof container === "string"
232
+ ? doc.querySelector(container)
233
+ : container;
234
+ if (!root)
235
+ throw new Error("mountLoginPanel: container not found");
236
+ ensureStyles(doc);
237
+ const locale = detectLocale(opts.locale);
238
+ const t = MESSAGES[locale];
239
+ const els = buildPanel(root, t);
240
+ const unwire = wirePanel(doc, els, client, opts, t);
241
+ return {
242
+ destroy() {
243
+ unwire();
244
+ root.innerHTML = "";
245
+ root.classList.remove("acx-root", "acx-card");
246
+ },
247
+ };
248
+ }
249
+ /** 弹窗式登录:创建全屏遮罩 + 面板;返回 close() 与 destroy()。 */
250
+ export function openLoginModal(client, opts = {}) {
251
+ const doc = globalThis.document;
252
+ ensureStyles(doc);
253
+ const overlay = doc.createElement("div");
254
+ overlay.className = "acx-overlay";
255
+ const modal = doc.createElement("div");
256
+ modal.className = "acx-modal";
257
+ const closeBtn = doc.createElement("button");
258
+ closeBtn.className = "acx-close";
259
+ closeBtn.type = "button";
260
+ closeBtn.setAttribute("aria-label", "Close");
261
+ closeBtn.textContent = "×";
262
+ const panel = doc.createElement("div");
263
+ modal.append(closeBtn, panel);
264
+ overlay.appendChild(modal);
265
+ doc.body.appendChild(overlay);
266
+ const handle = mountLoginPanel(panel, client, opts);
267
+ const close = () => {
268
+ handle.destroy();
269
+ overlay.remove();
270
+ };
271
+ closeBtn.addEventListener("click", close);
272
+ overlay.addEventListener("click", (e) => {
273
+ if (e.target === overlay)
274
+ close();
275
+ });
276
+ return { close, destroy: close };
277
+ }
278
+ //# sourceMappingURL=login-ui.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-ui.js","sourceRoot":"","sources":["../src/login-ui.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Freemium paywall helper (design.md §7.2 reference implementation).
3
+ *
4
+ * Products define their own free/pro boundary; this module only translates
5
+ * entitlements into a gate decision and an upgrade action. Keep product-specific
6
+ * limits in the product, not here.
7
+ *
8
+ * Usage (web):
9
+ * import { createPaywall } from "@pinnadash/account-sdk/paywall";
10
+ * const paywall = createPaywall({ account, freeLimit: 10 });
11
+ * const decision = await paywall.checkBatch(imageCount);
12
+ * if (!decision.allowed) showUpgradeDialog(decision.reason);
13
+ */
14
+ import type { AccountClient, Entitlements } from "./index.js";
15
+ export interface PaywallOptions {
16
+ account: AccountClient;
17
+ /** Free tier batch limit (design.md suggestion: 10). Product may override. */
18
+ freeLimit?: number;
19
+ /** Called when the user should see the upgrade UI. */
20
+ onUpgradePrompt?: (reason: UpgradeReason) => void;
21
+ }
22
+ export type UpgradeReason = "batch_limit" | "pro_feature";
23
+ export interface GateDecision {
24
+ allowed: boolean;
25
+ reason?: UpgradeReason;
26
+ /** What the user sees, e.g. "Free plan allows 10 images per batch". */
27
+ message: string;
28
+ }
29
+ export declare function createPaywall(opts: PaywallOptions): {
30
+ /** Sync entitlements into the paywall (call after login / on app start). */
31
+ sync(force?: boolean): Promise<Entitlements>;
32
+ isMember(): boolean;
33
+ /** Batch-size gate (design.md §7.2: free ≤ 10 images/batch). */
34
+ checkBatch(count: number): Promise<GateDecision>;
35
+ /** Feature gate (batch rename templates, watermark presets, etc.). */
36
+ checkFeature(featureName: string): GateDecision;
37
+ /** Deep link to the Account Center pricing page. */
38
+ upgradeUrl(): string;
39
+ };
40
+ //# sourceMappingURL=paywall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paywall.d.ts","sourceRoot":"","sources":["../src/paywall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,YAAY,EAAE,MAAM,YAAY,CAAC;AAEpF,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,aAAa,CAAC;IACvB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;CACnD;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,aAAa,CAAC;AAE1D,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,cAAc;IAK9C,4EAA4E;IACtE,IAAI,mBAAiB,OAAO,CAAC,YAAY,CAAC;IAKhD,QAAQ,IAAI,OAAO;IAInB,gEAAgE;IAC1D,UAAU,QAAQ,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAetD,sEAAsE;IACtE,YAAY,cAAc,MAAM,GAAG,YAAY;IAa/C,oDAAoD;IACpD,UAAU,IAAI,MAAM;EAWvB"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Freemium paywall helper (design.md §7.2 reference implementation).
3
+ *
4
+ * Products define their own free/pro boundary; this module only translates
5
+ * entitlements into a gate decision and an upgrade action. Keep product-specific
6
+ * limits in the product, not here.
7
+ *
8
+ * Usage (web):
9
+ * import { createPaywall } from "@pinnadash/account-sdk/paywall";
10
+ * const paywall = createPaywall({ account, freeLimit: 10 });
11
+ * const decision = await paywall.checkBatch(imageCount);
12
+ * if (!decision.allowed) showUpgradeDialog(decision.reason);
13
+ */
14
+ export function createPaywall(opts) {
15
+ const freeLimit = opts.freeLimit ?? 10;
16
+ let cached = null;
17
+ return {
18
+ /** Sync entitlements into the paywall (call after login / on app start). */
19
+ async sync(force = false) {
20
+ cached = await opts.account.getEntitlements(force);
21
+ return cached;
22
+ },
23
+ isMember() {
24
+ return cached?.member ?? false;
25
+ },
26
+ /** Batch-size gate (design.md §7.2: free ≤ 10 images/batch). */
27
+ async checkBatch(count) {
28
+ const ent = cached ?? (await this.sync());
29
+ if (ent.member)
30
+ return { allowed: true, message: "Member: unlimited batch processing." };
31
+ if (count <= freeLimit) {
32
+ return { allowed: true, message: `Free plan: ${count}/${freeLimit} images.` };
33
+ }
34
+ const decision = {
35
+ allowed: false,
36
+ reason: "batch_limit",
37
+ message: `Free plan allows ${freeLimit} images per batch. Upgrade to Pro for unlimited batches.`,
38
+ };
39
+ opts.onUpgradePrompt?.(decision.reason);
40
+ return decision;
41
+ },
42
+ /** Feature gate (batch rename templates, watermark presets, etc.). */
43
+ checkFeature(featureName) {
44
+ if (this.isMember()) {
45
+ return { allowed: true, message: `${featureName} available.` };
46
+ }
47
+ const decision = {
48
+ allowed: false,
49
+ reason: "pro_feature",
50
+ message: `${featureName} is a Pro feature. Upgrade to unlock it.`,
51
+ };
52
+ opts.onUpgradePrompt?.("pro_feature");
53
+ return decision;
54
+ },
55
+ /** Deep link to the Account Center pricing page. */
56
+ upgradeUrl() {
57
+ const inner = opts.account;
58
+ const centerUrl = inner.opts?.centerUrl;
59
+ // 订阅归因(design.md §4.1):带上本产品 client_id,定价页转进 checkout customData
60
+ const slug = inner.opts?.productSlug;
61
+ const suffix = slug ? `?client_id=${encodeURIComponent(slug)}` : "";
62
+ return `${centerUrl ?? ""}/pricing/${suffix}`;
63
+ },
64
+ };
65
+ }
66
+ //# sourceMappingURL=paywall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paywall.js","sourceRoot":"","sources":["../src/paywall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAqBH,MAAM,UAAU,aAAa,CAAC,IAAoB;IAChD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC;IACvC,IAAI,MAAM,GAAwB,IAAI,CAAC;IAEvC,OAAO;QACL,4EAA4E;QAC5E,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK;YACtB,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;YACnD,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,QAAQ;YACN,OAAO,MAAM,EAAE,MAAM,IAAI,KAAK,CAAC;QACjC,CAAC;QAED,gEAAgE;QAChE,KAAK,CAAC,UAAU,CAAC,KAAa;YAC5B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;YAC1C,IAAI,GAAG,CAAC,MAAM;gBAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,qCAAqC,EAAE,CAAC;YACzF,IAAI,KAAK,IAAI,SAAS,EAAE,CAAC;gBACvB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,KAAK,IAAI,SAAS,UAAU,EAAE,CAAC;YAChF,CAAC;YACD,MAAM,QAAQ,GAAiB;gBAC7B,OAAO,EAAE,KAAK;gBACd,MAAM,EAAE,aAAa;gBACrB,OAAO,EAAE,oBAAoB,SAAS,0DAA0D;aACjG,CAAC;YACF,IAAI,CAAC,eAAe,EAAE,CAAC,QAAQ,CAAC,MAAO,CAAC,CAAC;YACzC,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,sEAAsE;QACtE,YAAY,CAAC,WAAmB;YAC9B,IAAI,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;gBACpB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,WAAW,aAAa,EAAE,CAAC;YACjE,CAAC;YACD,MAAM,QAAQ,GAAiB;gBAC7B,OAAO,EAAE,KAAK;gBACd,MAAM,EAAE,aAAa;gBACrB,OAAO,EAAE,GAAG,WAAW,0CAA0C;aAClE,CAAC;YACF,IAAI,CAAC,eAAe,EAAE,CAAC,aAAa,CAAC,CAAC;YACtC,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,oDAAoD;QACpD,UAAU;YACR,MAAM,KAAK,GAAG,IAAI,CAAC,OAElB,CAAC;YACF,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,EAAE,SAAS,CAAC;YACxC,iEAAiE;YACjE,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,WAAW,CAAC;YACrC,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,cAAc,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACpE,OAAO,GAAG,SAAS,IAAI,EAAE,YAAY,MAAM,EAAE,CAAC;QAChD,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@pinnadash/account-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Pinnadash account system client SDK: PKCE login, deep-link callback, token refresh, offline grace, entitlements, paywall. Zero dependencies, Web Crypto + fetch, browser / Workers / Tauri webview.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./desktop": {
14
+ "types": "./dist/desktop.d.ts",
15
+ "default": "./dist/desktop.js"
16
+ },
17
+ "./paywall": {
18
+ "types": "./dist/paywall.d.ts",
19
+ "default": "./dist/paywall.js"
20
+ },
21
+ "./login-ui": {
22
+ "types": "./dist/login-ui.d.ts",
23
+ "default": "./dist/login-ui.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "INTEGRATION.md"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.build.json",
33
+ "prepublishOnly": "npm run build && npm test",
34
+ "test": "vitest run",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "registry": "https://registry.npmjs.org"
40
+ },
41
+ "keywords": [
42
+ "pinnadash",
43
+ "account",
44
+ "pkce",
45
+ "entitlements",
46
+ "subscription"
47
+ ],
48
+ "license": "GPL-3.0",
49
+ "devDependencies": {
50
+ "jsdom": "^27.0.0",
51
+ "typescript": "^7.0.2",
52
+ "vitest": "^5.0.0"
53
+ }
54
+ }