@travelclw/proof-protocol 0.1.2 → 0.2.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.
Files changed (7) hide show
  1. package/README.md +81 -69
  2. package/browser.d.ts +42 -40
  3. package/browser.js +342 -306
  4. package/core.js +221 -219
  5. package/node.d.ts +80 -76
  6. package/package.json +1 -10
  7. package/server.js +316 -288
package/core.js CHANGED
@@ -1,219 +1,221 @@
1
- 'use strict'
2
-
3
- const REQUEST_PROOF_HEADER = 'x-ctx-proof'
4
- const AUTH_CENTER_PROOF_AUDIENCE = 'auth-center'
5
- const ODM_REQUEST_PROOF_AUDIENCE = 'uweb-odm'
6
- const DEFAULT_REQUEST_PROOF_TIME_TOLERANCE_SECONDS = 2 * 60 * 60
7
-
8
- const REQUEST_PROOF_ERROR_CODES = Object.freeze({
9
- timeMismatch: 'E4601',
10
- requestExpired: 'E4602',
11
- requestUnverifiable: 'E4603',
12
- sessionMismatch: 'E4604',
13
- requestInvalid: 'E4605',
14
- serviceUnavailable: 'E503',
15
- })
16
-
17
- class RequestProofValidationError extends Error {
18
- constructor(code) {
19
- super(code)
20
- this.name = 'RequestProofValidationError'
21
- this.code = code
22
- }
23
- }
24
-
25
- const compareText = (left, right) => (left < right ? -1 : left > right ? 1 : 0)
26
-
27
- const normalizePath = value => {
28
- const rawValue = String(value || '').trim()
29
- if (!rawValue) return ''
30
-
31
- let path = rawValue
32
- if (/^[a-z][a-z\d+.-]*:\/\//i.test(rawValue)) {
33
- try {
34
- path = new URL(rawValue).pathname
35
- } catch {
36
- return ''
37
- }
38
- }
39
-
40
- const withoutQuery = path.split(/[?#]/)[0]
41
- const withLeadingSlash = withoutQuery.startsWith('/') ? withoutQuery : `/${withoutQuery}`
42
- return withLeadingSlash.replace(/\/{2,}/g, '/') || '/'
43
- }
44
-
45
- const canonicalizeQuery = value => {
46
- const rawValue = String(value || '').trim()
47
- if (!rawValue) return ''
48
-
49
- let query = ''
50
- if (/^[a-z][a-z\d+.-]*:\/\//i.test(rawValue)) {
51
- try {
52
- query = new URL(rawValue).search.slice(1)
53
- } catch {
54
- return ''
55
- }
56
- } else {
57
- const withoutFragment = rawValue.split('#')[0]
58
- const queryIndex = withoutFragment.indexOf('?')
59
- query = queryIndex >= 0 ? withoutFragment.slice(queryIndex + 1) : ''
60
- }
61
-
62
- return Array.from(new URLSearchParams(query).entries())
63
- .sort(([leftKey, leftValue], [rightKey, rightValue]) => {
64
- const keyResult = compareText(leftKey, rightKey)
65
- return keyResult || compareText(leftValue, rightValue)
66
- })
67
- .map(([key, entryValue]) => `${encodeURIComponent(key)}=${encodeURIComponent(entryValue)}`)
68
- .join('&')
69
- }
70
-
71
- const normalizeJsonValue = value => {
72
- if (value === null) return null
73
- if (Array.isArray(value)) {
74
- return value.map(item =>
75
- item === undefined || typeof item === 'function' || typeof item === 'symbol'
76
- ? null
77
- : normalizeJsonValue(item),
78
- )
79
- }
80
- if (typeof value === 'number') return Number.isFinite(value) ? value : null
81
- if (typeof value !== 'object') return value
82
- if (typeof value.toJSON === 'function') return normalizeJsonValue(value.toJSON())
83
-
84
- return Object.keys(value)
85
- .sort(compareText)
86
- .reduce((result, key) => {
87
- const item = value[key]
88
- if (item !== undefined && typeof item !== 'function' && typeof item !== 'symbol') {
89
- result[key] = normalizeJsonValue(item)
90
- }
91
- return result
92
- }, {})
93
- }
94
-
95
- const stableJson = value => JSON.stringify(normalizeJsonValue(value)) || ''
96
-
97
- const formEntriesToObject = entries => {
98
- const result = {}
99
- for (const [key, value] of entries) {
100
- const current = result[key]
101
- result[key] =
102
- current === undefined
103
- ? value
104
- : Array.isArray(current)
105
- ? [...current, value]
106
- : [current, value]
107
- }
108
- return result
109
- }
110
-
111
- const isBinaryBody = body => {
112
- if (typeof Buffer !== 'undefined' && typeof Buffer.isBuffer === 'function' && Buffer.isBuffer(body)) {
113
- return true
114
- }
115
- if (typeof Blob !== 'undefined' && body instanceof Blob) return true
116
- if (typeof ArrayBuffer !== 'undefined') {
117
- return body instanceof ArrayBuffer || ArrayBuffer.isView(body)
118
- }
119
- return false
120
- }
121
-
122
- const canonicalizeBody = (body, contentType, hasBody) => {
123
- const normalizedContentType = String(contentType || '').toLowerCase()
124
- if (hasBody === false) return 'none'
125
- if (normalizedContentType.includes('multipart/form-data')) return 'multipart'
126
- if (typeof FormData !== 'undefined' && body instanceof FormData) return 'multipart'
127
- if (body === undefined || body === null || body === '') return 'none'
128
- if (isBinaryBody(body)) return 'binary'
129
- if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
130
- return `form:${stableJson(formEntriesToObject(body.entries()))}`
131
- }
132
- if (typeof body === 'string') {
133
- if (normalizedContentType.includes('application/x-www-form-urlencoded')) {
134
- return `form:${stableJson(formEntriesToObject(new URLSearchParams(body).entries()))}`
135
- }
136
- if (normalizedContentType.includes('json')) {
137
- try {
138
- return `json:${stableJson(JSON.parse(body))}`
139
- } catch {
140
- return `text:${body}`
141
- }
142
- }
143
- return `text:${body}`
144
- }
145
- if (normalizedContentType.includes('application/x-www-form-urlencoded')) {
146
- return `form:${stableJson(body)}`
147
- }
148
- return `json:${stableJson(body)}`
149
- }
150
-
151
- const isRequestProofRequired = value => String(value ?? '').trim() !== '0'
152
-
153
- const normalizeProofMode = value => {
154
- const mode = String(value ?? '').trim().toLowerCase()
155
- if (mode === '0' || mode === 'off') return 'off'
156
- if (mode === '1' || mode === 'shadow') return 'shadow'
157
- return 'enforce'
158
- }
159
-
160
- const isRequestProofSetupError = error => {
161
- const message = error instanceof Error ? error.message : String(error || '')
162
- return /^request_proof_(?:unsupported|storage_unsupported|storage_failed|read_failed|write_failed|reset_failed|sign_failed)$/.test(
163
- message,
164
- )
165
- }
166
-
167
- const getRequestProofErrorCode = reason => {
168
- if (reason === 'proof_expired') return REQUEST_PROOF_ERROR_CODES.timeMismatch
169
- if (reason === 'proof_replayed') return REQUEST_PROOF_ERROR_CODES.requestExpired
170
- if (
171
- [
172
- 'missing_proof',
173
- 'missing_proof_binding',
174
- 'missing_bound_proof',
175
- 'missing_sso_source_proof',
176
- 'invalid_proof_key',
177
- ].includes(reason)
178
- ) {
179
- return REQUEST_PROOF_ERROR_CODES.requestUnverifiable
180
- }
181
- if (
182
- [
183
- 'missing_user',
184
- 'missing_token',
185
- 'missing_device_cookie',
186
- 'missing_binding',
187
- 'missing_session_context',
188
- 'token_mismatch',
189
- 'session_mismatch',
190
- 'session_id_mismatch',
191
- 'missing_sso_source_binding',
192
- 'sso_source_device_mismatch',
193
- ].includes(reason)
194
- ) {
195
- return REQUEST_PROOF_ERROR_CODES.sessionMismatch
196
- }
197
- if (reason === 'missing_device_secret' || reason === 'verification_failed') {
198
- return REQUEST_PROOF_ERROR_CODES.serviceUnavailable
199
- }
200
- return REQUEST_PROOF_ERROR_CODES.requestInvalid
201
- }
202
-
203
- module.exports = {
204
- AUTH_CENTER_PROOF_AUDIENCE,
205
- DEFAULT_REQUEST_PROOF_TIME_TOLERANCE_SECONDS,
206
- ODM_REQUEST_PROOF_AUDIENCE,
207
- REQUEST_PROOF_ERROR_CODES,
208
- REQUEST_PROOF_HEADER,
209
- RequestProofValidationError,
210
- canonicalizeBody,
211
- canonicalizeQuery,
212
- getRequestProofErrorCode,
213
- isRequestProofRequired,
214
- isRequestProofSetupError,
215
- normalizeJsonValue,
216
- normalizePath,
217
- normalizeProofMode,
218
- stableJson,
219
- }
1
+ 'use strict'
2
+
3
+ const REQUEST_PROOF_HEADER = 'x-ctx-proof'
4
+ const AUTH_CENTER_PROOF_AUDIENCE = 'auth-center'
5
+ const ODM_REQUEST_PROOF_AUDIENCE = 'uweb-odm'
6
+ const DEFAULT_REQUEST_PROOF_TIME_TOLERANCE_SECONDS = 2 * 60 * 60
7
+
8
+ const REQUEST_PROOF_ERROR_CODES = Object.freeze({
9
+ timeMismatch: 'E4601',
10
+ requestExpired: 'E4602',
11
+ requestUnverifiable: 'E4603',
12
+ sessionMismatch: 'E4604',
13
+ requestInvalid: 'E4605',
14
+ serviceUnavailable: 'E503',
15
+ })
16
+
17
+ class RequestProofValidationError extends Error {
18
+ constructor(code) {
19
+ super(code)
20
+ this.name = 'RequestProofValidationError'
21
+ this.code = code
22
+ }
23
+ }
24
+
25
+ const compareText = (left, right) => (left < right ? -1 : left > right ? 1 : 0)
26
+
27
+ const normalizePath = value => {
28
+ const rawValue = String(value || '').trim()
29
+ if (!rawValue) return ''
30
+
31
+ let path = rawValue
32
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(rawValue)) {
33
+ try {
34
+ path = new URL(rawValue).pathname
35
+ } catch {
36
+ return ''
37
+ }
38
+ }
39
+
40
+ const withoutQuery = path.split(/[?#]/)[0]
41
+ const withLeadingSlash = withoutQuery.startsWith('/') ? withoutQuery : `/${withoutQuery}`
42
+ return withLeadingSlash.replace(/\/{2,}/g, '/') || '/'
43
+ }
44
+
45
+ const canonicalizeQuery = value => {
46
+ const rawValue = String(value || '').trim()
47
+ if (!rawValue) return ''
48
+
49
+ let query = ''
50
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(rawValue)) {
51
+ try {
52
+ query = new URL(rawValue).search.slice(1)
53
+ } catch {
54
+ return ''
55
+ }
56
+ } else {
57
+ const withoutFragment = rawValue.split('#')[0]
58
+ const queryIndex = withoutFragment.indexOf('?')
59
+ query = queryIndex >= 0 ? withoutFragment.slice(queryIndex + 1) : ''
60
+ }
61
+
62
+ return Array.from(new URLSearchParams(query).entries())
63
+ .sort(([leftKey, leftValue], [rightKey, rightValue]) => {
64
+ const keyResult = compareText(leftKey, rightKey)
65
+ return keyResult || compareText(leftValue, rightValue)
66
+ })
67
+ .map(([key, entryValue]) => `${encodeURIComponent(key)}=${encodeURIComponent(entryValue)}`)
68
+ .join('&')
69
+ }
70
+
71
+ const normalizeJsonValue = value => {
72
+ if (value === null) return null
73
+ if (Array.isArray(value)) {
74
+ return value.map(item =>
75
+ item === undefined || typeof item === 'function' || typeof item === 'symbol'
76
+ ? null
77
+ : normalizeJsonValue(item),
78
+ )
79
+ }
80
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null
81
+ if (typeof value !== 'object') return value
82
+ if (typeof value.toJSON === 'function') return normalizeJsonValue(value.toJSON())
83
+
84
+ return Object.keys(value)
85
+ .sort(compareText)
86
+ .reduce((result, key) => {
87
+ const item = value[key]
88
+ if (item !== undefined && typeof item !== 'function' && typeof item !== 'symbol') {
89
+ result[key] = normalizeJsonValue(item)
90
+ }
91
+ return result
92
+ }, {})
93
+ }
94
+
95
+ const stableJson = value => JSON.stringify(normalizeJsonValue(value)) || ''
96
+
97
+ const formEntriesToObject = entries => {
98
+ const result = {}
99
+ for (const [key, value] of entries) {
100
+ const current = result[key]
101
+ result[key] =
102
+ current === undefined
103
+ ? value
104
+ : Array.isArray(current)
105
+ ? [...current, value]
106
+ : [current, value]
107
+ }
108
+ return result
109
+ }
110
+
111
+ const isBinaryBody = body => {
112
+ if (typeof Buffer !== 'undefined' && typeof Buffer.isBuffer === 'function' && Buffer.isBuffer(body)) {
113
+ return true
114
+ }
115
+ if (typeof Blob !== 'undefined' && body instanceof Blob) return true
116
+ if (typeof ArrayBuffer !== 'undefined') {
117
+ return body instanceof ArrayBuffer || ArrayBuffer.isView(body)
118
+ }
119
+ return false
120
+ }
121
+
122
+ const canonicalizeBody = (body, contentType, hasBody) => {
123
+ const normalizedContentType = String(contentType || '').toLowerCase()
124
+ if (hasBody === false) return 'none'
125
+ if (normalizedContentType.includes('multipart/form-data')) return 'multipart'
126
+ if (typeof FormData !== 'undefined' && body instanceof FormData) return 'multipart'
127
+ if (body === undefined || body === null || body === '') return 'none'
128
+ if (isBinaryBody(body)) return 'binary'
129
+ if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
130
+ return `form:${stableJson(formEntriesToObject(body.entries()))}`
131
+ }
132
+ if (typeof body === 'string') {
133
+ if (normalizedContentType.includes('application/x-www-form-urlencoded')) {
134
+ return `form:${stableJson(formEntriesToObject(new URLSearchParams(body).entries()))}`
135
+ }
136
+ if (normalizedContentType.includes('json')) {
137
+ try {
138
+ return `json:${stableJson(JSON.parse(body))}`
139
+ } catch {
140
+ return `text:${body}`
141
+ }
142
+ }
143
+ return `text:${body}`
144
+ }
145
+ if (normalizedContentType.includes('application/x-www-form-urlencoded')) {
146
+ return `form:${stableJson(body)}`
147
+ }
148
+ return `json:${stableJson(body)}`
149
+ }
150
+
151
+ const isRequestProofRequired = value => String(value ?? '').trim() !== '0'
152
+
153
+ const normalizeProofMode = value => {
154
+ const mode = String(value ?? '').trim().toLowerCase()
155
+ if (mode === '0' || mode === 'off') return 'off'
156
+ if (mode === '1' || mode === 'shadow') return 'shadow'
157
+ return 'enforce'
158
+ }
159
+
160
+ const isRequestProofSetupError = error => {
161
+ const message = error instanceof Error ? error.message : String(error || '')
162
+ return /^request_proof_(?:unsupported|storage_unsupported|storage_failed|read_failed|write_failed|reset_failed|sign_failed|key_missing|key_changed|session_missing|context_invalid)$/.test(
163
+ message,
164
+ )
165
+ }
166
+
167
+ const getRequestProofErrorCode = reason => {
168
+ if (reason === 'proof_expired') return REQUEST_PROOF_ERROR_CODES.timeMismatch
169
+ if (reason === 'proof_replayed') return REQUEST_PROOF_ERROR_CODES.requestExpired
170
+ if (
171
+ [
172
+ 'missing_proof',
173
+ 'missing_proof_binding',
174
+ 'missing_bound_proof',
175
+ 'missing_sso_source_proof',
176
+ 'invalid_proof_key',
177
+ ].includes(reason)
178
+ ) {
179
+ return REQUEST_PROOF_ERROR_CODES.requestUnverifiable
180
+ }
181
+ if (
182
+ [
183
+ 'missing_user',
184
+ 'missing_token',
185
+ 'missing_device_cookie',
186
+ 'missing_binding',
187
+ 'missing_session_context',
188
+ 'proof_session_mismatch',
189
+ 'proof_target_mismatch',
190
+ 'token_mismatch',
191
+ 'session_mismatch',
192
+ 'session_id_mismatch',
193
+ 'missing_sso_source_binding',
194
+ 'sso_source_device_mismatch',
195
+ ].includes(reason)
196
+ ) {
197
+ return REQUEST_PROOF_ERROR_CODES.sessionMismatch
198
+ }
199
+ if (reason === 'missing_device_secret' || reason === 'verification_failed') {
200
+ return REQUEST_PROOF_ERROR_CODES.serviceUnavailable
201
+ }
202
+ return REQUEST_PROOF_ERROR_CODES.requestInvalid
203
+ }
204
+
205
+ module.exports = {
206
+ AUTH_CENTER_PROOF_AUDIENCE,
207
+ DEFAULT_REQUEST_PROOF_TIME_TOLERANCE_SECONDS,
208
+ ODM_REQUEST_PROOF_AUDIENCE,
209
+ REQUEST_PROOF_ERROR_CODES,
210
+ REQUEST_PROOF_HEADER,
211
+ RequestProofValidationError,
212
+ canonicalizeBody,
213
+ canonicalizeQuery,
214
+ getRequestProofErrorCode,
215
+ isRequestProofRequired,
216
+ isRequestProofSetupError,
217
+ normalizeJsonValue,
218
+ normalizePath,
219
+ normalizeProofMode,
220
+ stableJson,
221
+ }
package/node.d.ts CHANGED
@@ -1,76 +1,80 @@
1
- export type RequestProofPublicKey = {
2
- kty: 'EC'
3
- crv: 'P-256'
4
- x: string
5
- y: string
6
- }
7
-
8
- export type NormalizedRequestProofKey = {
9
- keyId: string
10
- publicKey: RequestProofPublicKey
11
- }
12
-
13
- export type RequestProofTimeWarning = {
14
- reason: 'proof_expired'
15
- issuedAt: number | null
16
- serverNow: number
17
- diffSeconds: number | null
18
- toleranceSeconds: number
19
- }
20
-
21
- export type VerifyRequestProofOptions = {
22
- proof: string
23
- publicKey: RequestProofPublicKey
24
- keyId: string
25
- token: string
26
- method: string
27
- path: string
28
- audience: string
29
- timeToleranceSeconds?: number
30
- body?: unknown
31
- contentType?: unknown
32
- hasBody?: boolean
33
- requestId?: unknown
34
- requireRequestBindings?: boolean
35
- }
36
-
37
- export type ProofServerMode = 'off' | 'shadow' | 'enforce'
38
-
39
- export type ProofServerConfig = {
40
- mode: ProofServerMode
41
- deviceSessionSecret: string
42
- deviceSessionTtlSeconds: number
43
- timeToleranceSeconds: number
44
- }
45
-
46
- export const REQUEST_PROOF_REASON_TEXT: Readonly<Record<string, string>>
47
- export function getRequestProofReasonText(reason: string): string
48
-
49
- export function hasTransferredRequestBody(options: {
50
- body?: unknown
51
- contentLength?: unknown
52
- transferEncoding?: unknown
53
- }): boolean
54
- export function calculateRequestProofBindings(options: {
55
- path: unknown
56
- body?: unknown
57
- contentType?: unknown
58
- hasBody?: boolean
59
- requestId?: unknown
60
- }): { qsh: string; bth: string; rid: string }
61
- export function normalizeRequestProofKey(value: unknown): Promise<NormalizedRequestProofKey | null>
62
- export function getRequestProofKeyId(proofValue: string): string
63
- export function verifyRequestProof(options: VerifyRequestProofOptions): Promise<{
64
- jti: string
65
- timeWarning: RequestProofTimeWarning | null
66
- }>
67
- export function normalizeProofMode(value: unknown): ProofServerMode
68
- export function resolveProofServerConfig(options?: {
69
- env?: Record<string, string | undefined>
70
- mode?: string | number
71
- deviceSessionSecret?: string
72
- deviceSessionTtlSeconds?: number
73
- timeToleranceSeconds?: number
74
- }): ProofServerConfig
75
-
76
- export { RequestProofValidationError } from './core'
1
+ export type RequestProofPublicKey = {
2
+ kty: 'EC'
3
+ crv: 'P-256'
4
+ x: string
5
+ y: string
6
+ }
7
+
8
+ export type NormalizedRequestProofKey = {
9
+ keyId: string
10
+ publicKey: RequestProofPublicKey
11
+ }
12
+
13
+ export type RequestProofTimeWarning = {
14
+ reason: 'proof_expired'
15
+ issuedAt: number | null
16
+ serverNow: number
17
+ diffSeconds: number | null
18
+ toleranceSeconds: number
19
+ }
20
+
21
+ export function deriveProofSessionId(sessionId: string): string
22
+
23
+ export type VerifyRequestProofOptions = {
24
+ purpose?: 'session' | 'ticket'
25
+ sessionId?: string
26
+ proof: string
27
+ publicKey: RequestProofPublicKey
28
+ keyId: string
29
+ token: string
30
+ method: string
31
+ path: string
32
+ audience: string
33
+ timeToleranceSeconds?: number
34
+ body?: unknown
35
+ contentType?: unknown
36
+ hasBody?: boolean
37
+ requestId?: unknown
38
+ requireRequestBindings?: boolean
39
+ }
40
+
41
+ export type ProofServerMode = 'off' | 'shadow' | 'enforce'
42
+
43
+ export type ProofServerConfig = {
44
+ mode: ProofServerMode
45
+ deviceSessionSecret: string
46
+ deviceSessionTtlSeconds: number
47
+ timeToleranceSeconds: number
48
+ }
49
+
50
+ export const REQUEST_PROOF_REASON_TEXT: Readonly<Record<string, string>>
51
+ export function getRequestProofReasonText(reason: string): string
52
+
53
+ export function hasTransferredRequestBody(options: {
54
+ body?: unknown
55
+ contentLength?: unknown
56
+ transferEncoding?: unknown
57
+ }): boolean
58
+ export function calculateRequestProofBindings(options: {
59
+ path: unknown
60
+ body?: unknown
61
+ contentType?: unknown
62
+ hasBody?: boolean
63
+ requestId?: unknown
64
+ }): { qsh: string; bth: string; rid: string }
65
+ export function normalizeRequestProofKey(value: unknown): Promise<NormalizedRequestProofKey | null>
66
+ export function getRequestProofKeyId(proofValue: string): string
67
+ export function verifyRequestProof(options: VerifyRequestProofOptions): Promise<{
68
+ jti: string
69
+ timeWarning: RequestProofTimeWarning | null
70
+ }>
71
+ export function normalizeProofMode(value: unknown): ProofServerMode
72
+ export function resolveProofServerConfig(options?: {
73
+ env?: Record<string, string | undefined>
74
+ mode?: string | number
75
+ deviceSessionSecret?: string
76
+ deviceSessionTtlSeconds?: number
77
+ timeToleranceSeconds?: number
78
+ }): ProofServerConfig
79
+
80
+ export { RequestProofValidationError } from './core'
package/package.json CHANGED
@@ -1,18 +1,9 @@
1
1
  {
2
2
  "name": "@travelclw/proof-protocol",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Framework-independent browser and Node.js request Proof protocol",
5
5
  "license": "MIT",
6
6
  "author": "travelclw",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://gitee.com/bubbles_4/travelclw.git",
10
- "directory": "packages/proof-protocol"
11
- },
12
- "homepage": "https://gitee.com/bubbles_4/travelclw/tree/main/packages/proof-protocol#readme",
13
- "bugs": {
14
- "url": "https://gitee.com/bubbles_4/travelclw/issues"
15
- },
16
7
  "keywords": [
17
8
  "proof-of-possession",
18
9
  "request-signing",