@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 +4 -0
- package/browser.js +46 -7
- package/package.json +1 -1
- package/server.js +2 -2
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
|
-
|
|
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
|
-
})()
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
|
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
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
|
|