@travelclw/proof-protocol 0.1.0 → 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 -1
- package/browser.js +74 -23
- package/node.d.ts +0 -2
- package/package.json +1 -1
- package/server.js +2 -22
package/README.md
CHANGED
|
@@ -36,7 +36,6 @@ pnpm add jose@^5
|
|
|
36
36
|
- `AUTH_PROOF_DEVICE_SESSION_SECRET`
|
|
37
37
|
- `AUTH_PROOF_DEVICE_SESSION_TTL_SECONDS`
|
|
38
38
|
- `AUTH_PROOF_TIME_TOLERANCE_SECONDS`
|
|
39
|
-
- `AUTH_PROOF_SHADOW_BLOCK_MISSING_PROOF=0|1`
|
|
40
39
|
|
|
41
40
|
服务端应在启动阶段调用 `resolveProofServerConfig()`:
|
|
42
41
|
|
|
@@ -50,11 +49,15 @@ const proofConfig = resolveProofServerConfig()
|
|
|
50
49
|
|
|
51
50
|
环境变量中的非空值优先于调用方代码配置。缺少 TTL、时间窗口或 shadow 开关时使用协议默认值;显式配置非法值会直接报错,不会静默回退。
|
|
52
51
|
|
|
52
|
+
`verifyRequestProof()` 只负责验签、请求绑定和时间计算,过期/未来时间通过 `timeWarning` 返回;是否在 `enforce` 模式拦截、如何在 `shadow` 模式审计,以及如何用 Redis `SET NX EX` 原子消费 `jti`,由服务端适配层负责。
|
|
53
|
+
|
|
53
54
|
## 安全边界
|
|
54
55
|
|
|
55
56
|
- 不要把 `.env`、真实密钥、Token、Cookie、私钥或生产配置提交到源码或发布到 npm。
|
|
56
57
|
- `AUTH_PROOF_DEVICE_SESSION_SECRET` 只用于服务端设备会话摘要,不得暴露给浏览器。
|
|
57
58
|
- 浏览器 P-256 私钥由 WebCrypto 创建并以不可导出 `CryptoKey` 保存到 IndexedDB。
|
|
59
|
+
- 同一浏览器应用的多个标签页共享同一个 IndexedDB 记录;`reset()` 会通过 `BroadcastChannel` 通知其它标签页清空内存中的进行中操作,并在每次读取/签名前重新核对 IndexedDB 当前 `kid`。不支持 `BroadcastChannel` 时仍以 IndexedDB 核对为准。
|
|
60
|
+
- `proofKeyPromise` 只缓存进行中的密钥创建操作,不缓存已完成的密钥 Promise;标签页休眠或漏收广播时,下一次签名仍会从 IndexedDB 发现 Key 已轮换。
|
|
58
61
|
- 包只提供协议能力;会话存储、Redis 原子操作、Guard 策略和业务路由由使用方负责。
|
|
59
62
|
|
|
60
63
|
## 发布内容
|
package/browser.js
CHANGED
|
@@ -59,6 +59,8 @@ const createBrowserProofClient = options => {
|
|
|
59
59
|
const recordId = String(options?.recordId || 'request-proof-key').trim()
|
|
60
60
|
const required = () => isRequestProofRequired(options?.required?.())
|
|
61
61
|
let proofKeyPromise = null
|
|
62
|
+
let resetPromise = null
|
|
63
|
+
let resetChannel = null
|
|
62
64
|
|
|
63
65
|
if (!databaseName || !storeName || !recordId) {
|
|
64
66
|
throw new Error('request_proof_storage_failed')
|
|
@@ -148,42 +150,91 @@ const createBrowserProofClient = options => {
|
|
|
148
150
|
}
|
|
149
151
|
}
|
|
150
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
|
+
|
|
151
180
|
const getOrCreateKey = () => {
|
|
152
181
|
if (!proofKeyPromise) {
|
|
153
|
-
|
|
182
|
+
const pendingReset = resetPromise
|
|
183
|
+
const operation = (async () => {
|
|
184
|
+
await pendingReset
|
|
154
185
|
const storedKey = await readStoredKey()
|
|
155
186
|
if (isStoredKeyValid(storedKey)) return storedKey
|
|
156
187
|
return storeIfAbsent(await generateCandidate())
|
|
157
|
-
})()
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
})()
|
|
189
|
+
const pendingKey = operation.finally(() => {
|
|
190
|
+
if (proofKeyPromise === pendingKey) proofKeyPromise = null
|
|
160
191
|
})
|
|
192
|
+
proofKeyPromise = pendingKey
|
|
161
193
|
}
|
|
162
194
|
return proofKeyPromise
|
|
163
195
|
}
|
|
164
196
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const database = await openDatabase()
|
|
172
|
-
try {
|
|
173
|
-
await new Promise((resolve, reject) => {
|
|
174
|
-
const transaction = database.transaction(storeName, 'readwrite')
|
|
175
|
-
transaction.objectStore(storeName).delete(recordId)
|
|
176
|
-
transaction.oncomplete = () => resolve()
|
|
177
|
-
transaction.onerror = () => reject(new Error('request_proof_reset_failed'))
|
|
178
|
-
transaction.onabort = () => reject(new Error('request_proof_reset_failed'))
|
|
179
|
-
})
|
|
180
|
-
} finally {
|
|
181
|
-
database.close()
|
|
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()
|
|
182
203
|
}
|
|
183
204
|
}
|
|
184
205
|
|
|
206
|
+
const reset = () => {
|
|
207
|
+
const pendingReset = resetPromise
|
|
208
|
+
const operation = (async () => {
|
|
209
|
+
broadcastReset()
|
|
210
|
+
const pendingKey = proofKeyPromise
|
|
211
|
+
invalidateLocalKey()
|
|
212
|
+
await pendingReset?.catch(() => undefined)
|
|
213
|
+
await pendingKey?.catch(() => undefined)
|
|
214
|
+
if (!globalThis.indexedDB) return
|
|
215
|
+
|
|
216
|
+
const database = await openDatabase()
|
|
217
|
+
try {
|
|
218
|
+
await new Promise((resolve, reject) => {
|
|
219
|
+
const transaction = database.transaction(storeName, 'readwrite')
|
|
220
|
+
transaction.objectStore(storeName).delete(recordId)
|
|
221
|
+
transaction.oncomplete = () => resolve()
|
|
222
|
+
transaction.onerror = () => reject(new Error('request_proof_reset_failed'))
|
|
223
|
+
transaction.onabort = () => reject(new Error('request_proof_reset_failed'))
|
|
224
|
+
})
|
|
225
|
+
} finally {
|
|
226
|
+
database.close()
|
|
227
|
+
}
|
|
228
|
+
})()
|
|
229
|
+
|
|
230
|
+
resetPromise = operation
|
|
231
|
+
return operation.finally(() => {
|
|
232
|
+
if (resetPromise === operation) resetPromise = null
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
185
236
|
const getRegistration = async () => {
|
|
186
|
-
const key = await
|
|
237
|
+
const key = await getCurrentKey()
|
|
187
238
|
return { keyId: key.keyId, publicKey: key.publicKey }
|
|
188
239
|
}
|
|
189
240
|
|
|
@@ -205,13 +256,13 @@ const createBrowserProofClient = options => {
|
|
|
205
256
|
const createProof = async input => {
|
|
206
257
|
const token = String(input?.token || '').trim()
|
|
207
258
|
if (!token) return ''
|
|
208
|
-
const key = await getOrCreateKey()
|
|
209
259
|
const requestId = String(input?.requestId || '').trim()
|
|
210
260
|
const [ath, qsh, bth] = await Promise.all([
|
|
211
261
|
sha256Base64Url(token),
|
|
212
262
|
sha256Base64Url(canonicalizeQuery(input?.requestUri)),
|
|
213
263
|
sha256Base64Url(canonicalizeBody(input?.body, input?.contentType, input?.hasBody)),
|
|
214
264
|
])
|
|
265
|
+
const key = await getCurrentKey()
|
|
215
266
|
const header = { alg: 'ES256', kid: key.keyId, typ: 'dpop+jwt' }
|
|
216
267
|
const payload = {
|
|
217
268
|
ath,
|
package/node.d.ts
CHANGED
|
@@ -41,7 +41,6 @@ export type ProofServerConfig = {
|
|
|
41
41
|
deviceSessionSecret: string
|
|
42
42
|
deviceSessionTtlSeconds: number
|
|
43
43
|
timeToleranceSeconds: number
|
|
44
|
-
shadowBlockMissingProof: boolean
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
export const REQUEST_PROOF_REASON_TEXT: Readonly<Record<string, string>>
|
|
@@ -72,7 +71,6 @@ export function resolveProofServerConfig(options?: {
|
|
|
72
71
|
deviceSessionSecret?: string
|
|
73
72
|
deviceSessionTtlSeconds?: number
|
|
74
73
|
timeToleranceSeconds?: number
|
|
75
|
-
shadowBlockMissingProof?: boolean
|
|
76
74
|
}): ProofServerConfig
|
|
77
75
|
|
|
78
76
|
export { RequestProofValidationError } from './core'
|
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
|
|
|
@@ -238,15 +238,6 @@ const integerSetting = (value, fallback, name, minimum, maximum) => {
|
|
|
238
238
|
return parsed
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
const booleanSetting = (value, fallback, name) => {
|
|
242
|
-
if (value === undefined || String(value).trim() === '') return fallback
|
|
243
|
-
const normalized = String(value).trim()
|
|
244
|
-
if (normalized !== '0' && normalized !== '1') {
|
|
245
|
-
throw new Error(`${name} must be 0 or 1`)
|
|
246
|
-
}
|
|
247
|
-
return normalized === '1'
|
|
248
|
-
}
|
|
249
|
-
|
|
250
241
|
const resolveProofServerConfig = options => {
|
|
251
242
|
const env = options?.env ?? (typeof process !== 'undefined' ? process.env : {})
|
|
252
243
|
const mode = normalizeProofMode(readOverride(env, 'AUTH_PROOF_MODE', options?.mode))
|
|
@@ -270,16 +261,6 @@ const resolveProofServerConfig = options => {
|
|
|
270
261
|
30,
|
|
271
262
|
DEFAULT_REQUEST_PROOF_TIME_TOLERANCE_SECONDS,
|
|
272
263
|
)
|
|
273
|
-
const shadowBlockMissingProof = booleanSetting(
|
|
274
|
-
readOverride(
|
|
275
|
-
env,
|
|
276
|
-
'AUTH_PROOF_SHADOW_BLOCK_MISSING_PROOF',
|
|
277
|
-
options?.shadowBlockMissingProof ? '1' : '0',
|
|
278
|
-
),
|
|
279
|
-
false,
|
|
280
|
-
'AUTH_PROOF_SHADOW_BLOCK_MISSING_PROOF',
|
|
281
|
-
)
|
|
282
|
-
|
|
283
264
|
if (mode !== 'off' && deviceSessionSecret.length < 32) {
|
|
284
265
|
throw new Error(
|
|
285
266
|
'AUTH_PROOF_DEVICE_SESSION_SECRET is required when AUTH_PROOF_MODE is shadow or enforce and must contain at least 32 characters',
|
|
@@ -290,7 +271,6 @@ const resolveProofServerConfig = options => {
|
|
|
290
271
|
deviceSessionSecret,
|
|
291
272
|
deviceSessionTtlSeconds,
|
|
292
273
|
timeToleranceSeconds,
|
|
293
|
-
shadowBlockMissingProof,
|
|
294
274
|
}
|
|
295
275
|
}
|
|
296
276
|
|