@de./sdk 2.1.0 → 2.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.
- package/dist/allend/Access.js +20 -5
- package/dist/allend/Access.js.map +1 -1
- package/dist/allend/Agent/realtime.d.ts +8 -11
- package/dist/allend/Agent/realtime.js +10 -42
- package/dist/allend/Agent/realtime.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/allend/Realtime/index.d.ts +37 -10
- package/dist/allend/Realtime/index.js +56 -38
- package/dist/allend/Realtime/index.js.map +1 -1
- package/dist/allend/realtime-socket.d.ts +61 -0
- package/dist/allend/realtime-socket.js +122 -0
- package/dist/allend/realtime-socket.js.map +1 -0
- package/dist/backend/Auth.d.ts +14 -0
- package/dist/backend/Auth.js +16 -2
- package/dist/backend/Auth.js.map +1 -1
- package/dist/core.d.ts +2 -0
- package/dist/core.js.map +1 -1
- 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 +20 -4
- package/src/allend/Agent/realtime.ts +10 -47
- 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/allend/Realtime/index.ts +88 -56
- package/src/allend/realtime-socket.ts +171 -0
- package/src/backend/Auth.ts +30 -2
- package/src/core.ts +4 -0
- package/src/utils/index.ts +27 -8
|
@@ -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 ──────────────────────────────────────────────────
|
|
@@ -1,50 +1,99 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type { AccessOptions } from '../../types/access'
|
|
1
|
+
import type { OrderTrackingEvent } from '@de./types'
|
|
2
|
+
import type { RTLocation, Message, Peer } from '../../types'
|
|
4
3
|
|
|
5
|
-
import
|
|
6
|
-
import AccessManager from '../Access'
|
|
7
|
-
import { baseURL } from '../../baseUrl'
|
|
4
|
+
import RealtimeSocket, { type RealtimeConnectOptions } from '../realtime-socket'
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
// ─── Realtime ─────────────────────────────────────────────────────────────────
|
|
7
|
+
//
|
|
8
|
+
// The non-agent side of an order room: the person waiting for the delivery,
|
|
9
|
+
// the merchant who dispatched it, or a platform's own client connection.
|
|
10
|
+
//
|
|
11
|
+
// The flow is two steps and they are not interchangeable:
|
|
12
|
+
//
|
|
13
|
+
// connect( id, utype ) opens the socket. `id` is the uid the tracking
|
|
14
|
+
// session was minted for, and `utype` must match the
|
|
15
|
+
// `role` it was minted with.
|
|
16
|
+
// join( jrtoken ) enters the order's room.
|
|
17
|
+
//
|
|
18
|
+
// De. checks the second against the first — the JRT's seal is `role:uid`, and
|
|
19
|
+
// JOIN compares it to the connection's own `utype:id`. Connecting as one
|
|
20
|
+
// person and presenting another's token is refused, and so is connecting as
|
|
21
|
+
// 'client' with a token minted for role 'customer', which is what this client
|
|
22
|
+
// used to do unconditionally.
|
|
23
|
+
//
|
|
24
|
+
// Distinct from `AgentRealtime` (allend/Agent/realtime), which is the rider's
|
|
25
|
+
// socket and carries the consolidation response channel as well.
|
|
26
|
+
//
|
|
27
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
28
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Who this connection is, in De.'s vocabulary.
|
|
31
|
+
*
|
|
32
|
+
* A client is the party that engaged the provider — a CSP, a merchant, a
|
|
33
|
+
* branch. A customer is the person the delivery is handed to. They are not
|
|
34
|
+
* the same party and De. does not put them in the same room, so a customer
|
|
35
|
+
* app connecting as 'client' hears nothing addressed to customers and cannot
|
|
36
|
+
* redeem a customer's tracking token.
|
|
37
|
+
*/
|
|
38
|
+
export type RealtimeUserType = 'client' | 'customer' | 'vendor'
|
|
39
|
+
|
|
40
|
+
export default class Realtime extends RealtimeSocket {
|
|
41
|
+
private as: RealtimeUserType = 'client'
|
|
42
|
+
|
|
43
|
+
protected get utype(): string { return this.as }
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param id The uid the tracking session was issued for.
|
|
47
|
+
* @param utype Must equal the `role` that session was issued with.
|
|
48
|
+
* @param options `getToken` supplies a fresh access token per attempt —
|
|
49
|
+
* without it a reconnect after the token's short life is
|
|
50
|
+
* refused, and the app is left with an open socket that
|
|
51
|
+
* delivers nothing.
|
|
52
|
+
*/
|
|
53
|
+
connect( id: string, utype: RealtimeUserType = 'client', options?: RealtimeConnectOptions ): Promise<void> {
|
|
54
|
+
this.as = utype
|
|
55
|
+
return this.open( id, options )
|
|
19
56
|
}
|
|
20
57
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.nsp.on('connect', resolve )
|
|
33
|
-
this.nsp.on('connect_error', reject )
|
|
34
|
-
} )
|
|
58
|
+
// ── Order room streams (after join) ─────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The rider's position, as it moves.
|
|
62
|
+
*
|
|
63
|
+
* Rate-limited by De. to 10 per second per sender, so this is a stream and
|
|
64
|
+
* not a trickle: render from it directly rather than re-fetching anything.
|
|
65
|
+
*/
|
|
66
|
+
onLocation( fn: ( location: RTLocation ) => void ){
|
|
67
|
+
this.nsp?.on('LOCATION-CHANGE', fn )
|
|
68
|
+
return this
|
|
35
69
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
70
|
+
|
|
71
|
+
onRoute( fn: ( route: any ) => void ){
|
|
72
|
+
this.nsp?.on('ROUTE-CHANGE', fn )
|
|
73
|
+
return this
|
|
39
74
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} )
|
|
45
|
-
} )
|
|
75
|
+
|
|
76
|
+
onOrderUpdate( fn: ( event: OrderTrackingEvent ) => void ){
|
|
77
|
+
this.nsp?.on('ORDER_UPDATE', fn )
|
|
78
|
+
return this
|
|
46
79
|
}
|
|
47
80
|
|
|
81
|
+
onMessage( fn: ( message: Message ) => void ){
|
|
82
|
+
this.nsp?.on('MESSAGE', fn )
|
|
83
|
+
return this
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sendMessage( message: Message ){
|
|
87
|
+
this.nsp?.emit('MESSAGE', message )
|
|
88
|
+
return this
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Presence ────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
onJoined( fn: ( peer: Peer ) => void ){
|
|
94
|
+
this.nsp?.on('JOINED', fn )
|
|
95
|
+
return this
|
|
96
|
+
}
|
|
48
97
|
onLeft( fn: ( peer: Peer ) => void ){
|
|
49
98
|
this.nsp?.on('LEFT', fn )
|
|
50
99
|
return this
|
|
@@ -57,21 +106,4 @@ export default class Realtime extends AccessManager {
|
|
|
57
106
|
this.nsp?.on('DISCONNECTED', fn )
|
|
58
107
|
return this
|
|
59
108
|
}
|
|
60
|
-
|
|
61
|
-
// onRoute( fn: ( data: any ) => void ){
|
|
62
|
-
// this.nsp?.on('ROUTE-CHANGE', fn )
|
|
63
|
-
// return this
|
|
64
|
-
// }
|
|
65
|
-
// onStage( fn: ( data: OrderStage ) => void ){
|
|
66
|
-
// this.nsp?.on('STAGE-CHANGE', fn )
|
|
67
|
-
// return this
|
|
68
|
-
// }
|
|
69
|
-
// onLocation( fn: ( location: RTLocation ) => void ){
|
|
70
|
-
// this.nsp?.on('LOCATION-CHANGE', fn )
|
|
71
|
-
// return this
|
|
72
|
-
// }
|
|
73
|
-
// onMessage( fn: ( payload: Message ) => void ){
|
|
74
|
-
// this.nsp?.on('MESSAGE', fn )
|
|
75
|
-
// return this
|
|
76
|
-
// }
|
|
77
|
-
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { SocketAuthCredentials } from '../types/auth'
|
|
2
|
+
import type { AccessOptions } from '../types/access'
|
|
3
|
+
|
|
4
|
+
import io, { Socket } from 'socket.io-client'
|
|
5
|
+
import AccessManager from './Access'
|
|
6
|
+
import { baseURL } from '../baseUrl'
|
|
7
|
+
|
|
8
|
+
// ─── RealtimeSocket ───────────────────────────────────────────────────────────
|
|
9
|
+
//
|
|
10
|
+
// What `Realtime` and `AgentRealtime` share: opening the connection, and
|
|
11
|
+
// getting back into the rooms afterwards.
|
|
12
|
+
//
|
|
13
|
+
// Both used to hand socket.io a plain object for `auth` and call `join` once,
|
|
14
|
+
// which works exactly until the first reconnect — and on a phone the first
|
|
15
|
+
// reconnect is a lift doorway. Two things break there, both silently:
|
|
16
|
+
//
|
|
17
|
+
// · The token. socket.io captures the auth literal once and replays it on
|
|
18
|
+
// every attempt. De.'s access tokens are short-lived, so the replayed one
|
|
19
|
+
// is stale precisely on the reconnect after an outage long enough to have
|
|
20
|
+
// crossed an expiry. De. refuses the handshake — and until 2026-09-08 that
|
|
21
|
+
// refusal took de.arch's whole process down with it.
|
|
22
|
+
//
|
|
23
|
+
// · The rooms. Room membership is per-connection and De. does not restore
|
|
24
|
+
// it. A reconnected socket is connected to the server and in none of the
|
|
25
|
+
// rooms it was in, so it stays open, reports itself healthy, and delivers
|
|
26
|
+
// nothing. That is the worse of the two failures because nothing anywhere
|
|
27
|
+
// reports it.
|
|
28
|
+
//
|
|
29
|
+
// So `auth` is a function, evaluated per attempt, and every join is remembered
|
|
30
|
+
// and replayed.
|
|
31
|
+
//
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export type RealtimeConnectOptions = {
|
|
35
|
+
/**
|
|
36
|
+
* A fresh access token for each connection attempt.
|
|
37
|
+
*
|
|
38
|
+
* An app holds tokens it was given rather than credentials it can mint, so
|
|
39
|
+
* this is usually "ask my backend for a new tracking session". Omitted, the
|
|
40
|
+
* token this client was constructed with is used on every attempt, which is
|
|
41
|
+
* correct only for a process that rotates it in place.
|
|
42
|
+
*/
|
|
43
|
+
getToken?: () => string | Promise<string>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default abstract class RealtimeSocket extends AccessManager {
|
|
47
|
+
protected nsp?: Socket
|
|
48
|
+
protected iosHost: string
|
|
49
|
+
protected id?: string
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Every room this socket asked to be in, by the token that got it there.
|
|
53
|
+
*
|
|
54
|
+
* Kept so a reconnect can restore them. A JRT outlives an ordinary blip, so
|
|
55
|
+
* replaying it is right; when one has expired the rejoin fails and
|
|
56
|
+
* `onRejoinError` is how the app learns to ask for a new session rather than
|
|
57
|
+
* sitting on a silent connection.
|
|
58
|
+
*/
|
|
59
|
+
private rooms = new Set<string>()
|
|
60
|
+
private rejoinError?: ( error: Error ) => void
|
|
61
|
+
|
|
62
|
+
constructor( access: AccessOptions ){
|
|
63
|
+
super( access, 'API' )
|
|
64
|
+
|
|
65
|
+
// Socket.io is served by the same de.arch API server, so an explicit
|
|
66
|
+
// origin governs the socket exactly as it governs the requests.
|
|
67
|
+
this.iosHost = access.baseUrl?.replace( /\/+$/, '' )
|
|
68
|
+
|| baseURL('API', access.env || 'dev', access.devHostname )
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The `utype` this client connects as. Must match the JRT's `role`. */
|
|
72
|
+
protected abstract get utype(): string
|
|
73
|
+
|
|
74
|
+
protected open( id: string, options: RealtimeConnectOptions = {} ): Promise<void> {
|
|
75
|
+
this.id = id
|
|
76
|
+
|
|
77
|
+
return new Promise( ( resolve, reject ) => {
|
|
78
|
+
const credentials = async (): Promise<SocketAuthCredentials> => ({
|
|
79
|
+
utype: this.utype,
|
|
80
|
+
id,
|
|
81
|
+
remoteOrigin: globalThis?.window ? globalThis.origin : this.remoteOrigin as string,
|
|
82
|
+
accessToken: options.getToken ? await options.getToken() : this.accessToken as string
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
this.nsp = io( this.iosHost, {
|
|
86
|
+
// The function form — re-evaluated before every attempt, which is the
|
|
87
|
+
// whole point. The object form is read once, at construction.
|
|
88
|
+
auth: ( cb: ( data: any ) => void ) => {
|
|
89
|
+
credentials()
|
|
90
|
+
.then( cb )
|
|
91
|
+
// A token the app could not fetch is still an attempt: let De.
|
|
92
|
+
// refuse it and surface a connect_error, rather than hanging.
|
|
93
|
+
.catch( () => cb({ utype: this.utype, id }) )
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
this.nsp.on('connect', () => {
|
|
98
|
+
this.restore()
|
|
99
|
+
resolve()
|
|
100
|
+
})
|
|
101
|
+
this.nsp.on('connect_error', reject )
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Re-enter every room this socket was in before the connection dropped. */
|
|
106
|
+
private restore(): void {
|
|
107
|
+
for( const jrtoken of this.rooms )
|
|
108
|
+
this.nsp?.emit('JOIN', jrtoken, ( errmess?: string ) => {
|
|
109
|
+
if( !errmess ) return
|
|
110
|
+
|
|
111
|
+
this.rooms.delete( jrtoken )
|
|
112
|
+
this.rejoinError?.( new Error( errmess ) )
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Enter an order room with a Join Room Token from
|
|
118
|
+
* `GET /v1/tracking/:reference?role=…`.
|
|
119
|
+
*/
|
|
120
|
+
join( jrtoken: string ): Promise<boolean> {
|
|
121
|
+
return new Promise( ( resolve, reject ) => {
|
|
122
|
+
this.nsp?.emit('JOIN', jrtoken, ( errmess?: string ) => {
|
|
123
|
+
if( errmess ) return reject( new Error( errmess ) )
|
|
124
|
+
|
|
125
|
+
this.rooms.add( jrtoken )
|
|
126
|
+
resolve( true )
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Stop replaying a room — the order is closed, or the app navigated away. */
|
|
132
|
+
leave( jrtoken: string ): void {
|
|
133
|
+
this.rooms.delete( jrtoken )
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A room could not be re-entered after a reconnect.
|
|
138
|
+
*
|
|
139
|
+
* Almost always an expired JRT. The app's answer is to ask its backend for a
|
|
140
|
+
* fresh tracking session and `join` again — and, since it is reconnecting,
|
|
141
|
+
* to `replay` from the `since` that came with it.
|
|
142
|
+
*/
|
|
143
|
+
onRejoinError( fn: ( error: Error ) => void ){
|
|
144
|
+
this.rejoinError = fn
|
|
145
|
+
return this
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Fires on every reconnect, which is where a client should replay its gap. */
|
|
149
|
+
onReconnect( fn: () => void ){
|
|
150
|
+
this.nsp?.io.on('reconnect', fn )
|
|
151
|
+
return this
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
disconnect(){
|
|
155
|
+
this.rooms.clear()
|
|
156
|
+
this.nsp?.disconnect()
|
|
157
|
+
return true
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Ask for everything a room emitted while this socket was away.
|
|
162
|
+
*
|
|
163
|
+
* `since` is the timestamp the tracking session came back with, or that of
|
|
164
|
+
* the last event actually seen. Reconnecting without it leaves a hole
|
|
165
|
+
* exactly as long as the outage.
|
|
166
|
+
*/
|
|
167
|
+
replay( reference: string, since: number ){
|
|
168
|
+
this.nsp?.emit('REPLAY', { room: `order:${reference}`, since })
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
}
|
package/src/backend/Auth.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { baseURL } from '../baseUrl'
|
|
2
2
|
import type { AuthRequestOptions } from '../types/auth'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* When to rotate, in minutes.
|
|
6
|
+
*
|
|
7
|
+
* De.'s own default token life is four minutes, so this beats it by fifteen
|
|
8
|
+
* seconds. It is a default and not a constant because the server's life is
|
|
9
|
+
* configurable — `API_ACCESS_TOKEN_EXPIRY` — and these two numbers are
|
|
10
|
+
* otherwise free to drift apart in the direction that breaks: a deployment
|
|
11
|
+
* that shortens the token to two minutes leaves every client rotating a
|
|
12
|
+
* credential that expired ninety seconds ago, and every call in between is
|
|
13
|
+
* answered 401 with nothing to say why.
|
|
14
|
+
*/
|
|
15
|
+
const ACCESS_TOKEN_EXPIRY = 3.75
|
|
5
16
|
|
|
6
17
|
// ─── Config ───────────────────────────────────────────────────────────────────
|
|
7
18
|
//
|
|
@@ -27,7 +38,20 @@ export type AuthConfig = {
|
|
|
27
38
|
* credentials are. Omit it for pure machine-to-machine access.
|
|
28
39
|
*/
|
|
29
40
|
uid?: string
|
|
41
|
+
/**
|
|
42
|
+
* Rotate the token ahead of expiry.
|
|
43
|
+
*
|
|
44
|
+
* Off by default, which is the trap: a long-lived process that forgets it
|
|
45
|
+
* works perfectly until the first expiry and then fails every call until
|
|
46
|
+
* it is restarted. Anything holding a token for longer than one request
|
|
47
|
+
* wants this on.
|
|
48
|
+
*/
|
|
30
49
|
autorefresh?: boolean
|
|
50
|
+
/**
|
|
51
|
+
* Minutes between rotations. Defaults to 3.75 — set it below the De.
|
|
52
|
+
* deployment's `API_ACCESS_TOKEN_EXPIRY` when that is not the default 4.
|
|
53
|
+
*/
|
|
54
|
+
rotateAfterMins?: number
|
|
31
55
|
onNewToken?: ( token: string ) => void
|
|
32
56
|
/** Host to substitute for `localhost` in `dev` (Eg. a native emulator) */
|
|
33
57
|
devHostname?: string
|
|
@@ -51,6 +75,7 @@ export default class Auth {
|
|
|
51
75
|
private remoteOrigin?: string
|
|
52
76
|
private refreshTimer?: NodeJS.Timeout
|
|
53
77
|
private autorefresh?: boolean
|
|
78
|
+
private rotateAfterMins: number
|
|
54
79
|
private onNewToken?: (token: string) => void
|
|
55
80
|
private baseURL: string
|
|
56
81
|
private isRotating: boolean = false
|
|
@@ -71,6 +96,9 @@ export default class Auth {
|
|
|
71
96
|
this.version = config.version || 1
|
|
72
97
|
this.baseURL = baseURL('API', this.env, config.devHostname )
|
|
73
98
|
this.autorefresh = config.autorefresh || false
|
|
99
|
+
this.rotateAfterMins = config.rotateAfterMins && config.rotateAfterMins > 0
|
|
100
|
+
? config.rotateAfterMins
|
|
101
|
+
: ACCESS_TOKEN_EXPIRY
|
|
74
102
|
this.onNewToken = config.onNewToken
|
|
75
103
|
}
|
|
76
104
|
|
|
@@ -114,7 +142,7 @@ export default class Auth {
|
|
|
114
142
|
if( !this.autorefresh ) return
|
|
115
143
|
|
|
116
144
|
this.clearRotation()
|
|
117
|
-
this.refreshTimer = setTimeout( () => this.rotateToken(),
|
|
145
|
+
this.refreshTimer = setTimeout( () => this.rotateToken(), this.rotateAfterMins * 60 * 1000 )
|
|
118
146
|
}
|
|
119
147
|
|
|
120
148
|
/**
|
package/src/core.ts
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
export { default as Auth } from './backend/Auth'
|
|
23
23
|
export type { AuthConfig } from './backend/Auth'
|
|
24
24
|
|
|
25
|
+
// Order-room sockets — shared connect/rejoin behaviour
|
|
26
|
+
export type { RealtimeConnectOptions } from './allend/realtime-socket'
|
|
27
|
+
export type { RealtimeUserType } from './allend/Realtime'
|
|
28
|
+
|
|
25
29
|
// Client-side OTP auth (de.auth service)
|
|
26
30
|
export { default as OTPAuth } from './allend/OTPAuth'
|
|
27
31
|
|
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 }
|