@peanut-admin/admin 0.1.0-alpha.3 → 0.1.0-alpha.4

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.
@@ -7,11 +7,27 @@ export type ClientRequestMethod =
7
7
  | 'POST'
8
8
  | 'PUT'
9
9
 
10
+ export interface ClientHeaderSource {
11
+ forEach: (callback: (value: string, key: string) => void) => void
12
+ }
13
+
14
+ export type ClientRequestHeaders =
15
+ | Readonly<Record<string, string>>
16
+ | ReadonlyArray<readonly [string, string]>
17
+ | ClientHeaderSource
18
+
19
+ export interface ClientHeaders extends ClientHeaderSource {
20
+ delete: (name: string) => void
21
+ get: (name: string) => string | null
22
+ has: (name: string) => boolean
23
+ set: (name: string, value: string) => void
24
+ }
25
+
10
26
  export interface ClientRequest<TData = unknown> {
11
27
  readonly path: string
12
28
  readonly method?: ClientRequestMethod
13
29
  readonly data?: TData
14
- readonly headers?: HeadersInit
30
+ readonly headers?: ClientRequestHeaders
15
31
  readonly auth?: boolean
16
32
  }
17
33
 
@@ -19,7 +35,7 @@ export interface ClientTransportRequest<TData = unknown> {
19
35
  readonly path: string
20
36
  readonly method: ClientRequestMethod
21
37
  readonly data?: TData
22
- readonly headers: Headers
38
+ readonly headers: ClientHeaders
23
39
  }
24
40
 
25
41
  export type ClientTransport = (request: ClientTransportRequest) => Promise<unknown>
@@ -96,6 +112,9 @@ const pathControlCharacters = /[\u0000-\u001f\u007f]/
96
112
  const controlCharacters = /[\u0000-\u001f\u007f]/g
97
113
  const encodedUnsafePathSegment = /%(?:2e|2f|5c)/i
98
114
  const absolutePath = /^[A-Za-z][A-Za-z0-9+.-]*:/
115
+ const httpBaseUrl = /^(https?):\/\/([^/?#]+)(\/[^?#]*)?$/i
116
+ const validHeaderName = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
117
+ const invalidHeaderValue = /[\u0000\r\n]/
99
118
  const safeCode = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/
100
119
  const defaultUnauthorizedCode = 'CLIENT_UNAUTHORIZED'
101
120
  const defaultBusinessCode = 'CLIENT_BUSINESS_ERROR'
@@ -130,34 +149,49 @@ const assertClientPath = (path: string): void => {
130
149
  }
131
150
  }
132
151
 
133
- const validBaseUrl = (baseUrl: string): URL => {
134
- let url: URL
135
- try {
136
- url = new URL(baseUrl)
137
- } catch {
138
- throw new ClientRequestError('path', 'CLIENT_BASE_URL_INVALID', 'The client base URL is invalid.')
152
+ const invalidBaseUrl = (): ClientRequestError => (
153
+ new ClientRequestError('path', 'CLIENT_BASE_URL_INVALID', 'The client base URL is invalid.')
154
+ )
155
+
156
+ const validBaseUrl = (baseUrl: string): { origin: string; pathname: string } => {
157
+ if (
158
+ typeof baseUrl !== 'string'
159
+ || baseUrl.trim() !== baseUrl
160
+ || pathControlCharacters.test(baseUrl)
161
+ || baseUrl.includes('\\')
162
+ ) {
163
+ throw invalidBaseUrl()
139
164
  }
140
165
 
166
+ const match = httpBaseUrl.exec(baseUrl)
167
+ const protocol = match?.[1]
168
+ const authority = match?.[2]
169
+ const pathname = match?.[3] ?? '/'
141
170
  if (
142
- !['http:', 'https:'].includes(url.protocol)
143
- || url.username !== ''
144
- || url.password !== ''
145
- || url.search !== ''
146
- || url.hash !== ''
171
+ protocol === undefined
172
+ || authority === undefined
173
+ || authority === ''
174
+ || authority.includes('@')
175
+ || /\s/.test(authority)
176
+ || encodedUnsafePathSegment.test(pathname)
177
+ || pathname.split('/').some(segment => segment === '.' || segment === '..')
147
178
  ) {
148
- throw new ClientRequestError('path', 'CLIENT_BASE_URL_INVALID', 'The client base URL is invalid.')
179
+ throw invalidBaseUrl()
149
180
  }
150
181
 
151
- return url
182
+ return {
183
+ origin: `${protocol.toLowerCase()}://${authority}`,
184
+ pathname,
185
+ }
152
186
  }
153
187
 
154
188
  export const resolveClientUrl = (baseUrl: string, path: string): string => {
155
189
  assertClientPath(path)
156
190
  const base = validBaseUrl(baseUrl)
157
191
  const basePath = base.pathname.endsWith('/') ? base.pathname : `${base.pathname}/`
158
- const root = new URL(base.origin)
159
- root.pathname = basePath
160
- return new URL(path, root).toString()
192
+ return path.startsWith('/')
193
+ ? `${base.origin}${path}`
194
+ : `${base.origin}${basePath}${path}`
161
195
  }
162
196
 
163
197
  const safeMessage = (value: unknown, fallback: string): string => {
@@ -203,9 +237,66 @@ const normalizedDecodedResult = (value: ClientDecodeResult): ClientDecodeResult
203
237
  }
204
238
  }
205
239
 
206
- const requestHeaders = (headers: HeadersInit | undefined): Headers => {
207
- const result = new Headers(headers)
208
- // Headers is case-insensitive and delete removes all values for this name.
240
+ const isHeaderSource = (value: unknown): value is ClientHeaderSource => (
241
+ typeof value === 'object'
242
+ && value !== null
243
+ && typeof (value as { forEach?: unknown }).forEach === 'function'
244
+ )
245
+
246
+ class PortableClientHeaders implements ClientHeaders {
247
+ private readonly values = new Map<string, string>()
248
+
249
+ constructor(headers?: ClientRequestHeaders) {
250
+ if (headers === undefined) return
251
+
252
+ if (Array.isArray(headers)) {
253
+ for (const entry of headers) {
254
+ if (!Array.isArray(entry) || entry.length !== 2) throw new TypeError('invalid header entry')
255
+ this.set(entry[0], entry[1])
256
+ }
257
+ return
258
+ }
259
+
260
+ if (isHeaderSource(headers)) {
261
+ headers.forEach((value, key) => this.set(key, value))
262
+ return
263
+ }
264
+
265
+ if (typeof headers === 'object' && headers !== null) {
266
+ for (const [key, value] of Object.entries(headers)) this.set(key, value)
267
+ return
268
+ }
269
+
270
+ throw new TypeError('invalid headers')
271
+ }
272
+
273
+ delete(name: string): void {
274
+ this.values.delete(name.toLowerCase())
275
+ }
276
+
277
+ get(name: string): string | null {
278
+ return this.values.get(name.toLowerCase()) ?? null
279
+ }
280
+
281
+ has(name: string): boolean {
282
+ return this.values.has(name.toLowerCase())
283
+ }
284
+
285
+ set(name: string, value: string): void {
286
+ if (!validHeaderName.test(name) || typeof value !== 'string' || invalidHeaderValue.test(value)) {
287
+ throw new TypeError('invalid header')
288
+ }
289
+ this.values.set(name.toLowerCase(), value.trim())
290
+ }
291
+
292
+ forEach(callback: (value: string, key: string) => void): void {
293
+ this.values.forEach((value, key) => callback(value, key))
294
+ }
295
+ }
296
+
297
+ const requestHeaders = (headers: ClientRequestHeaders | undefined): ClientHeaders => {
298
+ const result = new PortableClientHeaders(headers)
299
+ // Header names are normalized, so delete removes every caller spelling.
209
300
  result.delete('Authorization')
210
301
  return result
211
302
  }
@@ -251,7 +342,7 @@ export const createClient = (options: ClientOptions): Client => {
251
342
  }
252
343
 
253
344
  const method = methodOf(input.method)
254
- let headers: Headers
345
+ let headers: ClientHeaders
255
346
  try {
256
347
  headers = requestHeaders(input.headers)
257
348
  } catch {
@@ -322,4 +413,3 @@ export const createClient = (options: ClientOptions): Client => {
322
413
  export type ClientResult<TData = unknown> = ClientDecodeResult<TData>
323
414
  export type ClientRequestResult<TData = unknown> = Promise<TData>
324
415
  export type ClientHook = (error: ClientRequestError) => void | Promise<void>
325
- export type ClientRequestHeaders = HeadersInit
@@ -1,11 +1,11 @@
1
1
  import { resolveClientUrl } from '@peanut-admin/admin/client'
2
- import type { ClientTransport, ClientTransportRequest } from '@peanut-admin/admin/client'
2
+ import type { ClientHeaders, ClientTransport, ClientTransportRequest } from '@peanut-admin/admin/client'
3
3
 
4
4
  export interface NuxtClientFetchOptions {
5
5
  readonly method?: string
6
6
  readonly query?: unknown
7
7
  readonly body?: unknown
8
- readonly headers?: HeadersInit
8
+ readonly headers?: Record<string, string>
9
9
  }
10
10
 
11
11
  export type NuxtClientFetch = (
@@ -20,6 +20,14 @@ export interface NuxtClientTransportOptions {
20
20
 
21
21
  const isQueryMethod = (method: string): boolean => method === 'GET' || method === 'DELETE'
22
22
 
23
+ const headersRecord = (headers: ClientHeaders): Record<string, string> => {
24
+ const result: Record<string, string> = {}
25
+ headers.forEach((value, key) => {
26
+ result[key] = value
27
+ })
28
+ return result
29
+ }
30
+
23
31
  export const createNuxtClientTransport = (
24
32
  options: NuxtClientTransportOptions,
25
33
  ): ClientTransport => {
@@ -28,7 +36,7 @@ export const createNuxtClientTransport = (
28
36
  const url = resolveClientUrl(options.baseUrl, request.path)
29
37
  const fetchOptions: NuxtClientFetchOptions = {
30
38
  method,
31
- headers: request.headers,
39
+ headers: headersRecord(request.headers),
32
40
  ...(request.data !== undefined
33
41
  ? isQueryMethod(method)
34
42
  ? { query: request.data }
@@ -1,5 +1,5 @@
1
1
  import { resolveClientUrl } from '@peanut-admin/admin/client'
2
- import type { ClientTransport, ClientTransportRequest } from '@peanut-admin/admin/client'
2
+ import type { ClientHeaders, ClientTransport, ClientTransportRequest } from '@peanut-admin/admin/client'
3
3
 
4
4
  export interface UniAppClientResponse {
5
5
  readonly data: unknown
@@ -23,7 +23,7 @@ export interface UniAppClientTransportOptions {
23
23
  readonly request: UniAppClientRequest
24
24
  }
25
25
 
26
- const headersRecord = (headers: Headers): Record<string, string> => {
26
+ const headersRecord = (headers: ClientHeaders): Record<string, string> => {
27
27
  const result: Record<string, string> = {}
28
28
  headers.forEach((value, key) => {
29
29
  result[key] = value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peanut-admin/admin",
3
- "version": "0.1.0-alpha.3",
3
+ "version": "0.1.0-alpha.4",
4
4
  "description": "Reusable Peanut Admin Web services and module contributions",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -100,22 +100,32 @@
100
100
  }
101
101
  },
102
102
  "dependencies": {
103
- "element-plus": "2.14.3",
104
- "openapi-fetch": "0.17.0",
105
- "pinia": "4.0.2",
106
- "vue-router": "5.2.0"
103
+ "openapi-fetch": "0.17.0"
107
104
  },
108
105
  "devDependencies": {
109
106
  "@vue/test-utils": "2.4.11",
107
+ "element-plus": "2.14.3",
110
108
  "happy-dom": "20.10.6",
109
+ "pinia": "4.0.2",
111
110
  "typescript": "5.9.3",
112
111
  "vite": "8.1.4",
113
112
  "vitest": "4.1.10",
114
113
  "vue": "3.5.39",
114
+ "vue-router": "5.2.0",
115
115
  "vue-tsc": "3.3.7"
116
116
  },
117
117
  "peerDependencies": {
118
- "vue": "^3.5.39"
118
+ "element-plus": "^2.14.3",
119
+ "pinia": ">=2.0.23 <5",
120
+ "vue": "^3.4.21"
121
+ },
122
+ "peerDependenciesMeta": {
123
+ "element-plus": {
124
+ "optional": true
125
+ },
126
+ "pinia": {
127
+ "optional": true
128
+ }
119
129
  },
120
130
  "scripts": {
121
131
  "test": "vitest run admin-core/tests admin-shell/tests file-media/tests import-export/tests integration-security/tests notification-sms/tests ops-console/tests reference-codes/tests settings/tests testing/tests client-core/tests client-nuxt/tests client-uniapp/tests",