@travelclw/proof-protocol 0.1.1 → 0.1.2

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 CHANGED
@@ -49,11 +49,15 @@ const proofConfig = resolveProofServerConfig()
49
49
 
50
50
  环境变量中的非空值优先于调用方代码配置。缺少 TTL、时间窗口或 shadow 开关时使用协议默认值;显式配置非法值会直接报错,不会静默回退。
51
51
 
52
+ `verifyRequestProof()` 只负责验签、请求绑定和时间计算,过期/未来时间通过 `timeWarning` 返回;是否在 `enforce` 模式拦截、如何在 `shadow` 模式审计,以及如何用 Redis `SET NX EX` 原子消费 `jti`,由服务端适配层负责。
53
+
52
54
  ## 安全边界
53
55
 
54
56
  - 不要把 `.env`、真实密钥、Token、Cookie、私钥或生产配置提交到源码或发布到 npm。
55
57
  - `AUTH_PROOF_DEVICE_SESSION_SECRET` 只用于服务端设备会话摘要,不得暴露给浏览器。
56
58
  - 浏览器 P-256 私钥由 WebCrypto 创建并以不可导出 `CryptoKey` 保存到 IndexedDB。
59
+ - 同一浏览器应用的多个标签页共享同一个 IndexedDB 记录;`reset()` 会通过 `BroadcastChannel` 通知其它标签页清空内存中的进行中操作,并在每次读取/签名前重新核对 IndexedDB 当前 `kid`。不支持 `BroadcastChannel` 时仍以 IndexedDB 核对为准。
60
+ - `proofKeyPromise` 只缓存进行中的密钥创建操作,不缓存已完成的密钥 Promise;标签页休眠或漏收广播时,下一次签名仍会从 IndexedDB 发现 Key 已轮换。
57
61
  - 包只提供协议能力;会话存储、Redis 原子操作、Guard 策略和业务路由由使用方负责。
58
62
 
59
63
  ## 发布内容
package/browser.js CHANGED
@@ -60,6 +60,7 @@ const createBrowserProofClient = options => {
60
60
  const required = () => isRequestProofRequired(options?.required?.())
61
61
  let proofKeyPromise = null
62
62
  let resetPromise = null
63
+ let resetChannel = null
63
64
 
64
65
  if (!databaseName || !storeName || !recordId) {
65
66
  throw new Error('request_proof_storage_failed')
@@ -149,27 +150,65 @@ const createBrowserProofClient = options => {
149
150
  }
150
151
  }
151
152
 
153
+ const getResetChannelName = () =>
154
+ `@travelclw/proof-protocol/reset/${databaseName}/${storeName}/${recordId}`
155
+
156
+ const invalidateLocalKey = () => {
157
+ proofKeyPromise = null
158
+ }
159
+
160
+ if (typeof globalThis.BroadcastChannel === 'function') {
161
+ try {
162
+ resetChannel = new globalThis.BroadcastChannel(getResetChannelName())
163
+ resetChannel.onmessage = event => {
164
+ if (event?.data?.type === 'request-proof-reset') invalidateLocalKey()
165
+ }
166
+ if (typeof resetChannel.unref === 'function') resetChannel.unref()
167
+ } catch {
168
+ resetChannel = null
169
+ }
170
+ }
171
+
172
+ const broadcastReset = () => {
173
+ try {
174
+ resetChannel?.postMessage({ type: 'request-proof-reset' })
175
+ } catch {
176
+ // IndexedDB verification below remains authoritative when the channel is unavailable.
177
+ }
178
+ }
179
+
152
180
  const getOrCreateKey = () => {
153
181
  if (!proofKeyPromise) {
154
182
  const pendingReset = resetPromise
155
- proofKeyPromise = (async () => {
183
+ const operation = (async () => {
156
184
  await pendingReset
157
185
  const storedKey = await readStoredKey()
158
186
  if (isStoredKeyValid(storedKey)) return storedKey
159
187
  return storeIfAbsent(await generateCandidate())
160
- })().catch(error => {
161
- proofKeyPromise = null
162
- throw error
188
+ })()
189
+ const pendingKey = operation.finally(() => {
190
+ if (proofKeyPromise === pendingKey) proofKeyPromise = null
163
191
  })
192
+ proofKeyPromise = pendingKey
164
193
  }
165
194
  return proofKeyPromise
166
195
  }
167
196
 
197
+ const getCurrentKey = async () => {
198
+ while (true) {
199
+ const key = await getOrCreateKey()
200
+ const storedKey = await readStoredKey()
201
+ if (isStoredKeyValid(storedKey) && storedKey.keyId === key.keyId) return storedKey
202
+ invalidateLocalKey()
203
+ }
204
+ }
205
+
168
206
  const reset = () => {
169
207
  const pendingReset = resetPromise
170
208
  const operation = (async () => {
209
+ broadcastReset()
171
210
  const pendingKey = proofKeyPromise
172
- proofKeyPromise = null
211
+ invalidateLocalKey()
173
212
  await pendingReset?.catch(() => undefined)
174
213
  await pendingKey?.catch(() => undefined)
175
214
  if (!globalThis.indexedDB) return
@@ -195,7 +234,7 @@ const createBrowserProofClient = options => {
195
234
  }
196
235
 
197
236
  const getRegistration = async () => {
198
- const key = await getOrCreateKey()
237
+ const key = await getCurrentKey()
199
238
  return { keyId: key.keyId, publicKey: key.publicKey }
200
239
  }
201
240
 
@@ -217,13 +256,13 @@ const createBrowserProofClient = options => {
217
256
  const createProof = async input => {
218
257
  const token = String(input?.token || '').trim()
219
258
  if (!token) return ''
220
- const key = await getOrCreateKey()
221
259
  const requestId = String(input?.requestId || '').trim()
222
260
  const [ath, qsh, bth] = await Promise.all([
223
261
  sha256Base64Url(token),
224
262
  sha256Base64Url(canonicalizeQuery(input?.requestUri)),
225
263
  sha256Base64Url(canonicalizeBody(input?.body, input?.contentType, input?.hasBody)),
226
264
  ])
265
+ const key = await getCurrentKey()
227
266
  const header = { alg: 'ES256', kid: key.keyId, typ: 'dpop+jwt' }
228
267
  const payload = {
229
268
  ath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travelclw/proof-protocol",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Framework-independent browser and Node.js request Proof protocol",
5
5
  "license": "MIT",
6
6
  "author": "travelclw",
package/server.js CHANGED
@@ -54,8 +54,8 @@ const getRequestProofReasonText = reason =>
54
54
  REQUEST_PROOF_REASON_TEXT[String(reason || '')] || '未知请求 Proof 校验失败原因'
55
55
 
56
56
  const safeEqual = (left, right) => {
57
- const leftBuffer = Buffer.from(left)
58
- const rightBuffer = Buffer.from(right)
57
+ const leftBuffer = Buffer.from(String(left ?? ''))
58
+ const rightBuffer = Buffer.from(String(right ?? ''))
59
59
  return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer)
60
60
  }
61
61