@de./sdk 2.0.0 → 2.1.1
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/dist/allend/Access.js +22 -5
- package/dist/allend/Access.js.map +1 -1
- package/dist/allend/LSP/agents.d.ts +14 -1
- package/dist/allend/LSP/agents.js +38 -0
- package/dist/allend/LSP/agents.js.map +1 -1
- package/dist/allend/MSI/Handles.d.ts +10 -2
- package/dist/allend/MSI/Handles.js +11 -3
- package/dist/allend/MSI/Handles.js.map +1 -1
- package/dist/allend/MSI/index.d.ts +10 -0
- package/dist/allend/MSI/index.js +19 -0
- package/dist/allend/MSI/index.js.map +1 -1
- package/dist/allend/MSI/native.js +69 -35
- package/dist/allend/MSI/native.js.map +1 -1
- package/dist/allend/MSI/origin.d.ts +15 -0
- package/dist/allend/MSI/origin.js +25 -0
- package/dist/allend/MSI/origin.js.map +1 -0
- package/dist/allend/OTPAuth/index.d.ts +12 -18
- package/dist/allend/OTPAuth/index.js +17 -7
- package/dist/allend/OTPAuth/index.js.map +1 -1
- package/dist/baseUrl.d.ts +3 -5
- package/dist/baseUrl.js +3 -5
- package/dist/baseUrl.js.map +1 -1
- package/dist/core.d.ts +1 -0
- package/dist/core.js +3 -1
- package/dist/core.js.map +1 -1
- package/dist/response.d.ts +9 -0
- package/dist/response.js +42 -0
- package/dist/response.js.map +1 -0
- package/dist/utils/index.d.ts +14 -0
- package/dist/utils/index.js +24 -4
- package/dist/utils/index.js.map +1 -1
- package/package.json +13 -7
- package/src/allend/Access.ts +23 -4
- package/src/allend/LSP/agents.ts +46 -1
- package/src/allend/MSI/Handles.ts +12 -4
- package/src/allend/MSI/index.ts +20 -0
- package/src/allend/MSI/native.tsx +73 -34
- package/src/allend/MSI/origin.ts +20 -0
- package/src/allend/OTPAuth/index.ts +33 -25
- package/src/baseUrl.ts +3 -5
- package/src/core.ts +1 -0
- package/src/response.ts +38 -0
- package/src/utils/index.ts +27 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MapOptions } from '../../types'
|
|
2
2
|
|
|
3
|
-
import React, { useRef, useEffect, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
|
|
3
|
+
import React, { useRef, useEffect, useState, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'
|
|
4
4
|
import { View, StyleSheet, AppState, type AppStateStatus, Platform } from 'react-native'
|
|
5
5
|
import { WebView } from 'react-native-webview'
|
|
6
6
|
import WIO from 'webview.io'
|
|
@@ -8,6 +8,7 @@ import Handles from './Handles'
|
|
|
8
8
|
import Controls from './Controls'
|
|
9
9
|
import Plugins, { type Plugin } from './Plugins'
|
|
10
10
|
import resolveAccessToken from './token'
|
|
11
|
+
import belongsToGateway from './origin'
|
|
11
12
|
|
|
12
13
|
export interface MSIInterface {
|
|
13
14
|
controls: Controls
|
|
@@ -115,6 +116,18 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
|
|
|
115
116
|
apiRef = useRef<MSIInterface | null>( null ),
|
|
116
117
|
isInitializedRef = useRef( false ),
|
|
117
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The bridge is built once, so its handlers close over the props of the
|
|
121
|
+
* render that built it. `bind` resolves the access token at connect time
|
|
122
|
+
* precisely so a reconnect picks up a rotated one -- but reading it from a
|
|
123
|
+
* captured `props` gave back whatever `getAccessToken` closed over at mount,
|
|
124
|
+
* which for the usual `() => token` is the token the host held then. Every
|
|
125
|
+
* reconnect after a rotation would have re-bound with a dead token. Same for
|
|
126
|
+
* the callbacks: a host passing inline handlers had the mount-time ones
|
|
127
|
+
* called forever.
|
|
128
|
+
*/
|
|
129
|
+
propsRef = useRef( props ),
|
|
130
|
+
|
|
118
131
|
[ isConnected, setIsConnected ] = useState( false ),
|
|
119
132
|
[ isReady, setIsReady ] = useState( false ),
|
|
120
133
|
|
|
@@ -128,13 +141,46 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
|
|
|
128
141
|
? error
|
|
129
142
|
: new Error( typeof error === 'string' ? error : error?.error || 'Unknown MSI error')
|
|
130
143
|
|
|
131
|
-
|
|
132
|
-
}, [
|
|
144
|
+
propsRef.current.onError?.( normalized )
|
|
145
|
+
}, [] ),
|
|
133
146
|
|
|
134
147
|
initializeConnection = useCallback( () => {
|
|
135
148
|
if( !wioRef.current || !webViewRef.current ) return
|
|
136
149
|
wioRef.current.initiate( webViewRef, baseURL )
|
|
137
|
-
}, [ baseURL ] )
|
|
150
|
+
}, [ baseURL ] ),
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Refuse to navigate anywhere but the gateway -- see `belongsToGateway`.
|
|
154
|
+
*/
|
|
155
|
+
isGatewayURL = useCallback( ( url: string ) => belongsToGateway( baseURL, url ), [ baseURL ] ),
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* One options object for both ends of the bridge.
|
|
159
|
+
*
|
|
160
|
+
* `getInjectedJavaScript()` bakes the transport's own configuration into the
|
|
161
|
+
* script the WebView runs — the auth secret, the skew tolerance, the replay
|
|
162
|
+
* window. It used to be called on a throwaway `new WIO({ type: 'WEBVIEW' })`
|
|
163
|
+
* constructed inline in the JSX, which shares nothing with the instance that
|
|
164
|
+
* actually holds the connection: the embedded side would have been handed a
|
|
165
|
+
* null secret and default tolerances no matter what the host was configured
|
|
166
|
+
* with. Nothing configures cryptoAuth today, so it never showed.
|
|
167
|
+
*/
|
|
168
|
+
wioOptions = useMemo( () => ({
|
|
169
|
+
type: 'WEBVIEW' as const,
|
|
170
|
+
debug: props.env === 'dev',
|
|
171
|
+
allowedIncomingEvents: ALLOWED_INCOMING_EVENTS,
|
|
172
|
+
maxMessagesPerSecond: 100,
|
|
173
|
+
connectionTimeout: 15000,
|
|
174
|
+
connectionPingInterval: 2000,
|
|
175
|
+
maxConnectionAttempts: 5,
|
|
176
|
+
autoReconnect: true,
|
|
177
|
+
heartbeatInterval: 30000
|
|
178
|
+
}), [ props.env ] ),
|
|
179
|
+
|
|
180
|
+
// Rebuilt only when those options change, rather than on every render.
|
|
181
|
+
injectedBridge = useMemo( () => new WIO( wioOptions ).getInjectedJavaScript(), [ wioOptions ] )
|
|
182
|
+
|
|
183
|
+
useEffect( () => { propsRef.current = props } )
|
|
138
184
|
|
|
139
185
|
// Expose the control surface to the host
|
|
140
186
|
useImperativeHandle( ref, () => ({
|
|
@@ -149,17 +195,7 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
|
|
|
149
195
|
}), [ isConnected, isReady, initializeConnection ] )
|
|
150
196
|
|
|
151
197
|
useEffect( () => {
|
|
152
|
-
const wio = wioRef.current = new WIO(
|
|
153
|
-
type: 'WEBVIEW',
|
|
154
|
-
debug: props.env === 'dev',
|
|
155
|
-
allowedIncomingEvents: ALLOWED_INCOMING_EVENTS,
|
|
156
|
-
maxMessagesPerSecond: 100,
|
|
157
|
-
connectionTimeout: 15000,
|
|
158
|
-
connectionPingInterval: 2000,
|
|
159
|
-
maxConnectionAttempts: 5,
|
|
160
|
-
autoReconnect: true,
|
|
161
|
-
heartbeatInterval: 30000
|
|
162
|
-
})
|
|
198
|
+
const wio = wioRef.current = new WIO( wioOptions )
|
|
163
199
|
|
|
164
200
|
wio
|
|
165
201
|
.on('connect', async () => {
|
|
@@ -168,11 +204,11 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
|
|
|
168
204
|
* Resolve the token here rather than at mount, so a reconnect after a
|
|
169
205
|
* backgrounded app or a dropped bridge binds with a current token.
|
|
170
206
|
*/
|
|
171
|
-
const { getAccessToken, onReady, onError, onLoaded, style, ...config } =
|
|
207
|
+
const { getAccessToken, onReady, onError, onLoaded, style, ...config } = propsRef.current
|
|
172
208
|
|
|
173
209
|
await wio.emitAsync('bind', {
|
|
174
210
|
...config,
|
|
175
|
-
accessToken: resolveAccessToken(
|
|
211
|
+
accessToken: resolveAccessToken( propsRef.current ),
|
|
176
212
|
origin: 'react-native'
|
|
177
213
|
}, BIND_TIMEOUT )
|
|
178
214
|
|
|
@@ -182,7 +218,7 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
|
|
|
182
218
|
})
|
|
183
219
|
.on('ready', () => {
|
|
184
220
|
setIsReady( true )
|
|
185
|
-
|
|
221
|
+
propsRef.current.onReady?.()
|
|
186
222
|
})
|
|
187
223
|
.on('disconnect', () => {
|
|
188
224
|
setIsConnected( false )
|
|
@@ -201,19 +237,21 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
|
|
|
201
237
|
})
|
|
202
238
|
|
|
203
239
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
240
|
+
* Re-establish the bridge on resume -- iOS in particular tears down the
|
|
241
|
+
* WebView's socket while the app is backgrounded.
|
|
242
|
+
*
|
|
243
|
+
* This used to also emit `app:background` / `app:foreground`, which read
|
|
244
|
+
* like the gateway paused its work. It never did: the gateway has no
|
|
245
|
+
* handler for either, and its own allowlist dropped them before they got
|
|
246
|
+
* that far. Nor should there be a blanket one -- a rider streaming their
|
|
247
|
+
* position must keep streaming with the phone in their pocket, so whether
|
|
248
|
+
* backgrounding stops anything is the host's call, made through
|
|
249
|
+
* `controls.untrackLiveLocation()` and its counterpart.
|
|
206
250
|
*/
|
|
207
251
|
const subscription = AppState.addEventListener('change', ( state: AppStateStatus ) => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if( state === 'active' ){
|
|
214
|
-
wio.emit('app:foreground')
|
|
215
|
-
!wio.isConnected() && initializeConnection()
|
|
216
|
-
}
|
|
252
|
+
state === 'active'
|
|
253
|
+
&& !wio.isConnected()
|
|
254
|
+
&& initializeConnection()
|
|
217
255
|
})
|
|
218
256
|
|
|
219
257
|
return () => {
|
|
@@ -231,16 +269,16 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
|
|
|
231
269
|
|
|
232
270
|
const
|
|
233
271
|
chn = wioRef.current,
|
|
234
|
-
controls = new Controls( chn,
|
|
235
|
-
handles = new Handles( chn, controls,
|
|
236
|
-
plugins = new Plugins( chn, handles, controls,
|
|
272
|
+
controls = new Controls( chn, propsRef.current ),
|
|
273
|
+
handles = new Handles( chn, controls, propsRef.current ),
|
|
274
|
+
plugins = new Plugins( chn, handles, controls, propsRef.current )
|
|
237
275
|
|
|
238
276
|
plugins.mount( REGISTERED_PLUGINS )
|
|
239
277
|
|
|
240
278
|
apiRef.current = { controls, handles, plugins }
|
|
241
279
|
isInitializedRef.current = true
|
|
242
280
|
|
|
243
|
-
|
|
281
|
+
propsRef.current.onLoaded?.( apiRef.current )
|
|
244
282
|
}, [ isConnected, isReady ] )
|
|
245
283
|
|
|
246
284
|
const onMessage = ( event: any ) => {
|
|
@@ -274,7 +312,7 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
|
|
|
274
312
|
style={styles.webview}
|
|
275
313
|
injectedJavaScript={`
|
|
276
314
|
${props.env === 'dev' ? injectedConsole() : ''}
|
|
277
|
-
${
|
|
315
|
+
${injectedBridge}
|
|
278
316
|
`}
|
|
279
317
|
javaScriptEnabled
|
|
280
318
|
domStorageEnabled
|
|
@@ -287,6 +325,7 @@ export default forwardRef<MSIRef, MSIProps>( ( props, ref ) => {
|
|
|
287
325
|
bounces={false}
|
|
288
326
|
scrollEnabled={false}
|
|
289
327
|
originWhitelist={[ baseURL ]}
|
|
328
|
+
onShouldStartLoadWithRequest={( { url }: any ) => isGatewayURL( url )}
|
|
290
329
|
onMessage={onMessage}
|
|
291
330
|
onLoadEnd={initializeConnection}
|
|
292
331
|
onError={( { nativeEvent }: any ) => reportError( new Error( nativeEvent?.description || 'WebView error') )}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a URL belongs to the MSI gateway itself.
|
|
3
|
+
*
|
|
4
|
+
* React Native's `originWhitelist` is prefix-matched and unanchored at the
|
|
5
|
+
* end, so a whitelist of `https://msi.dedot.io` also admits
|
|
6
|
+
* `https://msi.dedot.io.example.com`. That matters more in a WebView than it
|
|
7
|
+
* would in a browser: the injected bridge — and the auth secret baked into
|
|
8
|
+
* it — runs in whatever page the WebView ends up on. So the origin has to
|
|
9
|
+
* match exactly, and only a path, query or fragment separator may follow it.
|
|
10
|
+
*
|
|
11
|
+
* @param baseURL - Gateway origin, without a trailing slash
|
|
12
|
+
* @param url - URL the WebView is asking to navigate to
|
|
13
|
+
* @return - Whether the navigation is to the gateway
|
|
14
|
+
*/
|
|
15
|
+
export default function belongsToGateway( baseURL: string, url: string ): boolean {
|
|
16
|
+
if( !url || !baseURL ) return false
|
|
17
|
+
if( url === 'about:blank' || url === baseURL ) return true
|
|
18
|
+
|
|
19
|
+
return [ '/', '?', '#' ].some( separator => url.startsWith( baseURL + separator ) )
|
|
20
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SigninBody, CreateAccountBody, VerificationBody } from '@de./types'
|
|
1
2
|
import type { AccessOptions, UserSession } from '../../types/access'
|
|
2
3
|
import AccessManager from '../Access'
|
|
3
4
|
import { qs, type Res } from '../../utils'
|
|
@@ -16,15 +17,23 @@ import { qs, type Res } from '../../utils'
|
|
|
16
17
|
// single method it had could never succeed. Route shapes are taken from
|
|
17
18
|
// de.auth/src/routes/{auth,user}/index.ts.
|
|
18
19
|
//
|
|
20
|
+
// The paths now say what they do — `/auth/otp/send` rather than `/signin`,
|
|
21
|
+
// which sent a code and signed nobody in. The method names here are kept as
|
|
22
|
+
// they were: `signin` is what a caller is doing, even when what it costs is
|
|
23
|
+
// one SMS.
|
|
24
|
+
//
|
|
19
25
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Request bodies come from `@de./types`, which is what de.auth validates
|
|
29
|
+
* against — these were hand-declared here, and said `country?: string` where
|
|
30
|
+
* de.auth wants the Country OBJECT. Its schema is additionalProperties:false,
|
|
31
|
+
* so a caller trusting the SDK's type was answered `body/country must be
|
|
32
|
+
* object`. Nothing caught it because this client could not be constructed at
|
|
33
|
+
* all: AccessManager demanded a workspace context and a bearer token, which
|
|
34
|
+
* sign-in is what produces.
|
|
35
|
+
*/
|
|
36
|
+
export type { SigninBody, CreateAccountBody, VerificationBody, Country } from '@de./types'
|
|
28
37
|
|
|
29
38
|
export type SigninResult = {
|
|
30
39
|
/** What the caller must do next — verification, or account creation first. */
|
|
@@ -38,17 +47,6 @@ export type SigninResult = {
|
|
|
38
47
|
testVCode?: number
|
|
39
48
|
}
|
|
40
49
|
|
|
41
|
-
export type CreateAccountBody = {
|
|
42
|
-
phone: string
|
|
43
|
-
agreeTerms: boolean | string
|
|
44
|
-
firstName?: string
|
|
45
|
-
lastName?: string
|
|
46
|
-
photo?: string
|
|
47
|
-
type?: string
|
|
48
|
-
country?: string
|
|
49
|
-
newsletters?: boolean | string
|
|
50
|
-
}
|
|
51
|
-
|
|
52
50
|
export type VerifyResult = {
|
|
53
51
|
ctoken?: string
|
|
54
52
|
deviceId?: string
|
|
@@ -57,7 +55,17 @@ export type VerifyResult = {
|
|
|
57
55
|
|
|
58
56
|
export default class OTPAuth extends AccessManager {
|
|
59
57
|
constructor( access: AccessOptions ){
|
|
60
|
-
|
|
58
|
+
/**
|
|
59
|
+
* de.auth admits an agent that does not start with `De.service` only if
|
|
60
|
+
* the request carries an Authorization header — and the whole point of
|
|
61
|
+
* this client is the flow that has no token yet. The SDK's default
|
|
62
|
+
* platform is `proxy`, so every sign-in through it was answered 412
|
|
63
|
+
* before reaching a handler.
|
|
64
|
+
*
|
|
65
|
+
* `service` is the platform this client is for; a caller that knows
|
|
66
|
+
* better can still say so.
|
|
67
|
+
*/
|
|
68
|
+
super({ ...access, platform: access.platform || 'service' }, 'ASI' )
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
// ── Sign-in ───────────────────────────────────────────────────────────────
|
|
@@ -66,26 +74,26 @@ export default class OTPAuth extends AccessManager {
|
|
|
66
74
|
async signin( body: SigninBody ): Promise<Res<SigninResult>> {
|
|
67
75
|
if( !body.phone ) throw new Error('<phone> argument required')
|
|
68
76
|
|
|
69
|
-
return this.request<Res<SigninResult>>({ url: '/
|
|
77
|
+
return this.request<Res<SigninResult>>({ url: '/auth/otp/send', method: 'POST', body })
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
/** Step 2 — exchanges the code for a session. */
|
|
73
81
|
async verify( body: { phone: string, pvc: number, new_phone?: string }): Promise<Res<VerifyResult>> {
|
|
74
|
-
return this.request<Res<VerifyResult>>({ url: '/
|
|
82
|
+
return this.request<Res<VerifyResult>>({ url: '/auth/otp/verify', method: 'POST', body })
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
/** Step 1a — for a phone de.auth has never seen. */
|
|
78
86
|
async setAccount( body: CreateAccountBody ): Promise<Res<{ next: string }>> {
|
|
79
|
-
return this.request<Res<{ next: string }>>({ url: '/
|
|
87
|
+
return this.request<Res<{ next: string }>>({ url: '/auth/account/create', method: 'POST', body })
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
async resendSms( body: { phone: string }): Promise<Res<void>> {
|
|
83
|
-
return this.request<Res<void>>({ url: '/resend
|
|
91
|
+
return this.request<Res<void>>({ url: '/auth/otp/resend', method: 'POST', body })
|
|
84
92
|
}
|
|
85
93
|
|
|
86
|
-
/**
|
|
94
|
+
/** A state change, so a POST — it was a GET, which is cacheable. */
|
|
87
95
|
async signout( querystring?: { allDevices?: boolean | string }): Promise<Res<void>> {
|
|
88
|
-
return this.request<Res<void>>({ url: `/
|
|
96
|
+
return this.request<Res<void>>({ url: `/auth/session/revoke${qs( querystring )}`, method: 'POST' })
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
// ── The signed-in person ──────────────────────────────────────────────────
|
package/src/baseUrl.ts
CHANGED
|
@@ -41,11 +41,9 @@ export const API_SERVER_BASEURL = {
|
|
|
41
41
|
* It is not reachable through the API host: de.arch serves no `/workspace`
|
|
42
42
|
* routes, so a client pointed there gets 404 for the whole surface.
|
|
43
43
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* is also what a self-hosted De. needs, and what a dev machine running
|
|
48
|
-
* de.workspace on a port other than 22200 needs.
|
|
44
|
+
* Pass `baseUrl` to address a host that differs — what a self-hosted De.
|
|
45
|
+
* needs, and what a dev machine running de.workspace on a port other than
|
|
46
|
+
* 22200 needs.
|
|
49
47
|
*/
|
|
50
48
|
export const WSP_SERVER_BASEURL = {
|
|
51
49
|
dev: `http://localhost:${DEV_PORTS.WSP}`,
|
package/src/core.ts
CHANGED
|
@@ -59,6 +59,7 @@ export type { WorkflowsConfig } from './allend/Workflows'
|
|
|
59
59
|
export { default as Utils } from './utils'
|
|
60
60
|
|
|
61
61
|
export { default as APIError } from './error'
|
|
62
|
+
export { statusOf } from './response'
|
|
62
63
|
|
|
63
64
|
// ── Client layer (allend) ─────────────────────────────────────────────────────
|
|
64
65
|
// DClient ({ Client, Event }), the IoT socket client, and the customer/agent/
|
package/src/response.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HTTP status a returned envelope arrived with.
|
|
3
|
+
*
|
|
4
|
+
* Clients return De.'s `{ error, status, message, data }` envelope, which is
|
|
5
|
+
* what makes `status` — the precise refusal code — reachable. It carries no
|
|
6
|
+
* HTTP status, and for most callers that is right: the envelope says more than
|
|
7
|
+
* a number can.
|
|
8
|
+
*
|
|
9
|
+
* A proxy is the exception. It answers its own client with an HTTP status, and
|
|
10
|
+
* mapping every De. refusal onto a generic 400 loses the distinction between
|
|
11
|
+
* "no such order" and "you may not do that" for whoever is downstream.
|
|
12
|
+
*
|
|
13
|
+
* So the status is kept beside the response rather than inside it: the type
|
|
14
|
+
* stays exactly the envelope, and a caller that needs the number asks for it,
|
|
15
|
+
* keyed on the object it was given. Keying on identity rather than on a
|
|
16
|
+
* mutable field is what makes it safe under concurrency — two calls in flight
|
|
17
|
+
* cannot read each other's status.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const answer = await lsp.orders.get( reference )
|
|
21
|
+
* if( answer.error ) throw new HTTPException( statusOf( answer ) ?? 400, ... )
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
const statuses = new WeakMap<object, number>()
|
|
25
|
+
|
|
26
|
+
/** Record the status for a parsed response body. Called by the transport. */
|
|
27
|
+
export const rememberStatus = ( body: unknown, status: number ): void => {
|
|
28
|
+
if( body && typeof body === 'object' ) statuses.set( body as object, status )
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The HTTP status the given envelope arrived with.
|
|
33
|
+
*
|
|
34
|
+
* `undefined` when the object did not come from this SDK's transport, or when
|
|
35
|
+
* the call threw instead of returning — `APIError` carries its own `status`.
|
|
36
|
+
*/
|
|
37
|
+
export const statusOf = ( envelope: unknown ): number | undefined =>
|
|
38
|
+
envelope && typeof envelope === 'object' ? statuses.get( envelope as object ) : undefined
|
package/src/utils/index.ts
CHANGED
|
@@ -21,16 +21,35 @@ export type Res<T = any> = { error: boolean, status?: string, message?: string,
|
|
|
21
21
|
*/
|
|
22
22
|
export type Data<T> = 'data' extends keyof T ? T['data'] : undefined
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Serialize a querystring for a De. route.
|
|
26
|
+
*
|
|
27
|
+
* Array values are appended as repeated keys — `state=A&state=B` — which is
|
|
28
|
+
* what Fastify's query parser turns back into an array. `String( value )` on
|
|
29
|
+
* an array produces `A,B`, a single comma-joined string, and the route answers
|
|
30
|
+
* `querystring/state must be array`: the schema is right, the request never
|
|
31
|
+
* matched it. Every array parameter the SDK sent was malformed this way, the
|
|
32
|
+
* same shape of fault as the coordinates object that once serialized to
|
|
33
|
+
* `[object Object]`.
|
|
34
|
+
*
|
|
35
|
+
* @param params - Query parameters; null and undefined values are dropped
|
|
36
|
+
* @return - `?a=1&b=2`, or an empty string when there is nothing to send
|
|
37
|
+
*/
|
|
24
38
|
export const qs = ( params?: Record<string, any> ): string => {
|
|
25
39
|
if( !params ) return ''
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
|
|
41
|
+
const q = new URLSearchParams()
|
|
42
|
+
|
|
43
|
+
for( const [ key, value ] of Object.entries( params ) ){
|
|
44
|
+
if( value === null || value === undefined ) continue
|
|
45
|
+
|
|
46
|
+
Array.isArray( value )
|
|
47
|
+
? value.forEach( each => each !== null && each !== undefined && q.append( key, String( each ) ) )
|
|
48
|
+
: q.append( key, String( value ) )
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const query = q.toString()
|
|
52
|
+
return query ? `?${query}` : ''
|
|
34
53
|
}
|
|
35
54
|
|
|
36
55
|
export default { Stream }
|