@hanzo/base 0.2.0 → 0.2.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/{chunk-LBAV5X5P.js → chunk-3WOGNODW.js} +17 -17
- package/dist/{chunk-LBAV5X5P.js.map → chunk-3WOGNODW.js.map} +1 -1
- package/dist/chunk-DC6LDHAD.js +244 -0
- package/dist/chunk-DC6LDHAD.js.map +1 -0
- package/dist/{chunk-5NHFZRMO.js → chunk-FTBTJJQY.js} +3 -3
- package/dist/chunk-FTBTJJQY.js.map +1 -0
- package/dist/client-Ckpi5rr9.d.ts +368 -0
- package/dist/compat/index.d.ts +203 -1
- package/dist/compat/index.js +2 -1
- package/dist/compat/index.js.map +1 -1
- package/dist/core/index.d.ts +7 -363
- package/dist/core/index.js +2 -1
- package/dist/crdt/index.d.ts +1 -1
- package/dist/crdt/index.js +1 -1
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +5 -5
- package/dist/react/index.js.map +1 -1
- package/package.json +4 -6
- package/src/compat/index.ts +33 -10
- package/src/core/auth-stores.ts +222 -0
- package/src/core/client.ts +12 -12
- package/src/core/collection.ts +2 -2
- package/src/core/index.ts +27 -2
- package/src/core/realtime.ts +3 -3
- package/src/core/tokens.ts +139 -0
- package/src/core/types.ts +100 -0
- package/src/crdt/sync.ts +1 -1
- package/src/react/hooks.ts +3 -3
- package/dist/chunk-5NHFZRMO.js.map +0 -1
package/src/compat/index.ts
CHANGED
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @hanzo/base/compat -- drop-in for legacy client imports.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Re-exports every symbol consumers used to import from the legacy
|
|
5
|
+
* client. Switching the specifier (and only the specifier) carries
|
|
6
|
+
* existing code over with no further changes:
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* - import Base, { LocalAuthStore } from '@hanzo/base/compat'
|
|
9
|
+
*
|
|
10
|
+
* Everything here is implemented natively in @hanzo/base — no
|
|
11
|
+
* upstream package dependency.
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
export {
|
|
12
15
|
default,
|
|
13
|
-
default as PocketBase,
|
|
14
16
|
default as Base,
|
|
17
|
+
BaseClient,
|
|
18
|
+
MemoryAuthStore,
|
|
15
19
|
LocalAuthStore,
|
|
16
20
|
AsyncAuthStore,
|
|
17
|
-
BaseAuthStore,
|
|
18
|
-
isTokenExpired,
|
|
19
21
|
ClientResponseError,
|
|
22
|
+
isTokenExpired,
|
|
23
|
+
getTokenPayload,
|
|
20
24
|
cookieParse,
|
|
21
25
|
cookieSerialize,
|
|
22
|
-
getTokenPayload,
|
|
23
26
|
normalizeUnknownQueryParams,
|
|
24
|
-
} from '
|
|
27
|
+
} from '../core/index.js'
|
|
28
|
+
|
|
29
|
+
export type {
|
|
30
|
+
AuthStore,
|
|
31
|
+
AuthChangeCallback,
|
|
32
|
+
ClientConfig,
|
|
33
|
+
ListOptions,
|
|
34
|
+
ListResult,
|
|
35
|
+
BaseAuthStore,
|
|
36
|
+
BaseRecord,
|
|
37
|
+
RecordModel,
|
|
38
|
+
CollectionField,
|
|
39
|
+
CollectionModel,
|
|
40
|
+
RecordQueryOptions,
|
|
41
|
+
RecordFullListOptions,
|
|
42
|
+
FileOptions,
|
|
43
|
+
AuthResponse,
|
|
44
|
+
OAuth2Options,
|
|
45
|
+
ClientResponseErrorData,
|
|
46
|
+
CookieSerializeOptions,
|
|
47
|
+
} from '../core/index.js'
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth-store implementations beyond the in-memory default.
|
|
3
|
+
*
|
|
4
|
+
* Native `LocalAuthStore` / `AsyncAuthStore` implementations so the
|
|
5
|
+
* SDK has zero upstream client dependency. The shape matches the
|
|
6
|
+
* legacy client's auth stores so existing apps migrating to
|
|
7
|
+
* `@hanzo/base` keep working.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AuthStore, AuthChangeCallback } from './client.js'
|
|
11
|
+
import type { BaseRecord } from './state.js'
|
|
12
|
+
|
|
13
|
+
interface PersistedAuth {
|
|
14
|
+
token: string
|
|
15
|
+
record: BaseRecord | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Type alias matching the upstream interface name. New code should use
|
|
20
|
+
* `AuthStore`.
|
|
21
|
+
*/
|
|
22
|
+
export type BaseAuthStore = AuthStore
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Synchronous localStorage-backed auth store. Suitable for browser SPAs.
|
|
26
|
+
* Falls back to in-memory storage when `window.localStorage` is absent
|
|
27
|
+
* (SSR, sandboxed iframes, etc.).
|
|
28
|
+
*/
|
|
29
|
+
export class LocalAuthStore implements AuthStore {
|
|
30
|
+
private _storageKey: string
|
|
31
|
+
private _listeners = new Set<AuthChangeCallback>()
|
|
32
|
+
private _memToken = ''
|
|
33
|
+
private _memRecord: BaseRecord | null = null
|
|
34
|
+
|
|
35
|
+
constructor(storageKey: string = 'base_auth') {
|
|
36
|
+
this._storageKey = storageKey
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private get _storage(): Storage | null {
|
|
40
|
+
try {
|
|
41
|
+
if (typeof globalThis !== 'undefined' && 'localStorage' in globalThis) {
|
|
42
|
+
return (globalThis as { localStorage?: Storage }).localStorage ?? null
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Access can throw in some sandboxed contexts; fall through.
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private _read(): PersistedAuth {
|
|
51
|
+
const storage = this._storage
|
|
52
|
+
if (!storage) return { token: this._memToken, record: this._memRecord }
|
|
53
|
+
try {
|
|
54
|
+
const raw = storage.getItem(this._storageKey)
|
|
55
|
+
if (!raw) return { token: '', record: null }
|
|
56
|
+
const parsed = JSON.parse(raw) as Partial<PersistedAuth>
|
|
57
|
+
return { token: parsed.token ?? '', record: parsed.record ?? null }
|
|
58
|
+
} catch {
|
|
59
|
+
return { token: '', record: null }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private _write(value: PersistedAuth): void {
|
|
64
|
+
const storage = this._storage
|
|
65
|
+
if (!storage) {
|
|
66
|
+
this._memToken = value.token
|
|
67
|
+
this._memRecord = value.record
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
if (!value.token) {
|
|
72
|
+
storage.removeItem(this._storageKey)
|
|
73
|
+
} else {
|
|
74
|
+
storage.setItem(this._storageKey, JSON.stringify(value))
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Quota / serialization errors fall back to memory.
|
|
78
|
+
this._memToken = value.token
|
|
79
|
+
this._memRecord = value.record
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get token(): string {
|
|
84
|
+
return this._read().token
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get record(): BaseRecord | null {
|
|
88
|
+
return this._read().record
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get isValid(): boolean {
|
|
92
|
+
const token = this.token
|
|
93
|
+
if (!token) return false
|
|
94
|
+
try {
|
|
95
|
+
const parts = token.split('.')
|
|
96
|
+
if (parts.length !== 3) return false
|
|
97
|
+
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')))
|
|
98
|
+
if (typeof payload.exp === 'number') return payload.exp > Date.now() / 1000
|
|
99
|
+
return true
|
|
100
|
+
} catch {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
save(token: string, record: BaseRecord | null): void {
|
|
106
|
+
this._write({ token, record })
|
|
107
|
+
this._notify(token, record)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
clear(): void {
|
|
111
|
+
this._write({ token: '', record: null })
|
|
112
|
+
this._notify('', null)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onChange(callback: AuthChangeCallback): () => void {
|
|
116
|
+
this._listeners.add(callback)
|
|
117
|
+
return () => {
|
|
118
|
+
this._listeners.delete(callback)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private _notify(token: string, record: BaseRecord | null): void {
|
|
123
|
+
for (const cb of this._listeners) {
|
|
124
|
+
try {
|
|
125
|
+
cb(token, record)
|
|
126
|
+
} catch {
|
|
127
|
+
// listener errors must not break notification
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Async auth store — wraps any async storage backend (cookies via
|
|
135
|
+
* fetch, encrypted SecureStore on mobile, KV namespace on the edge,
|
|
136
|
+
* etc.). Reads and writes are buffered through an in-memory cache so
|
|
137
|
+
* the `token`/`record` accessors stay synchronous (matching upstream).
|
|
138
|
+
*
|
|
139
|
+
* Pass a `save` function that persists the serialized payload to your
|
|
140
|
+
* backend, and an `initial` value loaded synchronously at app boot.
|
|
141
|
+
*/
|
|
142
|
+
export class AsyncAuthStore implements AuthStore {
|
|
143
|
+
private _token = ''
|
|
144
|
+
private _record: BaseRecord | null = null
|
|
145
|
+
private _listeners = new Set<AuthChangeCallback>()
|
|
146
|
+
private readonly _save: (serialized: string) => Promise<void> | void
|
|
147
|
+
private readonly _clear?: () => Promise<void> | void
|
|
148
|
+
|
|
149
|
+
constructor(config: {
|
|
150
|
+
save: (serialized: string) => Promise<void> | void
|
|
151
|
+
initial?: string | null
|
|
152
|
+
clear?: () => Promise<void> | void
|
|
153
|
+
}) {
|
|
154
|
+
this._save = config.save
|
|
155
|
+
this._clear = config.clear
|
|
156
|
+
if (config.initial) {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(config.initial) as Partial<PersistedAuth>
|
|
159
|
+
this._token = parsed.token ?? ''
|
|
160
|
+
this._record = parsed.record ?? null
|
|
161
|
+
} catch {
|
|
162
|
+
// ignore malformed initial value
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get token(): string {
|
|
168
|
+
return this._token
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get record(): BaseRecord | null {
|
|
172
|
+
return this._record
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
get isValid(): boolean {
|
|
176
|
+
if (!this._token) return false
|
|
177
|
+
try {
|
|
178
|
+
const parts = this._token.split('.')
|
|
179
|
+
if (parts.length !== 3) return false
|
|
180
|
+
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')))
|
|
181
|
+
if (typeof payload.exp === 'number') return payload.exp > Date.now() / 1000
|
|
182
|
+
return true
|
|
183
|
+
} catch {
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
save(token: string, record: BaseRecord | null): void {
|
|
189
|
+
this._token = token
|
|
190
|
+
this._record = record
|
|
191
|
+
void this._save(JSON.stringify({ token, record }))
|
|
192
|
+
this._notify()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
clear(): void {
|
|
196
|
+
this._token = ''
|
|
197
|
+
this._record = null
|
|
198
|
+
if (this._clear) {
|
|
199
|
+
void this._clear()
|
|
200
|
+
} else {
|
|
201
|
+
void this._save('')
|
|
202
|
+
}
|
|
203
|
+
this._notify()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
onChange(callback: AuthChangeCallback): () => void {
|
|
207
|
+
this._listeners.add(callback)
|
|
208
|
+
return () => {
|
|
209
|
+
this._listeners.delete(callback)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private _notify(): void {
|
|
214
|
+
for (const cb of this._listeners) {
|
|
215
|
+
try {
|
|
216
|
+
cb(this._token, this._record)
|
|
217
|
+
} catch {
|
|
218
|
+
// listener errors must not break notification
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
package/src/core/client.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Two API surfaces:
|
|
5
5
|
*
|
|
6
|
-
* 1.
|
|
6
|
+
* 1. Base-compatible: client.collection('posts').getList(...)
|
|
7
7
|
* 2. Direct (convenience): client.list('posts', { filter: '...' })
|
|
8
8
|
*
|
|
9
9
|
* Both share the same QueryStore, RealtimeService, and AuthStore.
|
|
@@ -105,7 +105,7 @@ export class FileService {
|
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* Build a full URL to a record file.
|
|
108
|
-
* Compatible with
|
|
108
|
+
* Compatible with Base's files.getURL().
|
|
109
109
|
*/
|
|
110
110
|
getURL(record: BaseRecord, filename: string, options?: FileOptions): string {
|
|
111
111
|
if (!filename || !record.id) return ''
|
|
@@ -113,7 +113,7 @@ export class FileService {
|
|
|
113
113
|
const collectionId = (record.collectionId ?? record.collectionName ?? '') as string
|
|
114
114
|
const parts = [
|
|
115
115
|
this._baseUrl,
|
|
116
|
-
'
|
|
116
|
+
'v1',
|
|
117
117
|
'files',
|
|
118
118
|
encodeURIComponent(collectionId),
|
|
119
119
|
encodeURIComponent(record.id),
|
|
@@ -200,7 +200,7 @@ export class BaseClient {
|
|
|
200
200
|
})
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
// ----
|
|
203
|
+
// ---- Base-compatible collection() API ------------------------------------
|
|
204
204
|
|
|
205
205
|
/** Get or create a CollectionService for the given name/id. */
|
|
206
206
|
collection(nameOrId: string): CollectionService {
|
|
@@ -269,7 +269,7 @@ export class BaseClient {
|
|
|
269
269
|
if (options?.perPage) params.set('perPage', String(options.perPage))
|
|
270
270
|
|
|
271
271
|
const qs = params.toString()
|
|
272
|
-
const path = `/
|
|
272
|
+
const path = `/v1/collections/${encodeURIComponent(collection)}/records${qs ? '?' + qs : ''}`
|
|
273
273
|
const result = await this._request<ListResult>('GET', path)
|
|
274
274
|
|
|
275
275
|
this.store.setQuery(collection, options?.filter ?? '', result.items)
|
|
@@ -281,26 +281,26 @@ export class BaseClient {
|
|
|
281
281
|
if (options?.expand) params.set('expand', options.expand)
|
|
282
282
|
if (options?.fields) params.set('fields', options.fields)
|
|
283
283
|
const qs = params.toString()
|
|
284
|
-
const path = `/
|
|
284
|
+
const path = `/v1/collections/${encodeURIComponent(collection)}/records/${encodeURIComponent(id)}${qs ? '?' + qs : ''}`
|
|
285
285
|
return this._request<BaseRecord>('GET', path)
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
async create(collection: string, data: Record<string, unknown>): Promise<BaseRecord> {
|
|
289
|
-
const path = `/
|
|
289
|
+
const path = `/v1/collections/${encodeURIComponent(collection)}/records`
|
|
290
290
|
const record = await this._request<BaseRecord>('POST', path, data)
|
|
291
291
|
this.store.applyServerUpdate(collection, 'create', record)
|
|
292
292
|
return record
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
async update(collection: string, id: string, data: Record<string, unknown>): Promise<BaseRecord> {
|
|
296
|
-
const path = `/
|
|
296
|
+
const path = `/v1/collections/${encodeURIComponent(collection)}/records/${encodeURIComponent(id)}`
|
|
297
297
|
const record = await this._request<BaseRecord>('PATCH', path, data)
|
|
298
298
|
this.store.applyServerUpdate(collection, 'update', record)
|
|
299
299
|
return record
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
async delete(collection: string, id: string): Promise<void> {
|
|
303
|
-
const path = `/
|
|
303
|
+
const path = `/v1/collections/${encodeURIComponent(collection)}/records/${encodeURIComponent(id)}`
|
|
304
304
|
await this._request<void>('DELETE', path)
|
|
305
305
|
this.store.applyServerUpdate(collection, 'delete', { id } as BaseRecord)
|
|
306
306
|
}
|
|
@@ -312,7 +312,7 @@ export class BaseClient {
|
|
|
312
312
|
identity: string,
|
|
313
313
|
password: string,
|
|
314
314
|
): Promise<{ token: string; record: BaseRecord }> {
|
|
315
|
-
const path = `/
|
|
315
|
+
const path = `/v1/collections/${encodeURIComponent(collection)}/auth-with-password`
|
|
316
316
|
const result = await this._request<{ token: string; record: BaseRecord }>('POST', path, {
|
|
317
317
|
identity,
|
|
318
318
|
password,
|
|
@@ -329,7 +329,7 @@ export class BaseClient {
|
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
async refreshAuth(collection: string): Promise<{ token: string; record: BaseRecord }> {
|
|
332
|
-
const path = `/
|
|
332
|
+
const path = `/v1/collections/${encodeURIComponent(collection)}/auth-refresh`
|
|
333
333
|
const result = await this._request<{ token: string; record: BaseRecord }>('POST', path)
|
|
334
334
|
this.authStore.save(result.token, result.record)
|
|
335
335
|
return result
|
|
@@ -391,7 +391,7 @@ export class BaseClient {
|
|
|
391
391
|
// ---- Health check -------------------------------------------------------
|
|
392
392
|
|
|
393
393
|
async health(): Promise<{ code: number; message: string }> {
|
|
394
|
-
return this.send('/
|
|
394
|
+
return this.send('/v1/health')
|
|
395
395
|
}
|
|
396
396
|
|
|
397
397
|
// ---- Realtime convenience -----------------------------------------------
|
package/src/core/collection.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CollectionService -- typed CRUD + auth + realtime for a single collection.
|
|
3
3
|
*
|
|
4
|
-
* API-compatible with
|
|
4
|
+
* API-compatible with the upstream RecordService interface, extended with
|
|
5
5
|
* reactive features (subscribe/unsubscribe, optimistic writes).
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -379,7 +379,7 @@ export class CollectionService {
|
|
|
379
379
|
// ---- Internal -----------------------------------------------------------
|
|
380
380
|
|
|
381
381
|
private _collectionPath(): string {
|
|
382
|
-
return `/
|
|
382
|
+
return `/v1/collections/${encodeURIComponent(this.collectionIdOrName)}`
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
private _applyOptions(params: URLSearchParams, options?: RecordQueryOptions): void {
|
package/src/core/index.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
2
|
+
* @hanzo/base -- Core entry point.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Exposes the full Base client surface natively. No upstream package
|
|
5
|
+
* dependency — every type and helper the SDK needs lives here.
|
|
5
6
|
*/
|
|
6
7
|
|
|
8
|
+
import { BaseClient } from './client.js'
|
|
9
|
+
|
|
7
10
|
// Client
|
|
8
11
|
export { BaseClient, BaseClientError, MemoryAuthStore, FileService } from './client.js'
|
|
9
12
|
export type { AuthStore, AuthChangeCallback, ClientConfig, ListOptions, ListResult } from './client.js'
|
|
@@ -35,3 +38,25 @@ export type {
|
|
|
35
38
|
RealtimeCallback,
|
|
36
39
|
ConnectionCallback,
|
|
37
40
|
} from './realtime.js'
|
|
41
|
+
|
|
42
|
+
// Schema types — admin UI consumers
|
|
43
|
+
export type { CollectionField, CollectionModel, RecordModel } from './types.js'
|
|
44
|
+
|
|
45
|
+
// Auth stores — beyond the in-memory default
|
|
46
|
+
export { LocalAuthStore, AsyncAuthStore } from './auth-stores.js'
|
|
47
|
+
export type { BaseAuthStore } from './auth-stores.js'
|
|
48
|
+
|
|
49
|
+
// Token + cookie helpers
|
|
50
|
+
export {
|
|
51
|
+
getTokenPayload,
|
|
52
|
+
isTokenExpired,
|
|
53
|
+
cookieParse,
|
|
54
|
+
cookieSerialize,
|
|
55
|
+
normalizeUnknownQueryParams,
|
|
56
|
+
} from './tokens.js'
|
|
57
|
+
export type { CookieSerializeOptions } from './tokens.js'
|
|
58
|
+
|
|
59
|
+
// Default export — matches the upstream client default. Consumers can
|
|
60
|
+
// `import Base from '@hanzo/base'` and continue calling `new Base(url)`
|
|
61
|
+
// exactly as they did against the upstream package.
|
|
62
|
+
export default BaseClient
|
package/src/core/realtime.ts
CHANGED
|
@@ -157,10 +157,10 @@ export class RealtimeService {
|
|
|
157
157
|
this._intentionalDisconnect = false
|
|
158
158
|
this._setState('connecting')
|
|
159
159
|
|
|
160
|
-
const url = `${this._baseUrl}/
|
|
160
|
+
const url = `${this._baseUrl}/v1/realtime`
|
|
161
161
|
this._eventSource = new EventSource(url)
|
|
162
162
|
|
|
163
|
-
this._eventSource.addEventListener('
|
|
163
|
+
this._eventSource.addEventListener('CONNECT', (e: MessageEvent) => {
|
|
164
164
|
try {
|
|
165
165
|
const data = JSON.parse(e.data) as { clientId: string }
|
|
166
166
|
this._clientId = data.clientId
|
|
@@ -240,7 +240,7 @@ export class RealtimeService {
|
|
|
240
240
|
|
|
241
241
|
const token = this._getToken()
|
|
242
242
|
try {
|
|
243
|
-
await fetch(`${this._baseUrl}/
|
|
243
|
+
await fetch(`${this._baseUrl}/v1/realtime`, {
|
|
244
244
|
method: 'POST',
|
|
245
245
|
headers: {
|
|
246
246
|
'Content-Type': 'application/json',
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWT + cookie helpers — small utilities the compat layer used to
|
|
3
|
+
* re-export from the upstream client. Implemented natively so the SDK
|
|
4
|
+
* has zero upstream dependency.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Decode the payload of a JWT without verifying its signature.
|
|
9
|
+
* Returns `null` for malformed tokens. Safe for browser + Node use
|
|
10
|
+
* (relies only on global `atob`).
|
|
11
|
+
*/
|
|
12
|
+
export function getTokenPayload<T = Record<string, unknown>>(token: string): T | null {
|
|
13
|
+
if (!token) return null
|
|
14
|
+
const parts = token.split('.')
|
|
15
|
+
if (parts.length !== 3) return null
|
|
16
|
+
try {
|
|
17
|
+
const padded = parts[1].replace(/-/g, '+').replace(/_/g, '/')
|
|
18
|
+
return JSON.parse(atob(padded)) as T
|
|
19
|
+
} catch {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check whether a JWT's `exp` claim has passed.
|
|
26
|
+
* `expirationThreshold` (seconds) is subtracted from `exp` to expire
|
|
27
|
+
* tokens early — set this when you want to refresh before the actual
|
|
28
|
+
* expiration. Tokens without an `exp` claim are treated as
|
|
29
|
+
* non-expiring.
|
|
30
|
+
*/
|
|
31
|
+
export function isTokenExpired(token: string, expirationThreshold: number = 0): boolean {
|
|
32
|
+
const payload = getTokenPayload<{ exp?: number }>(token)
|
|
33
|
+
if (!payload || typeof payload.exp !== 'number') return true
|
|
34
|
+
return payload.exp - expirationThreshold <= Date.now() / 1000
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Cookie parsing — extracts a name→value map from a Set-Cookie or
|
|
39
|
+
* Cookie header value. Decodes URI-encoded values. Mirrors the
|
|
40
|
+
* `cookie` npm package's signature so it's drop-in for the upstream
|
|
41
|
+
* client's `cookieParse`.
|
|
42
|
+
*/
|
|
43
|
+
export function cookieParse(input: string): Record<string, string> {
|
|
44
|
+
const out: Record<string, string> = {}
|
|
45
|
+
if (!input) return out
|
|
46
|
+
for (const segment of input.split(/;\s*/)) {
|
|
47
|
+
if (!segment) continue
|
|
48
|
+
const eq = segment.indexOf('=')
|
|
49
|
+
if (eq < 0) continue
|
|
50
|
+
const key = segment.slice(0, eq).trim()
|
|
51
|
+
let value = segment.slice(eq + 1).trim()
|
|
52
|
+
if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1)
|
|
53
|
+
try {
|
|
54
|
+
out[key] = decodeURIComponent(value)
|
|
55
|
+
} catch {
|
|
56
|
+
out[key] = value
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CookieSerializeOptions {
|
|
63
|
+
encode?: (value: string) => string
|
|
64
|
+
maxAge?: number
|
|
65
|
+
domain?: string
|
|
66
|
+
path?: string
|
|
67
|
+
expires?: Date
|
|
68
|
+
httpOnly?: boolean
|
|
69
|
+
secure?: boolean
|
|
70
|
+
sameSite?: 'strict' | 'lax' | 'none' | boolean
|
|
71
|
+
priority?: 'low' | 'medium' | 'high'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Cookie serialization — builds a `Set-Cookie` header value.
|
|
76
|
+
* `encode` defaults to `encodeURIComponent`. Throws if the name or
|
|
77
|
+
* encoded value contain invalid characters.
|
|
78
|
+
*/
|
|
79
|
+
export function cookieSerialize(
|
|
80
|
+
name: string,
|
|
81
|
+
value: string,
|
|
82
|
+
options: CookieSerializeOptions = {},
|
|
83
|
+
): string {
|
|
84
|
+
if (!/^[\w!#$%&'*+\-.^`|~]+$/.test(name)) {
|
|
85
|
+
throw new TypeError(`cookieSerialize: invalid cookie name ${JSON.stringify(name)}`)
|
|
86
|
+
}
|
|
87
|
+
const encode = options.encode ?? encodeURIComponent
|
|
88
|
+
const encoded = encode(value)
|
|
89
|
+
if (encoded && !/^[\w!#$%&'()*+\-./:<=>?@[\]^`{|}~]*$/.test(encoded)) {
|
|
90
|
+
throw new TypeError(`cookieSerialize: invalid cookie value for ${name}`)
|
|
91
|
+
}
|
|
92
|
+
const parts = [`${name}=${encoded}`]
|
|
93
|
+
if (typeof options.maxAge === 'number' && Number.isFinite(options.maxAge)) {
|
|
94
|
+
parts.push(`Max-Age=${Math.floor(options.maxAge)}`)
|
|
95
|
+
}
|
|
96
|
+
if (options.domain) parts.push(`Domain=${options.domain}`)
|
|
97
|
+
if (options.path) parts.push(`Path=${options.path}`)
|
|
98
|
+
if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`)
|
|
99
|
+
if (options.httpOnly) parts.push('HttpOnly')
|
|
100
|
+
if (options.secure) parts.push('Secure')
|
|
101
|
+
if (options.sameSite !== undefined && options.sameSite !== false) {
|
|
102
|
+
const ss = options.sameSite
|
|
103
|
+
const value =
|
|
104
|
+
ss === true
|
|
105
|
+
? 'Strict'
|
|
106
|
+
: `${ss.charAt(0).toUpperCase()}${ss.slice(1)}`
|
|
107
|
+
parts.push(`SameSite=${value}`)
|
|
108
|
+
}
|
|
109
|
+
if (options.priority) {
|
|
110
|
+
parts.push(`Priority=${options.priority.charAt(0).toUpperCase() + options.priority.slice(1)}`)
|
|
111
|
+
}
|
|
112
|
+
return parts.join('; ')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Normalize a query-param record so values are always strings (or
|
|
117
|
+
* arrays of strings). Mirrors the upstream `normalizeUnknownQueryParams`
|
|
118
|
+
* helper used by the auto-encoding URL builder. Nullish entries are
|
|
119
|
+
* dropped; non-primitive entries are JSON-stringified.
|
|
120
|
+
*/
|
|
121
|
+
export function normalizeUnknownQueryParams(
|
|
122
|
+
params: Record<string, unknown> | null | undefined,
|
|
123
|
+
): Record<string, string | string[]> {
|
|
124
|
+
const out: Record<string, string | string[]> = {}
|
|
125
|
+
if (!params) return out
|
|
126
|
+
for (const [key, raw] of Object.entries(params)) {
|
|
127
|
+
if (raw === undefined || raw === null) continue
|
|
128
|
+
if (Array.isArray(raw)) {
|
|
129
|
+
out[key] = raw.map((v) => (typeof v === 'string' ? v : JSON.stringify(v)))
|
|
130
|
+
} else if (typeof raw === 'string') {
|
|
131
|
+
out[key] = raw
|
|
132
|
+
} else if (typeof raw === 'number' || typeof raw === 'boolean') {
|
|
133
|
+
out[key] = String(raw)
|
|
134
|
+
} else {
|
|
135
|
+
out[key] = JSON.stringify(raw)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out
|
|
139
|
+
}
|