stable-deviceid 1.0.0 → 1.0.1

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 (3) hide show
  1. package/README.md +89 -84
  2. package/README.zh-CN.md +197 -0
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,34 +1,32 @@
1
1
  # stable-deviceid
2
2
 
3
- 跨端一致的现代浏览器设备身份库。为 Web 应用提供**稳定、可自愈、隐私友好**的设备标识:
4
- 结构化稳定设备 ID(生成 / 校验 / 自愈)、canvas + WebGL 设备指纹(默认关闭)、
5
- HTTP 响应头身份同步、SSO 跨 origin 身份归一、axios 一站式接入。
3
+ [English](./README.md) | [简体中文](./README.zh-CN.md)
6
4
 
7
- > 设计目标:设备 ID 一经生成终身复用——用户清除缓存可自动恢复(httpOnly cookie 兜底)、
8
- > 时钟偏差不引起身份漂移(±5 分钟容差)、多标签页实时同步、同设备跨应用(SSO iframe)归一为同一身份。
5
+ Stable, self-healing device identity for modern web applications.
9
6
 
10
- ## 特性
7
+ `stable-deviceid` gives your web app a **durable device identifier** that survives the things that normally break it: storage cleared by the user or by Safari ITP, clock skew between client and server, multi-tab races, and per-origin identity fragmentation in iframe-based SSO. It ships with an optional, privacy-first device fingerprint and a one-line axios integration.
11
8
 
12
- - 🆔 **结构化设备 ID**:`{PLATFORM}-{ENCODED_TS}-{RANDOM}`(如 `WEB-DaBOSbNdSuc-8s4T`),
13
- 时间戳位混淆 + Base62 编码 + 高熵随机后缀(`crypto.getRandomValues` 拒绝采样)
14
- - 🔁 **自愈能力**:损坏 / 老格式 / 过期 / 未来时间的存量 ID 自动重生;服务端校验失败自动收敛(一轮往返)
15
- - 🔄 **响应头同步**:服务端可下发权威 ID(`X-Device-Id`),客户端写回后下一请求立即生效
16
- - 🧩 **SSO 跨 origin 归一**:oauth21 式 iframe 登录场景,子应用采纳权威域 ID,同物理设备全端同身份
17
- - 🛡 **安全内建**:与后端校验规则逐条对齐、脏值拒绝、超长头忽略、日志截断、SSO 消息双重校验
18
- - 🤫 **隐私友好**:设备指纹默认关闭,需显式启用;存储层静默降级,不抛异常
19
- - 📦 **零依赖**:ESM-only、tree-shaking 友好(`sideEffects: false`)、TypeScript 类型完备
9
+ ## Features
20
10
 
21
- ## 安装
11
+ - **Structured device ID** — `{PLATFORM}-{ENCODED_TS}-{RANDOM}` (e.g. `WEB-DaBOSbNdSuc-8s4T`): obfuscated millisecond timestamp (64-bit magic XOR → Base62), 6-char high-entropy random suffix via `crypto.getRandomValues` with rejection sampling
12
+ - **Self-healing** — corrupted / legacy-format / expired / future-dated stored IDs are regenerated locally; server-side rejection converges within one round trip
13
+ - **Response-header sync** — the backend can push an authoritative ID via `X-Device-Id`; the client persists it and the very next request uses it
14
+ - **SSO identity unification** — in iframe-based SSO, child apps adopt the authoritative ID from the identity domain so one physical device maps to one identity everywhere
15
+ - **Security built in** — local validation mirrors the backend rule-by-rule (platform enum, lengths, Base62 charset, future-time rejection with ±5 min clock-skew tolerance, 365-day max age), dirty-value rejection, oversized-header filtering, log truncation
16
+ - **Privacy-friendly** — canvas + WebGL fingerprinting is **off by default**; storage access degrades silently to an in-memory fallback
17
+ - **Zero dependencies** — ESM-only, tree-shakable (`sideEffects: false`), full TypeScript types, 93 unit tests
18
+
19
+ ## Installation
22
20
 
23
21
  ```bash
24
22
  npm install stable-deviceid
25
23
  ```
26
24
 
27
- 要求 Node.js >= 18(仅构建环境;运行时为现代浏览器)。
25
+ Requires Node.js >= 18 (build tooling only; runtime targets modern browsers).
28
26
 
29
- ## 快速开始
27
+ ## Quick Start
30
28
 
31
- ### axios 一站式接入(推荐)
29
+ ### One-line axios integration (recommended)
32
30
 
33
31
  ```ts
34
32
  import axios from 'axios';
@@ -38,26 +36,26 @@ const http = axios.create({ baseURL: '/api', withCredentials: true });
38
36
  setupDeviceSync(http);
39
37
  ```
40
38
 
41
- `setupDeviceSync` 自动完成:
39
+ `setupDeviceSync` registers everything for you:
42
40
 
43
- 1. 请求拦截器注入 `x-device-id` 头(内存缓存直读,近乎零开销)
44
- 2. 按需注入设备指纹头 `x-device-fp`(默认按环境判定,见下方指纹开关)
45
- 3. 响应拦截器同步服务端下发的设备 ID(写回存储 + 失效内存缓存)
46
- 4. 注册跨标签页 storage 监听(幂等)
41
+ 1. A request interceptor injecting the `x-device-id` header (served from an in-memory cache — effectively zero overhead)
42
+ 2. The device-fingerprint header `x-device-fp` when enabled (see [Device Fingerprinting](#device-fingerprinting))
43
+ 3. A response interceptor syncing the device ID pushed via response headers
44
+ 4. Cross-tab synchronization via the `storage` event (idempotent)
47
45
 
48
46
  ```ts
49
- // 可选配置
47
+ // Options
50
48
  setupDeviceSync(http, {
51
- fingerprint: true, // 强制开启指纹(默认按 meta/env 判定)
52
- onDeviceIdChange: (oldId, newId) => { /* 设备 ID 变更回调 */ }
49
+ fingerprint: true, // force-enable fingerprinting (default: environment-based)
50
+ onDeviceIdChange: (oldId, newId) => { /* react to identity changes */ }
53
51
  });
54
52
 
55
- // dispose:卸载拦截器(HMR / 单测场景)
53
+ // Disposal for HMR / tests
56
54
  const dispose = setupDeviceSync(http);
57
55
  dispose();
58
56
  ```
59
57
 
60
- ### 原生 fetch / 显式带头
58
+ ### Native fetch / explicit headers
61
59
 
62
60
  ```ts
63
61
  import { getDeviceHeaders } from 'stable-deviceid';
@@ -65,15 +63,15 @@ import { getDeviceHeaders } from 'stable-deviceid';
65
63
  fetch('/api/profile', { headers: { ...getDeviceHeaders() } });
66
64
  ```
67
65
 
68
- ### 手动拦截器(细粒度控制)
66
+ ### Manual interceptors (fine-grained control)
69
67
 
70
68
  ```ts
71
69
  import {
72
- getStableDeviceId, // 稳定设备 ID(持久化 + 内存缓存)
73
- getDeviceFingerprint, // 设备指纹(异步,32 hex
74
- isDeviceFingerprintEnabled, // 指纹开关判定
75
- handleDeviceSyncInResponse, // 响应头同步(传 axios response
76
- initDeviceSync // 全局初始化(跨标签页监听,幂等)
70
+ getStableDeviceId, // stable device ID (persisted + in-memory cache)
71
+ getDeviceFingerprint, // fingerprint (async, 32-char hex)
72
+ isDeviceFingerprintEnabled, // fingerprint toggle
73
+ handleDeviceSyncInResponse, // response-header sync (pass the axios response)
74
+ initDeviceSync // global init (cross-tab listener, idempotent)
77
75
  } from 'stable-deviceid';
78
76
 
79
77
  http.interceptors.request.use(async config => {
@@ -81,7 +79,7 @@ http.interceptors.request.use(async config => {
81
79
  if (isDeviceFingerprintEnabled()) {
82
80
  try {
83
81
  config.headers['x-device-fp'] = await getDeviceFingerprint();
84
- } catch { /* 采集失败不影响主流程 */ }
82
+ } catch { /* fingerprinting must never break the request */ }
85
83
  }
86
84
  return config;
87
85
  });
@@ -92,76 +90,83 @@ http.interceptors.response.use(response => {
92
90
  });
93
91
  ```
94
92
 
95
- ## API 一览
93
+ ## API Reference
96
94
 
97
- | 导出 | 说明 |
95
+ | Export | Description |
98
96
  | --- | --- |
99
- | `setupDeviceSync(instance, options?)` | 一站式接入(axios),返回 dispose 函数 |
100
- | `getDeviceHeaders()` | 同步返回 `{ 'x-device-id': id }`,供 fetch / 显式带头 |
101
- | `getStableDeviceId()` | 稳定设备 ID(首次生成,之后持久复用) |
102
- | `getCurrentDeviceId()` / `setDeviceId(id)` / `clearDeviceId()` | 读取 / 校验写入 / 清除(均含格式校验) |
103
- | `adoptDeviceId(id)` | 采纳 SSO 权威域下发的 ID(格式 + 平台段双校验,跨 origin 归一) |
104
- | `validateDeviceIdFormat(id)` | 本地格式校验(返回 `{ valid, reason? }`) |
105
- | `parseDeviceId(id)` | 解析 ID 信息(platform / timestamp / age),非法返回 null |
106
- | `syncDeviceFromHeaders(headers, options?)` | 从响应头同步(兼容 Headers / AxiosHeaders / 普通对象) |
107
- | `handleDeviceSyncInResponse(response, options?)` | 响应拦截器集成(axios / fetch Response 均可) |
108
- | `initDeviceSync(options?)` | 全局初始化:跨标签页监听 + 变更回调(幂等) |
109
- | `getDeviceFingerprint()` / `isDeviceFingerprintEnabled()` | 指纹采集(Promise 缓存、并发去重)与开关判定 |
110
- | `sha256(message)` | SHA-256Web Crypto 优先,非安全上下文自动降级纯 JS |
111
- | `getDeviceIdStats()` | 调试统计(当前 ID / 解析信息 / 来源) |
112
-
113
- 常量:`STORAGE_KEY`、`MAX_AGE_DAYS`(365)、`CLOCK_SKEW_TOLERANCE_MS`(±5 分钟)、
114
- `DEVICE_PLATFORMS`、`RANDOM_SUFFIX_LENGTH`。
115
-
116
- ## SSO 跨 origin 身份归一
117
-
118
- iframe 嵌入 SSO 登录页(如 oauth21)时,登录成功消息中携带权威域设备 ID,
119
- 子应用在 bindSession 之前采纳,同物理设备即归一为同一身份:
97
+ | `setupDeviceSync(instance, options?)` | One-line axios integration; returns a dispose function |
98
+ | `getDeviceHeaders()` | Synchronous `{ 'x-device-id': id }` for fetch / explicit headers |
99
+ | `getStableDeviceId()` | Stable device ID (generated once, then reused) |
100
+ | `getCurrentDeviceId()` / `setDeviceId(id)` / `clearDeviceId()` | Read / validated write / clear |
101
+ | `adoptDeviceId(id)` | Adopt an SSO-pushed authoritative ID (format + platform checks, cross-origin unification) |
102
+ | `validateDeviceIdFormat(id)` | Local format validation → `{ valid, reason? }` |
103
+ | `parseDeviceId(id)` | Parse ID metadata (platform / timestamp / age); `null` when invalid |
104
+ | `syncDeviceFromHeaders(headers, options?)` | Sync from response headers (Headers / AxiosHeaders / plain objects) |
105
+ | `handleDeviceSyncInResponse(response, options?)` | Response-interceptor helper (axios and fetch Response) |
106
+ | `initDeviceSync(options?)` | Global init: cross-tab listener + change callback (idempotent) |
107
+ | `getDeviceFingerprint()` / `isDeviceFingerprintEnabled()` | Fingerprint collection (promise-cached, concurrent-safe) and toggle |
108
+ | `sha256(message)` | SHA-256 (Web Crypto first, pure-JS fallback on insecure contexts) |
109
+ | `getDeviceIdStats()` | Debug stats (current ID / parsed info / source) |
110
+
111
+ Constants: `STORAGE_KEY`, `MAX_AGE_DAYS` (365), `CLOCK_SKEW_TOLERANCE_MS` (±5 min), `DEVICE_PLATFORMS`, `RANDOM_SUFFIX_LENGTH`.
112
+
113
+ ## SSO Identity Unification
114
+
115
+ When your app embeds an SSO login page (e.g. an OAuth authorization iframe) in a different origin, each origin would normally hold its own device ID. On login success, have the identity domain push its authoritative ID inside the `LOGIN_SUCCESS` message and adopt it **before** binding the session, so the bind request already carries the unified identity:
120
116
 
121
117
  ```ts
122
118
  window.addEventListener('message', event => {
123
- // 务必校验 event.origin event.source(示例省略)
119
+ // Always verify event.origin and event.source in production (omitted here)
124
120
  if (event.data?.type === 'LOGIN_SUCCESS' && event.data.deviceId) {
125
- adoptDeviceId(event.data.deviceId); // 包内做格式 + 平台段双校验
121
+ adoptDeviceId(event.data.deviceId); // format + platform validation built in
126
122
  }
127
123
  });
128
124
  ```
129
125
 
130
- ## 设备指纹(默认关闭,隐私友好)
126
+ ## Device Fingerprinting (off by default)
131
127
 
132
128
  ```html
133
- <!-- 方式一:后端/模板注入 meta 开关 -->
129
+ <!-- Option 1: server/template-injected meta toggle -->
134
130
  <meta name="device-fp" content="true" />
135
131
  ```
136
132
 
137
133
  ```bash
138
- # 方式二:Vite 环境变量
134
+ # Option 2: Vite environment variable
139
135
  VITE_DEVICE_FINGERPRINT=true
140
136
  ```
141
137
 
142
138
  ```ts
143
- // 方式三:代码强制指定(非 Vite 构建器推荐)
139
+ // Option 3: explicit option (recommended for non-Vite bundlers)
144
140
  setupDeviceSync(http, { fingerprint: true });
145
141
  ```
146
142
 
147
- 采集维度:canvas 渲染差异 + WebGL renderer/vendorSHA-256 取前 32 hex,
148
- 进程内 Promise 缓存(并发去重、双失败返回空串而非常量哈希,防误匹配)。
143
+ Collection combines canvas rendering differences and WebGL renderer/vendor strings, hashed with SHA-256 (first 32 hex chars). The result is promise-cached: concurrent calls are deduplicated and a failed collection (headless / anti-fingerprint browsers) returns an empty string instead of a constant hash, preventing false matches.
144
+
145
+ ## Security Design
146
+
147
+ - **Backend-aligned validation** — local rules mirror the backend `validateDeviceId` one-to-one; illegitimate stored IDs are regenerated locally, avoiding per-request regeneration loops
148
+ - **Dirty-input defense** — response-header values over 128 chars are ignored, external input in logs is truncated, `setDeviceId` rejects malformed writes
149
+ - **Cache coherence** — any identity write invalidates the in-memory cache immediately; the next request carries the new ID
150
+ - **Graceful degradation** — private browsing / storage failures fall back to an in-memory layer without throwing, never breaking the request chain
149
151
 
150
- ## 安全设计
152
+ ## Device ID Format
153
+
154
+ ```
155
+ {PLATFORM}-{ENCODED_TIMESTAMP}-{RANDOM_SUFFIX}
156
+ Example: WEB-DaBOSbNdSuc-8s4T
157
+ ```
151
158
 
152
- - **前后端校验对齐**:格式校验与后端 `validateDeviceId` 逐条一致(平台枚举 /
153
- 长度 / Base62 字符集 / 拒绝超容差未来时间 / 365 天有效期),存量非法 ID 本地即重生
154
- - **脏输入防御**:响应头超 128 字符忽略、日志截断外部输入、`setDeviceId` 入口校验
155
- - **缓存一致性**:任何来源的 ID 写入后立即失效内存缓存,下一请求即生效
156
- - **隐私模式降级**:存储不可用时静默降级内存层(会话内稳定),不抛异常不中断请求链路
159
+ - `PLATFORM` `WEB` / `IOS` / `ANDROID` (detected from the user agent)
160
+ - `ENCODED_TIMESTAMP` (ms timestamp 2024-01-01 offset) XOR 64-bit golden-ratio magic → Base62, fixed 11 chars
161
+ - `RANDOM_SUFFIX` 6-char Base62 (≈35.7 bits of entropy), rejection-sampled
157
162
 
158
- ## nodeServers monorepo 内开发
163
+ ## Using Inside the nodeServers Monorepo
159
164
 
160
- workspace 内前端(oauth21 / firewall / posecraft)通过 vite alias 直连源码消费:
165
+ Frontends in this workspace consume the source directly through a Vite alias (development stays bundler-native):
161
166
 
162
167
  ```ts
163
168
  // vite.config.ts
164
- resolve: { alias: { stable-deviceid: fileURLToPath(new URL('../packages/shared-device/src/index.ts', import.meta.url)) } }
169
+ resolve: { alias: { 'stable-deviceid': fileURLToPath(new URL('../packages/shared-device/src/index.ts', import.meta.url)) } }
165
170
  ```
166
171
 
167
172
  ```json
@@ -170,22 +175,22 @@ resolve: { alias: { stable-deviceid: fileURLToPath(new URL('../packages/shared-d
170
175
  "include": ["src/**/*.ts", "../packages/shared-device/src/**/*.ts"]
171
176
  ```
172
177
 
173
- `package.json` dependencies 中 `"stable-deviceid": "*"` workspace 链接。
178
+ `"stable-deviceid": "*"` in each app's `package.json` links the workspace package.
174
179
 
175
- ## 构建与发布(维护者)
180
+ ## Build & Publish (maintainers)
176
181
 
177
182
  ```bash
178
183
  cd packages/shared-device
179
- npm run build # esbuild 逐模块转换 → dist/ + tsc 产出 index.d.ts
180
- npm publish # prepack 自动构建;ESM-onlyaccess: public
184
+ npm run build # esbuild per-module transform → dist/ + tsc emits dist/index.d.ts
185
+ npm publish # prepack rebuilds automatically; ESM-only, access: public
181
186
  ```
182
187
 
183
- ## 已知限制
188
+ ## Limitations
184
189
 
185
- - 隐私模式下存储降级为会话内临时 ID,每次刷新变化(console.warn 告警)
186
- - `isDeviceFingerprintEnabled` 依赖 `import.meta.env`(Vite)与 DOM;非 Vite 消费方请用 meta 标签或 `fingerprint` 选项
187
- - `crypto.getRandomValues` 不可用的极旧浏览器以 `Math.random` 生成随机后缀(告警提示)
188
- - Safari ITP 会清除脚本可写存储,需配合服务端 httpOnly cookie 兜底恢复机制使用
190
+ - In private browsing, storage falls back to a per-session in-memory ID that changes on reload (a `console.warn` is emitted)
191
+ - `isDeviceFingerprintEnabled` relies on `import.meta.env` (Vite) and the DOM; non-Vite consumers should use the meta tag or the `fingerprint` option
192
+ - On very old browsers without `crypto.getRandomValues`, the random suffix falls back to `Math.random` (warned; affects uniqueness only, not security)
193
+ - Safari ITP clears script-writable storage; pair the library with a server-side httpOnly-cookie recovery mechanism for full continuity
189
194
 
190
195
  ## License
191
196
 
@@ -0,0 +1,197 @@
1
+ # stable-deviceid
2
+
3
+ [English](./README.md) | 简体中文
4
+
5
+ 为现代 Web 应用提供**稳定、可自愈**的设备身份标识。
6
+
7
+ `stable-deviceid` 解决浏览器设备标识在实际生产中的全部痛点:用户或 Safari ITP 清除存储、客户端与服务端时钟偏差、多标签页竞态、iframe SSO 下各 origin 身份分裂。内置隐私友好的设备指纹采集(默认关闭)与 axios 一站式接入,一行代码完成全部配置。
8
+
9
+ ## 特性
10
+
11
+ - **结构化设备 ID** — `{PLATFORM}-{ENCODED_TS}-{RANDOM}`(如 `WEB-DaBOSbNdSuc-8s4T`):毫秒时间戳位混淆(64 位魔数 XOR → Base62)+ 6 字符高熵随机后缀(`crypto.getRandomValues` 拒绝采样)
12
+ - **自愈能力** — 损坏 / 老格式 / 过期 / 未来时间的存量 ID 本地自动重生;服务端校验失败一轮往返即收敛
13
+ - **响应头同步** — 服务端可通过 `X-Device-Id` 下发权威 ID,客户端持久化后下一个请求立即生效
14
+ - **SSO 身份归一** — iframe SSO 场景下子应用采纳身份域的权威 ID,同一物理设备全端同一身份
15
+ - **安全内建** — 本地校验与后端逐条对齐(平台枚举、长度、Base62 字符集、含 ±5 分钟时钟偏差容差的未来时间拒绝、365 天有效期)、脏值拒绝、超长头过滤、日志截断
16
+ - **隐私友好** — canvas + WebGL 指纹**默认关闭**;存储访问静默降级内存层,不抛异常
17
+ - **零依赖** — ESM-only、tree-shaking 友好(`sideEffects: false`)、TypeScript 类型完备、93 个单元测试
18
+
19
+ ## 安装
20
+
21
+ ```bash
22
+ npm install stable-deviceid
23
+ ```
24
+
25
+ 要求 Node.js >= 18(仅构建环境;运行时面向现代浏览器)。
26
+
27
+ ## 快速开始
28
+
29
+ ### axios 一站式接入(推荐)
30
+
31
+ ```ts
32
+ import axios from 'axios';
33
+ import { setupDeviceSync } from 'stable-deviceid';
34
+
35
+ const http = axios.create({ baseURL: '/api', withCredentials: true });
36
+ setupDeviceSync(http);
37
+ ```
38
+
39
+ `setupDeviceSync` 自动完成:
40
+
41
+ 1. 请求拦截器注入 `x-device-id` 头(内存缓存直读,近乎零开销)
42
+ 2. 按需注入设备指纹头 `x-device-fp`(见[设备指纹](#设备指纹默认关闭隐私友好))
43
+ 3. 响应拦截器同步响应头下发的设备 ID
44
+ 4. 跨标签页 storage 监听(幂等)
45
+
46
+ ```ts
47
+ // 可选配置
48
+ setupDeviceSync(http, {
49
+ fingerprint: true, // 强制开启指纹(默认按环境判定)
50
+ onDeviceIdChange: (oldId, newId) => { /* 设备身份变更回调 */ }
51
+ });
52
+
53
+ // 卸载拦截器(HMR / 单测场景)
54
+ const dispose = setupDeviceSync(http);
55
+ dispose();
56
+ ```
57
+
58
+ ### 原生 fetch / 显式带头
59
+
60
+ ```ts
61
+ import { getDeviceHeaders } from 'stable-deviceid';
62
+
63
+ fetch('/api/profile', { headers: { ...getDeviceHeaders() } });
64
+ ```
65
+
66
+ ### 手动拦截器(细粒度控制)
67
+
68
+ ```ts
69
+ import {
70
+ getStableDeviceId, // 稳定设备 ID(持久化 + 内存缓存)
71
+ getDeviceFingerprint, // 指纹(异步,32 位 hex)
72
+ isDeviceFingerprintEnabled, // 指纹开关判定
73
+ handleDeviceSyncInResponse, // 响应头同步(传 axios response)
74
+ initDeviceSync // 全局初始化(跨标签页监听,幂等)
75
+ } from 'stable-deviceid';
76
+
77
+ http.interceptors.request.use(async config => {
78
+ config.headers['x-device-id'] = getStableDeviceId();
79
+ if (isDeviceFingerprintEnabled()) {
80
+ try {
81
+ config.headers['x-device-fp'] = await getDeviceFingerprint();
82
+ } catch { /* 指纹采集绝不阻断请求 */ }
83
+ }
84
+ return config;
85
+ });
86
+
87
+ http.interceptors.response.use(response => {
88
+ handleDeviceSyncInResponse(response);
89
+ return response;
90
+ });
91
+ ```
92
+
93
+ ## API 参考
94
+
95
+ | 导出 | 说明 |
96
+ | --- | --- |
97
+ | `setupDeviceSync(instance, options?)` | axios 一站式接入,返回 dispose 函数 |
98
+ | `getDeviceHeaders()` | 同步返回 `{ 'x-device-id': id }`,供 fetch / 显式带头 |
99
+ | `getStableDeviceId()` | 稳定设备 ID(首次生成,终身复用) |
100
+ | `getCurrentDeviceId()` / `setDeviceId(id)` / `clearDeviceId()` | 读取 / 校验写入 / 清除 |
101
+ | `adoptDeviceId(id)` | 采纳 SSO 权威域 ID(格式 + 平台段双校验,跨 origin 归一) |
102
+ | `validateDeviceIdFormat(id)` | 本地格式校验 → `{ valid, reason? }` |
103
+ | `parseDeviceId(id)` | 解析 ID 元信息(platform / timestamp / age),非法返回 null |
104
+ | `syncDeviceFromHeaders(headers, options?)` | 响应头同步(兼容 Headers / AxiosHeaders / 普通对象) |
105
+ | `handleDeviceSyncInResponse(response, options?)` | 响应拦截器集成(axios / fetch Response 均可) |
106
+ | `initDeviceSync(options?)` | 全局初始化:跨标签页监听 + 变更回调(幂等) |
107
+ | `getDeviceFingerprint()` / `isDeviceFingerprintEnabled()` | 指纹采集(Promise 缓存、并发去重)与开关判定 |
108
+ | `sha256(message)` | SHA-256(Web Crypto 优先,非安全上下文自动降级纯 JS) |
109
+ | `getDeviceIdStats()` | 调试统计(当前 ID / 解析信息 / 来源) |
110
+
111
+ 常量:`STORAGE_KEY`、`MAX_AGE_DAYS`(365)、`CLOCK_SKEW_TOLERANCE_MS`(±5 分钟)、`DEVICE_PLATFORMS`、`RANDOM_SUFFIX_LENGTH`。
112
+
113
+ ## SSO 跨 origin 身份归一
114
+
115
+ 当应用以 iframe 嵌入不同 origin 的 SSO 登录页(如 OAuth 授权页)时,各 origin 本会各持一份设备 ID。在登录成功消息中由身份域下发权威 ID,子应用在 **bindSession 之前**采纳,绑定请求即携带统一身份:
116
+
117
+ ```ts
118
+ window.addEventListener('message', event => {
119
+ // 生产环境务必校验 event.origin 与 event.source(此处省略)
120
+ if (event.data?.type === 'LOGIN_SUCCESS' && event.data.deviceId) {
121
+ adoptDeviceId(event.data.deviceId); // 包内做格式 + 平台段双校验
122
+ }
123
+ });
124
+ ```
125
+
126
+ ## 设备指纹(默认关闭,隐私友好)
127
+
128
+ ```html
129
+ <!-- 方式一:服务端/模板注入 meta 开关 -->
130
+ <meta name="device-fp" content="true" />
131
+ ```
132
+
133
+ ```bash
134
+ # 方式二:Vite 环境变量
135
+ VITE_DEVICE_FINGERPRINT=true
136
+ ```
137
+
138
+ ```ts
139
+ // 方式三:显式配置(非 Vite 构建器推荐)
140
+ setupDeviceSync(http, { fingerprint: true });
141
+ ```
142
+
143
+ 采集维度:canvas 渲染差异 + WebGL renderer/vendor,SHA-256 取前 32 位 hex。结果以 Promise 缓存:并发调用去重,采集失败(headless / 反指纹浏览器)返回空串而非常量哈希,防止误匹配。
144
+
145
+ ## 安全设计
146
+
147
+ - **前后端校验对齐** — 本地规则与后端 `validateDeviceId` 逐条一致,非法存量 ID 本地即重生,避免每请求重生循环
148
+ - **脏输入防御** — 响应头超 128 字符忽略、日志截断外部输入、`setDeviceId` 拒绝非法写入
149
+ - **缓存一致性** — 任何身份写入立即失效内存缓存,下一请求即生效
150
+ - **优雅降级** — 隐私模式 / 存储故障静默降级内存层,不抛异常、不中断请求链路
151
+
152
+ ## 设备 ID 格式
153
+
154
+ ```
155
+ {PLATFORM}-{ENCODED_TIMESTAMP}-{RANDOM_SUFFIX}
156
+ 示例:WEB-DaBOSbNdSuc-8s4T
157
+ ```
158
+
159
+ - `PLATFORM` — `WEB` / `IOS` / `ANDROID`(UA 检测)
160
+ - `ENCODED_TIMESTAMP` —(毫秒时间戳 − 2024-01-01 偏移)XOR 64 位黄金比例魔数 → Base62,固定 11 字符
161
+ - `RANDOM_SUFFIX` — 6 字符 Base62(约 35.7 bit 熵),拒绝采样无模偏差
162
+
163
+ ## 在 nodeServers monorepo 内开发
164
+
165
+ workspace 内前端通过 Vite alias 直连源码消费(开发体验保持 bundler 原生):
166
+
167
+ ```ts
168
+ // vite.config.ts
169
+ resolve: { alias: { 'stable-deviceid': fileURLToPath(new URL('../packages/shared-device/src/index.ts', import.meta.url)) } }
170
+ ```
171
+
172
+ ```json
173
+ // tsconfig.json
174
+ "paths": { "stable-deviceid": ["../packages/shared-device/src/index.ts"], "stable-deviceid/*": ["../packages/shared-device/src/*"] },
175
+ "include": ["src/**/*.ts", "../packages/shared-device/src/**/*.ts"]
176
+ ```
177
+
178
+ 各应用 `package.json` 中 `"stable-deviceid": "*"` 供 workspace 链接。
179
+
180
+ ## 构建与发布(维护者)
181
+
182
+ ```bash
183
+ cd packages/shared-device
184
+ npm run build # esbuild 逐模块转换 → dist/ + tsc 产出 dist/index.d.ts
185
+ npm publish # prepack 自动构建;ESM-only,access: public
186
+ ```
187
+
188
+ ## 已知限制
189
+
190
+ - 隐私模式下存储降级为会话内内存 ID,刷新即变(有 `console.warn` 告警)
191
+ - `isDeviceFingerprintEnabled` 依赖 `import.meta.env`(Vite)与 DOM;非 Vite 消费方请用 meta 标签或 `fingerprint` 选项
192
+ - 极旧浏览器无 `crypto.getRandomValues` 时随机后缀降级 `Math.random`(有告警;仅影响唯一性不影响安全性)
193
+ - Safari ITP 会清除脚本可写存储,需配合服务端 httpOnly cookie 兜底恢复机制实现完全连续性
194
+
195
+ ## 许可证
196
+
197
+ [MIT](./LICENSE) © 2026 qirly
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stable-deviceid",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "跨端一致的现代浏览器设备身份库:结构化稳定设备 ID(生成/校验/自愈)、canvas+WebGL 指纹(默认关闭)、响应头身份同步、SSO 跨 origin 归一、axios 一站式接入",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,6 +15,7 @@
15
15
  "files": [
16
16
  "dist",
17
17
  "README.md",
18
+ "README.zh-CN.md",
18
19
  "LICENSE"
19
20
  ],
20
21
  "sideEffects": false,