@jokio/sdk 0.1.9 → 0.1.11

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.
@@ -1,152 +0,0 @@
1
- import {
2
- startAuthentication,
3
- startRegistration,
4
- } from '@simplewebauthn/browser'
5
-
6
- type Config = {
7
- authUrl: string
8
- }
9
-
10
- const USER_STORAGE_KEY = 'LAST_AUTH_DATA'
11
-
12
- export class AuthService {
13
- constructor(private config: Config) {}
14
-
15
- async requestEmailLogin(
16
- email: string,
17
- returnUrl: string = location.href,
18
- ) {
19
- const res = await fetch(
20
- this.config.authUrl +
21
- `/email-verification-request/${email}?returnUrl=${returnUrl}`,
22
- { credentials: 'include' },
23
- ).then(processResponse)
24
-
25
- return res as boolean
26
- }
27
-
28
- async completeEmailLogin(email: string, otpCode: string) {
29
- const res = await fetch(
30
- this.config.authUrl +
31
- `/email-verification-complete/${email}/${otpCode}`,
32
- { credentials: 'include' },
33
- ).then(processResponse)
34
-
35
- localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(res))
36
-
37
- return res as IdentityUser
38
- }
39
-
40
- async requestPasskeyLogin(
41
- opts: {
42
- displayName?: string
43
- isRegistration?: boolean
44
- addAsAdditionalDevice?: boolean
45
- } = {},
46
- ) {
47
- const {
48
- displayName = '',
49
- isRegistration = false,
50
- addAsAdditionalDevice = false,
51
- } = opts
52
-
53
- // 1. get challenge options
54
- const passkeyOpts = await fetch(
55
- this.config.authUrl +
56
- `/webauth-challenge-request?registration=${
57
- isRegistration ? 'true' : ''
58
- }&displayName=${displayName}`,
59
- { credentials: 'include' },
60
- ).then(processResponse)
61
-
62
- // 2. ask user to register
63
- let attResp
64
- try {
65
- if (!passkeyOpts.user?.id) {
66
- attResp = await startAuthentication({
67
- optionsJSON: passkeyOpts,
68
- })
69
- } else {
70
- attResp = await startRegistration({
71
- optionsJSON: passkeyOpts,
72
- })
73
- }
74
- } catch (error: any) {
75
- if (error.name === 'InvalidStateError') {
76
- alert(
77
- 'Error: Authenticator was probably already registered by user',
78
- )
79
- } else {
80
- alert(error.message)
81
- }
82
-
83
- throw error
84
- }
85
-
86
- // 3. verify response
87
- const res = await fetch(
88
- this.config.authUrl +
89
- `/webauth-challenge-complete?displayName=${displayName}&addAsAdditionalDevice=${
90
- addAsAdditionalDevice ? 'true' : ''
91
- }`,
92
- {
93
- method: 'POST',
94
- body: JSON.stringify(attResp),
95
- headers: {
96
- 'Content-Type': 'application/json',
97
- },
98
- credentials: 'include',
99
- },
100
- ).then(processResponse)
101
-
102
- localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(res))
103
-
104
- return res as IdentityUser
105
- }
106
-
107
- async guestLogin() {
108
- const res = await fetch(this.config.authUrl + `/sign-out`, {
109
- credentials: 'include',
110
- }).then(processResponse)
111
-
112
- localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(res))
113
-
114
- return res as IdentityUser
115
- }
116
-
117
- async clearLoginData() {
118
- localStorage.removeItem(USER_STORAGE_KEY)
119
- }
120
-
121
- getLastLoginData() {
122
- const s = localStorage.getItem(USER_STORAGE_KEY)
123
- if (!s) {
124
- return null
125
- }
126
-
127
- try {
128
- const res = JSON.parse(s)
129
-
130
- return res || null
131
- } catch {
132
- return null
133
- }
134
- }
135
- }
136
-
137
- export type IdentityUser = {
138
- name: string
139
- email: string
140
- userId: string
141
- sessionId: string
142
- verified: boolean
143
- }
144
- const processResponse = async (x: Response) => {
145
- const data = await x.json()
146
-
147
- if (!x.ok) {
148
- throw new Error(data.message)
149
- }
150
-
151
- return data
152
- }
@@ -1,15 +0,0 @@
1
- export function playAudioFromBuffer(buffer: ArrayBuffer) {
2
- const blob = new Blob([buffer], { type: 'audio/mpeg' }) // or 'audio/wav' depending on your format
3
- const url = URL.createObjectURL(blob)
4
-
5
- const audio = new Audio()
6
- audio.src = url
7
- // audio.play()
8
-
9
- // Optional: cleanup after playback
10
- audio.onended = () => {
11
- URL.revokeObjectURL(url)
12
- }
13
-
14
- return audio
15
- }
@@ -1,100 +0,0 @@
1
- const uint2hex = (x: Uint8Array) =>
2
- [...x].map(x => x.toString(16).padStart(2, '0')).join('')
3
-
4
- const hex2uint = (hex: string) =>
5
- new Uint8Array(
6
- hex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)),
7
- )
8
-
9
- const ab2b64 = (ab: any) =>
10
- btoa(String.fromCharCode(...new Uint8Array(ab)))
11
-
12
- const b642ab = (str: string) =>
13
- Uint8Array.from(atob(str), c => c.charCodeAt(0)).buffer
14
-
15
- const sha256 = async (buffer: ArrayBuffer) =>
16
- await crypto.subtle.digest('SHA-256', buffer)
17
-
18
- const textEncoder = new TextEncoder()
19
-
20
- export class CryptoService {
21
- async createSessionKeyPair() {
22
- return await crypto.subtle.generateKey(
23
- {
24
- name: 'ECDH',
25
- namedCurve: 'P-256',
26
- },
27
- false, // non-extractable private key
28
- ['deriveKey', 'deriveBits'],
29
- )
30
- }
31
-
32
- async exportPublicKey(publicKey: CryptoKey): Promise<string> {
33
- const spki = await crypto.subtle.exportKey('spki', publicKey)
34
-
35
- return ab2b64(spki)
36
- }
37
-
38
- async deriveAESKey(privateKey: CryptoKey, publicKeyString: string) {
39
- const spki = b642ab(publicKeyString)
40
-
41
- const publicKey = await crypto.subtle.importKey(
42
- 'spki',
43
- spki,
44
- { name: 'ECDH', namedCurve: 'P-256' },
45
- false,
46
- [],
47
- )
48
-
49
- // Derive key directly
50
- const aesKey = await crypto.subtle.deriveKey(
51
- {
52
- name: 'ECDH',
53
- public: publicKey,
54
- },
55
- privateKey,
56
- { name: 'AES-GCM', length: 256 },
57
- false,
58
- ['encrypt', 'decrypt'],
59
- )
60
-
61
- return aesKey
62
- }
63
-
64
- async exportRawKey(key: CryptoKey) {
65
- const raw = await crypto.subtle.exportKey('raw', key)
66
- return uint2hex(new Uint8Array(raw))
67
- }
68
-
69
- async encrypt(text: string, key: CryptoKey) {
70
- const iv = crypto.getRandomValues(new Uint8Array(12))
71
- const encoded = new TextEncoder().encode(text)
72
- const ciphertext = await crypto.subtle.encrypt(
73
- { name: 'AES-GCM', iv },
74
- key,
75
- encoded,
76
- )
77
-
78
- return `${uint2hex(new Uint8Array(ciphertext))}_${uint2hex(iv)}`
79
- }
80
-
81
- async decrypt(encryptedText: string, key: CryptoKey) {
82
- const parts = encryptedText.split('_')
83
-
84
- const ciphertext = hex2uint(parts[0]).buffer
85
- const iv = hex2uint(parts[1])
86
-
87
- const decrypted = await crypto.subtle.decrypt(
88
- { name: 'AES-GCM', iv },
89
- key,
90
- ciphertext,
91
- )
92
- return new TextDecoder().decode(decrypted)
93
- }
94
-
95
- async hashString(text: string) {
96
- const hashBuffer = await sha256(textEncoder.encode(text).buffer)
97
-
98
- return ab2b64(hashBuffer)
99
- }
100
- }
package/src/index.ts DELETED
@@ -1,39 +0,0 @@
1
- import { AuthService } from './auth.service'
2
- import { CryptoService } from './crypto.service'
3
- import { NatsService } from './nats.service'
4
- import { EdgeTtsService } from './tts.service'
5
-
6
- export { NatsService } from './nats.service'
7
- export { VoicevoxTtsService } from './voicevoxTts'
8
-
9
- type Config = {
10
- debug: boolean
11
- authUrl: string
12
- natsUrl: string
13
- }
14
-
15
- const defaultConfig: Config = {
16
- debug: false,
17
- authUrl: 'https://auth.jok.io',
18
- natsUrl: 'https://natsx.jok.io',
19
- }
20
-
21
- export const jok = {
22
- setup(config: Partial<Config>) {
23
- if (config.authUrl) {
24
- this.auth = new AuthService({ authUrl: config.authUrl })
25
- }
26
-
27
- if (config.natsUrl) {
28
- this.nats = new NatsService({ natsUrl: config.natsUrl })
29
- }
30
- },
31
-
32
- auth: new AuthService(defaultConfig),
33
-
34
- tts: new EdgeTtsService(),
35
-
36
- nats: new NatsService(defaultConfig),
37
-
38
- crypto: new CryptoService(),
39
- }
@@ -1,348 +0,0 @@
1
- import {
2
- connect,
3
- DebugEvents,
4
- Events,
5
- JSONCodec,
6
- type NatsConnection,
7
- } from 'nats.ws'
8
- import { AuthService } from './auth.service'
9
-
10
- type Config = {
11
- natsUrl: string
12
- }
13
-
14
- const jsonCodec = JSONCodec()
15
-
16
- export class NatsService<TApi> {
17
- onStatusChange?: (isConnected: boolean) => void
18
-
19
- private nc?: NatsConnection
20
- private sessionId: string = ''
21
- private userId: string = ''
22
-
23
- private globalPrefix = ''
24
-
25
- private resolveConnected = (_: unknown) => {}
26
- isConnected = new Promise(
27
- resolve => (this.resolveConnected = resolve),
28
- )
29
-
30
- constructor(private config: Config) {}
31
-
32
- async connect(
33
- opts: {
34
- natsWsServerUrls?: string[]
35
- user?: IdentityUser
36
- prefix?: string
37
- } = {},
38
- ) {
39
- const { natsWsServerUrls, user, prefix } = opts
40
-
41
- const finalNatsServerUrls = natsWsServerUrls ?? [
42
- this.config.natsUrl,
43
- ]
44
-
45
- this.nc = await connect({
46
- servers: finalNatsServerUrls,
47
- })
48
-
49
- const userInfo =
50
- user ?? new AuthService({ authUrl: '' }).getLastLoginData()
51
-
52
- if (!userInfo) {
53
- throw new Error(
54
- 'Please authenticate first by calling `auth` apis',
55
- )
56
- }
57
-
58
- this.userId = userInfo.userId
59
- this.sessionId = userInfo.sessionId
60
-
61
- this.globalPrefix = prefix ?? ''
62
-
63
- this.resolveConnected(true)
64
-
65
- this.onStatusChange?.(true)
66
- console.log(
67
- '[nats] connected.',
68
- this.globalPrefix ? `(prefix: ${this.globalPrefix})` : '',
69
- )
70
-
71
- // Monitor connection status
72
- ;(async () => {
73
- for await (const status of this.nc!.status()) {
74
- if (status.type !== DebugEvents.PingTimer) {
75
- console.log('[nats] connection status:', status.type)
76
- }
77
-
78
- switch (status.type) {
79
- case Events.Reconnect:
80
- this.onStatusChange?.(true)
81
- break
82
-
83
- case Events.Disconnect:
84
- case Events.Error:
85
- case DebugEvents.Reconnecting:
86
- this.onStatusChange?.(false)
87
- break
88
- }
89
- }
90
- })()
91
- }
92
-
93
- async disconnect() {
94
- await this.nc?.drain()
95
- await this.nc?.close()
96
- }
97
-
98
- async publish<TSubject extends keyof TApi & string>(
99
- subject: TSubject,
100
- data: InferReturnType<TApi, TSubject>,
101
- ) {
102
- await this.isConnected
103
-
104
- if (!this.nc) {
105
- throw new Error('[nats] not connected')
106
- }
107
-
108
- try {
109
- const finalSubject = this.buildSubject(subject)
110
-
111
- console.debug('[nats]>>', finalSubject)
112
-
113
- this.nc.publish(finalSubject, jsonCodec.encode(data))
114
- } catch (error) {
115
- console.error('[nats] publish failed:', error)
116
- throw error
117
- }
118
- }
119
-
120
- async call<TSubject extends keyof TApi & string>(
121
- subject: TSubject,
122
- data: InferProps<TApi, TSubject>,
123
- opts?: { timeoutInMs?: number },
124
- ): Promise<InferReturnType<TApi, TSubject>> {
125
- await this.isConnected
126
-
127
- if (!this.nc) {
128
- throw new Error('[nats] not connected')
129
- }
130
-
131
- try {
132
- const finalSubject = this.buildSubject(subject)
133
-
134
- console.debug('[nats]>>', finalSubject)
135
-
136
- const response = await this.nc.request(
137
- finalSubject,
138
- jsonCodec.encode(data),
139
- { timeout: opts?.timeoutInMs ?? 5000 },
140
- )
141
- const result:
142
- | { ok: true; data: any }
143
- | { ok: false; message: string } = jsonCodec.decode(
144
- response.data,
145
- ) as any
146
-
147
- if (!result.ok) {
148
- throw new Error(result.message)
149
- }
150
-
151
- return result.data
152
- } catch (error) {
153
- console.error('[nats] request failed:', error)
154
- throw new Error('Api call failed.')
155
- }
156
- }
157
-
158
- async on<TSubject extends keyof TApi & string>(
159
- subject: TSubject,
160
- cb: (
161
- data: InferReturnType<TApi, TSubject>,
162
- ctx: {
163
- subject: string
164
- userId: string
165
- sessionId: string
166
- },
167
- ) => void,
168
- ) {
169
- await this.isConnected
170
-
171
- if (!this.nc) {
172
- throw new Error('[nats] not connected')
173
- }
174
-
175
- const prefix = this.globalPrefix ? `${this.globalPrefix}.` : ''
176
- const finalSubject = prefix + subject + '.>'
177
-
178
- const subscription = this.nc!.subscribe(finalSubject)
179
-
180
- ;(async () => {
181
- for await (const message of subscription) {
182
- try {
183
- console.debug('[nats]<<', message.subject)
184
-
185
- const subjectParts = message.subject.split('.')
186
-
187
- // -- process prefix --
188
- if (
189
- this.globalPrefix &&
190
- subjectParts[0] !== this.globalPrefix
191
- ) {
192
- console.log('[nats]', 'Skipped - ' + message.subject)
193
- continue
194
- }
195
-
196
- if (subjectParts[0] === 'dev') {
197
- // remove the first element / prefix
198
- subjectParts.shift()
199
- }
200
- // -- process prefix --
201
-
202
- const subject = subjectParts
203
- .slice(0, subjectParts.length - 2)
204
- .join('.')
205
- const userId = subjectParts[subjectParts.length - 2]
206
- const sessionId = subjectParts[subjectParts.length - 1]
207
-
208
- const decodedMessage = jsonCodec.decode(message.data)
209
-
210
- cb(decodedMessage as any, {
211
- subject,
212
- userId,
213
- sessionId,
214
- })
215
- } catch (err) {
216
- console.error('[nats] subscribe error', err)
217
- }
218
- }
219
- })()
220
-
221
- return () => subscription.unsubscribe()
222
- }
223
-
224
- /**
225
- * Reqiores permission to publish response in `_INBOX.>`. If you need to just listen events, use `on`.
226
- */
227
- async handler<TSubject extends keyof TApi & string>(
228
- subject: TSubject,
229
- cb: (
230
- data: InferProps<TApi, TSubject>,
231
- ctx: {
232
- subject: string
233
- userId: string
234
- sessionId: string
235
- },
236
- ) => Promise<InferReturnType<TApi, TSubject>>,
237
- ) {
238
- await this.isConnected
239
-
240
- if (!this.nc) {
241
- throw new Error('[nats] not connected')
242
- }
243
-
244
- const prefix = this.globalPrefix ? `${this.globalPrefix}.` : ''
245
- const finalSubject = prefix + subject + '.>'
246
-
247
- const subscription = this.nc!.subscribe(finalSubject)
248
-
249
- ;(async () => {
250
- for await (const message of subscription) {
251
- try {
252
- console.debug('[nats]<<', message.subject)
253
-
254
- const subjectParts = message.subject.split('.')
255
-
256
- // -- process prefix --
257
- if (
258
- this.globalPrefix &&
259
- subjectParts[0] !== this.globalPrefix
260
- ) {
261
- console.log('[nats]', 'Skipped - ' + message.subject)
262
- continue
263
- }
264
-
265
- if (subjectParts[0] === 'dev') {
266
- // remove the first element / prefix
267
- subjectParts.shift()
268
- }
269
- // -- process prefix --
270
-
271
- const subject = subjectParts
272
- .slice(0, subjectParts.length - 2)
273
- .join('.')
274
- const userId = subjectParts[subjectParts.length - 2]
275
- const sessionId = subjectParts[subjectParts.length - 1]
276
-
277
- const decodedMessage = jsonCodec.decode(message.data)
278
-
279
- try {
280
- const res = await cb(decodedMessage as any, {
281
- subject,
282
- userId,
283
- sessionId,
284
- })
285
-
286
- if (message.reply) {
287
- message.respond(
288
- jsonCodec.encode({
289
- ok: true,
290
- data: res,
291
- }),
292
- )
293
- }
294
- } catch (err: any) {
295
- console.error('[nats] handle', err)
296
-
297
- if (message.reply) {
298
- message.respond(
299
- jsonCodec.encode({
300
- ok: false,
301
- message: err.message,
302
- }),
303
- )
304
- }
305
- }
306
- } catch (err: any) {
307
- console.error('[nats] handle', err)
308
-
309
- if (message.reply) {
310
- message.respond(
311
- jsonCodec.encode({
312
- ok: false,
313
- message: err.message,
314
- }),
315
- )
316
- }
317
- }
318
- }
319
- })()
320
-
321
- return () => subscription.unsubscribe()
322
- }
323
-
324
- private buildSubject(subject: string) {
325
- return [this.globalPrefix, subject, this.userId, this.sessionId]
326
- .filter(x => x)
327
- .join('.')
328
- }
329
- }
330
-
331
- type IdentityUser = {
332
- userId: string
333
- sessionId: string
334
- }
335
-
336
- type InferProps<
337
- TApi,
338
- TSubject extends keyof TApi,
339
- > = TApi[TSubject] extends (args: infer TProps) => unknown
340
- ? TProps
341
- : {}
342
-
343
- type InferReturnType<
344
- TApi,
345
- TSubject extends keyof TApi,
346
- > = TApi[TSubject] extends (args: any) => infer TResult
347
- ? TResult
348
- : TApi[TSubject]