@jokio/sdk 0.1.10 → 0.1.12
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/auth.service.d.ts +25 -0
- package/dist/common/playAudioFromBuffer.d.ts +1 -0
- package/dist/crypto.service.d.ts +9 -0
- package/dist/index.d.ts +19 -0
- package/dist/jokio.sdk.es.js +8534 -0
- package/dist/nats.service.d.ts +45 -0
- package/dist/tts.service.d.ts +13 -0
- package/dist/voicevoxTts.d.ts +11 -0
- package/package.json +7 -3
- package/.prettierrc +0 -12
- package/package-lock.json +0 -1062
- package/src/auth.service.ts +0 -152
- package/src/common/playAudioFromBuffer.ts +0 -15
- package/src/crypto.service.ts +0 -100
- package/src/index.ts +0 -39
- package/src/nats.service.ts +0 -348
- package/src/tts.service.ts +0 -153
- package/src/voicevoxTts.ts +0 -94
- package/tsconfig.json +0 -14
- package/vite.config.ts +0 -23
- /package/{test → dist}/index.html +0 -0
package/src/auth.service.ts
DELETED
|
@@ -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
|
-
}
|
package/src/crypto.service.ts
DELETED
|
@@ -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
|
-
}
|
package/src/nats.service.ts
DELETED
|
@@ -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]
|