@empyria/webui 0.1.1 → 0.1.3
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/app/consts.ts +1 -1
- package/package.json +9 -7
- package/server/env.js +15 -1
- package/server/plugins/moleculer.ts +139 -0
- package/server/plugins/restate.ts +38 -0
package/app/consts.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@empyria/webui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Restate.dev helpers for the Empyria nanoservice framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Imre Fazekas <imre.fazekas@icloud.com>",
|
|
@@ -23,8 +23,10 @@
|
|
|
23
23
|
"lint:fix": "bun oxlint --fix"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@empyria/common": "0.1.
|
|
27
|
-
"@
|
|
26
|
+
"@empyria/common": "0.1.3",
|
|
27
|
+
"@empyria/moleculer": "0.1.3",
|
|
28
|
+
"@empyria/restate": "0.1.6",
|
|
29
|
+
"@iconify-json/lucide": "^1.2.126",
|
|
28
30
|
"@iconify-json/simple-icons": "^1.2.93",
|
|
29
31
|
"@pinia/nuxt": "^1.0.2",
|
|
30
32
|
"@simplewebauthn/browser": "^13.3.0",
|
|
@@ -43,15 +45,15 @@
|
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
47
|
"@nuxt/fonts": "^0.14.0",
|
|
46
|
-
"@nuxt/ui": "^4.
|
|
48
|
+
"@nuxt/ui": "^4.11.0",
|
|
47
49
|
"@nuxtjs/tailwindcss": "7.0.0-beta.1",
|
|
48
50
|
"@tailwindcss/typography": "^0.5.20",
|
|
49
51
|
"nuxt": "^4.5.2",
|
|
50
52
|
"tailwindcss": "^4.3.3",
|
|
51
53
|
"typescript": "^7.0.2",
|
|
52
|
-
"vue-tsc": "^3.3.
|
|
53
|
-
"oxfmt": "0.
|
|
54
|
-
"oxlint": "1.
|
|
54
|
+
"vue-tsc": "^3.3.11",
|
|
55
|
+
"oxfmt": "0.65.0",
|
|
56
|
+
"oxlint": "1.80.0"
|
|
55
57
|
},
|
|
56
58
|
"engines": {
|
|
57
59
|
"bun": ">=1.4.0",
|
package/server/env.js
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { createEnv, validate } from '@empyria/common'
|
|
2
2
|
|
|
3
|
-
const schema = createEnv({
|
|
3
|
+
const schema = createEnv({
|
|
4
|
+
/*
|
|
5
|
+
moleculer: {
|
|
6
|
+
transporterURL: env.NUXT_MOL_TRANSPORTER,
|
|
7
|
+
namespace: env.NUXT_MOL_NAMESPACE,
|
|
8
|
+
servicesRequired: env.NUXT_MOL_SERVICES_REQUIRED?.split(',') || [],
|
|
9
|
+
waitTimeout: parseInt(env.NUXT_MOL_WAIT_TIMEOUT || '30000', 10),
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
restate: {
|
|
13
|
+
url: env.NUXT_RESTATE_URL,
|
|
14
|
+
adminURL: env.NUXT_RESTATE_ADMIN_URL,
|
|
15
|
+
},
|
|
16
|
+
*/
|
|
17
|
+
})
|
|
4
18
|
|
|
5
19
|
export const env = validate(schema, { ...process.env }, { coerceTypes: true })
|
|
6
20
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { ServiceBroker as MoleculerBroker } from 'moleculer'
|
|
2
|
+
import { randomUUID as uuid } from 'node:crypto'
|
|
3
|
+
import { connect } from '@empyria/moleculer/Connector.js'
|
|
4
|
+
import { getCookie } from 'h3'
|
|
5
|
+
|
|
6
|
+
import { AccessTokenCookieKey } from '../../app/consts'
|
|
7
|
+
|
|
8
|
+
import { settings } from '../env'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
BaseErrors,
|
|
12
|
+
EMPYRIA_FEDERATION_ID,
|
|
13
|
+
MOLECULER_SERVICE_ROLE,
|
|
14
|
+
moleculerMeta,
|
|
15
|
+
} from '@empyria/common'
|
|
16
|
+
|
|
17
|
+
const whitelistedActions = [
|
|
18
|
+
'v1.Auth.registerPasskeyBegin',
|
|
19
|
+
'v1.Auth.registerPasskeyFinish',
|
|
20
|
+
'v1.Auth.loginPasskeyBegin',
|
|
21
|
+
'v1.Auth.loginPasskeyFinish',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const NUXT_NAME = 'Nuxt Gateway'
|
|
25
|
+
const NUXT_META = moleculerMeta({
|
|
26
|
+
actor: NUXT_NAME,
|
|
27
|
+
federation: EMPYRIA_FEDERATION_ID,
|
|
28
|
+
flowID: uuid(),
|
|
29
|
+
processID: uuid(),
|
|
30
|
+
role: MOLECULER_SERVICE_ROLE,
|
|
31
|
+
tokenKey: `${NUXT_NAME}`,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
declare global {
|
|
35
|
+
var moleculer: {
|
|
36
|
+
broker: MoleculerBroker
|
|
37
|
+
call<T>(
|
|
38
|
+
actionName: string,
|
|
39
|
+
params?: any,
|
|
40
|
+
opts?: { timeout?: number; meta?: Record<string, any> },
|
|
41
|
+
): Promise<T>
|
|
42
|
+
emit(eventName: string, payload?: any, groups?: string | string[]): Promise<void>
|
|
43
|
+
broadcast(eventName: string, payload?: any, groups?: string | string[]): Promise<void>
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default defineNitroPlugin(async () => {
|
|
48
|
+
const transporterURL = settings.moleculer.transporterURL
|
|
49
|
+
const namespace = settings.moleculer.namespace
|
|
50
|
+
const nodeID = `UI-${uuid()}`
|
|
51
|
+
const servicesRequired = settings.moleculer.servicesRequired
|
|
52
|
+
const waitTimeout = settings.moleculer.waitTimeout
|
|
53
|
+
|
|
54
|
+
let connector: Awaited<ReturnType<typeof connect>> | undefined
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Wait for required services to be available (only in production)
|
|
58
|
+
connector = await connect({
|
|
59
|
+
namespace,
|
|
60
|
+
nodeID,
|
|
61
|
+
transporter: transporterURL,
|
|
62
|
+
logLevel: 'warn', // Keep it quiet in the UI
|
|
63
|
+
servicesRequired: settings.prod ? servicesRequired : [],
|
|
64
|
+
waitTimeout,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (settings.prod) {
|
|
68
|
+
console.log('✅ Required services are available')
|
|
69
|
+
} else {
|
|
70
|
+
console.log(`ℹ️ Skipping service wait in development mode`)
|
|
71
|
+
console.log(`ℹ️ Set NODE_ENV=production to enable strict service checking`)
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.error('❌ Failed to initialize Moleculer:', err)
|
|
75
|
+
if (settings.prod) {
|
|
76
|
+
throw err // Fail fast in production
|
|
77
|
+
} else {
|
|
78
|
+
console.warn('⚠️ Moleculer initialization failed but continuing in development mode')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const broker = connector?.broker as MoleculerBroker
|
|
83
|
+
|
|
84
|
+
globalThis.moleculer = {
|
|
85
|
+
broker,
|
|
86
|
+
|
|
87
|
+
async call<T>(
|
|
88
|
+
actionName: string,
|
|
89
|
+
params?: any,
|
|
90
|
+
opts?: { timeout?: number; meta?: Record<string, any> },
|
|
91
|
+
): Promise<T> {
|
|
92
|
+
const event = useEvent()
|
|
93
|
+
const tokenKey = event?.context?.tokenKey || getCookie(event, AccessTokenCookieKey)
|
|
94
|
+
|
|
95
|
+
if (!whitelistedActions.includes(actionName) && !tokenKey) {
|
|
96
|
+
throw BaseErrors.ServiceVoilation({
|
|
97
|
+
service: `Service ${actionName} communication`,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const user = whitelistedActions.includes(actionName)
|
|
102
|
+
? NUXT_META.user
|
|
103
|
+
: tokenKey &&
|
|
104
|
+
(await connector!.call(
|
|
105
|
+
'v1.Auth.resolveToken',
|
|
106
|
+
{ tokenKey },
|
|
107
|
+
{ meta: NUXT_META },
|
|
108
|
+
))
|
|
109
|
+
|
|
110
|
+
const meta = { ...opts?.meta, tokenKey, user }
|
|
111
|
+
return connector!.call(actionName, params, {
|
|
112
|
+
timeout: opts?.timeout,
|
|
113
|
+
meta: meta,
|
|
114
|
+
})
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async emit(eventName: string, payload?: any, groups?: string | string[]): Promise<void> {
|
|
118
|
+
await broker.emit(eventName, payload, groups)
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async broadcast(
|
|
122
|
+
eventName: string,
|
|
123
|
+
payload?: any,
|
|
124
|
+
groups?: string | string[],
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
await broker.broadcast(eventName, payload, groups)
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Graceful shutdown
|
|
131
|
+
const shutdown = async () => {
|
|
132
|
+
console.log('🛑 Shutting down Moleculer broker...')
|
|
133
|
+
await connector?.stop()
|
|
134
|
+
console.log('✅ Moleculer broker stopped')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
process.on('SIGINT', shutdown)
|
|
138
|
+
process.on('SIGTERM', shutdown)
|
|
139
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// server/plugins/restate.ts
|
|
2
|
+
import { checkServiceHandler, clients, sendMessage } from '@empyria/restate'
|
|
3
|
+
import { getCookie } from 'h3'
|
|
4
|
+
import { AccessTokenCookieKey } from '../../app/consts'
|
|
5
|
+
|
|
6
|
+
import { settings } from '../env'
|
|
7
|
+
|
|
8
|
+
export default defineNitroPlugin(() => {
|
|
9
|
+
const restateURL = settings.restate.url
|
|
10
|
+
const restateAdminURL = settings.restate.adminURL
|
|
11
|
+
|
|
12
|
+
const restateClient = clients.connect({ url: restateURL })
|
|
13
|
+
|
|
14
|
+
globalThis.restate = {
|
|
15
|
+
client: restateClient,
|
|
16
|
+
adminURL: restateAdminURL,
|
|
17
|
+
discoverService: (serviceName: string, handlerName?: string) =>
|
|
18
|
+
checkServiceHandler(restateAdminURL, serviceName, handlerName),
|
|
19
|
+
|
|
20
|
+
async call<T>(serviceName: string, handlerName: string, params?: any): Promise<T> {
|
|
21
|
+
await checkServiceHandler(restateAdminURL, serviceName, handlerName)
|
|
22
|
+
|
|
23
|
+
const event = useEvent()
|
|
24
|
+
const tokenKey = event?.context?.tokenKey || getCookie(event, AccessTokenCookieKey)
|
|
25
|
+
|
|
26
|
+
const paramsWithToken = tokenKey ? { ...params, tokenKey } : params
|
|
27
|
+
|
|
28
|
+
return sendMessage({
|
|
29
|
+
restateURL,
|
|
30
|
+
name: serviceName,
|
|
31
|
+
message: handlerName,
|
|
32
|
+
payload: paramsWithToken,
|
|
33
|
+
})
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('✅ Restate client initialized')
|
|
38
|
+
})
|