@quazardous/qdadm 2.16.0 → 2.17.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/node/gen/decorators.d.ts +65 -0
- package/dist/node/gen/decorators.js +87 -0
- package/dist/node/gen/generateManagers.d.ts +107 -0
- package/dist/node/gen/generateManagers.js +338 -0
- package/dist/node/gen/schema.d.ts +195 -0
- package/dist/node/gen/schema.js +91 -0
- package/dist/node/gen/vite-plugin.d.ts +64 -0
- package/dist/node/gen/vite-plugin.js +99 -0
- package/dist/node/vite/qdadmDebugPlugin.d.ts +60 -0
- package/dist/node/vite/qdadmDebugPlugin.js +462 -0
- package/dist/node/vite/qdadmPlugin.d.ts +29 -0
- package/dist/node/vite/qdadmPlugin.js +91 -0
- package/package.json +9 -6
- package/src/auth/index.ts +9 -0
- package/src/auth/oauth/GoogleOAuthAdapter.ts +235 -0
- package/src/auth/oauth/index.ts +23 -0
- package/src/auth/oauth/pkce.ts +96 -0
- package/src/components/index.ts +1 -0
- package/src/components/pages/LoginPage.vue +29 -0
- package/src/components/pages/OAuthCallbackPage.vue +134 -0
- package/src/gen/vite-plugin.ts +4 -1
- package/src/index.ts +6 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qdadm Vite dev plugin — exposes the running app's DebugBridge over HTTP.
|
|
3
|
+
*
|
|
4
|
+
* Endpoints (default prefix `/__qdadm`):
|
|
5
|
+
* GET /__qdadm/ — index of endpoints + cache stats
|
|
6
|
+
* GET /__qdadm/sessions — list of connected client sessions
|
|
7
|
+
* GET /__qdadm/describe.json — bridge.describe() (manifests)
|
|
8
|
+
* GET /__qdadm/snapshot.json — bridge.dump() (live state)
|
|
9
|
+
* POST /__qdadm/call — body {collector, action, args?}
|
|
10
|
+
*
|
|
11
|
+
* Session selection: every browser tab that loads the app gets a fresh
|
|
12
|
+
* session id (uuid in the injected client). All endpoints accept a
|
|
13
|
+
* `?session=<id|latest>` query param. Default is `latest` (the most recently
|
|
14
|
+
* active session).
|
|
15
|
+
*
|
|
16
|
+
* Wire-up: the plugin injects a small ESM script via `transformIndexHtml`.
|
|
17
|
+
* That inline script lives in Vite's HMR pipeline (so `import.meta.hot`
|
|
18
|
+
* works), polls for `window.__qdadm.debug.bridge`, then pushes describe +
|
|
19
|
+
* snapshot on every reactive tick. The plugin caches per-session payloads
|
|
20
|
+
* in memory; GETs are served from cache (no round-trip needed).
|
|
21
|
+
*
|
|
22
|
+
* `call` is always request-response — it mutates state in the live page.
|
|
23
|
+
*
|
|
24
|
+
* Cache responses include `X-Qdadm-Stale-Ms`, `X-Qdadm-Source: cache|live`,
|
|
25
|
+
* and `X-Qdadm-Session: <id>` headers.
|
|
26
|
+
*/
|
|
27
|
+
export function qdadmDebugPlugin(options = {}) {
|
|
28
|
+
const prefix = options.prefix ?? '/__qdadm';
|
|
29
|
+
const timeoutMs = options.timeoutMs ?? 3000;
|
|
30
|
+
const sessionTtlMs = options.sessionTtlMs ?? 10 * 60 * 1000;
|
|
31
|
+
const sessions = new Map();
|
|
32
|
+
const pending = new Map();
|
|
33
|
+
let server = null;
|
|
34
|
+
let nextId = 1;
|
|
35
|
+
function pruneStaleSessions() {
|
|
36
|
+
const cutoff = Date.now() - sessionTtlMs;
|
|
37
|
+
for (const [id, s] of sessions) {
|
|
38
|
+
if (s.lastSeenAt < cutoff)
|
|
39
|
+
sessions.delete(id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function pickSession(sessionParam) {
|
|
43
|
+
pruneStaleSessions();
|
|
44
|
+
if (!sessionParam || sessionParam === 'latest') {
|
|
45
|
+
let best = null;
|
|
46
|
+
for (const s of sessions.values()) {
|
|
47
|
+
if (!best || s.lastSeenAt > best.lastSeenAt)
|
|
48
|
+
best = s;
|
|
49
|
+
}
|
|
50
|
+
return best;
|
|
51
|
+
}
|
|
52
|
+
return sessions.get(sessionParam) ?? null;
|
|
53
|
+
}
|
|
54
|
+
function ask(type, payload, sessionId) {
|
|
55
|
+
if (!server)
|
|
56
|
+
return Promise.reject(new Error('[qdadm-debug] vite server not ready'));
|
|
57
|
+
const id = String(nextId++);
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const timeout = setTimeout(() => {
|
|
60
|
+
pending.delete(id);
|
|
61
|
+
reject(new Error(`[qdadm-debug] timeout waiting for ${type} — open the app in a browser at least once`));
|
|
62
|
+
}, timeoutMs);
|
|
63
|
+
pending.set(id, { resolve, reject, timeout });
|
|
64
|
+
server.ws.send('qdadm:debug:request', { id, type, payload, sessionId });
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function serveCachedOrLive(res, sessionParam, field, liveType) {
|
|
68
|
+
const session = pickSession(sessionParam);
|
|
69
|
+
if (session) {
|
|
70
|
+
const cache = session[field];
|
|
71
|
+
if (cache) {
|
|
72
|
+
res.setHeader('x-qdadm-stale-ms', String(Date.now() - cache.at));
|
|
73
|
+
res.setHeader('x-qdadm-source', 'cache');
|
|
74
|
+
res.setHeader('x-qdadm-session', session.id);
|
|
75
|
+
return sendJson(res, 200, cache.data);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const data = await ask(liveType, undefined, session?.id);
|
|
80
|
+
res.setHeader('x-qdadm-stale-ms', '0');
|
|
81
|
+
res.setHeader('x-qdadm-source', 'live');
|
|
82
|
+
if (session)
|
|
83
|
+
res.setHeader('x-qdadm-session', session.id);
|
|
84
|
+
return sendJson(res, 200, data);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
return sendJson(res, 503, {
|
|
88
|
+
error: e.message,
|
|
89
|
+
sessions: listSessions(),
|
|
90
|
+
hint: sessions.size === 0
|
|
91
|
+
? 'No connected client. Load http://<dev>/ in a browser once.'
|
|
92
|
+
: 'No cached payload yet for the chosen session — try ?session=latest.',
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function listSessions() {
|
|
97
|
+
pruneStaleSessions();
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
return Array.from(sessions.values())
|
|
100
|
+
.sort((a, b) => b.lastSeenAt - a.lastSeenAt)
|
|
101
|
+
.map((s) => ({
|
|
102
|
+
id: s.id,
|
|
103
|
+
firstSeenAt: s.firstSeenAt,
|
|
104
|
+
lastSeenAt: s.lastSeenAt,
|
|
105
|
+
ageMs: now - s.lastSeenAt,
|
|
106
|
+
hasDescribe: !!s.describe,
|
|
107
|
+
hasSnapshot: !!s.snapshot,
|
|
108
|
+
meta: s.meta,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
function ensureSession(id, meta) {
|
|
112
|
+
let s = sessions.get(id);
|
|
113
|
+
if (!s) {
|
|
114
|
+
s = {
|
|
115
|
+
id,
|
|
116
|
+
firstSeenAt: Date.now(),
|
|
117
|
+
lastSeenAt: Date.now(),
|
|
118
|
+
describe: null,
|
|
119
|
+
snapshot: null,
|
|
120
|
+
meta: meta ?? {},
|
|
121
|
+
};
|
|
122
|
+
sessions.set(id, s);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
s.lastSeenAt = Date.now();
|
|
126
|
+
if (meta)
|
|
127
|
+
s.meta = { ...s.meta, ...meta };
|
|
128
|
+
}
|
|
129
|
+
return s;
|
|
130
|
+
}
|
|
131
|
+
/** Inter-plugin API (#1398) — consumed by @quazardous/qdmcp. */
|
|
132
|
+
const api = {
|
|
133
|
+
ask,
|
|
134
|
+
pickSession: (sessionParam) => {
|
|
135
|
+
const s = pickSession(sessionParam);
|
|
136
|
+
return s ? { id: s.id, lastSeenAt: s.lastSeenAt, meta: s.meta } : null;
|
|
137
|
+
},
|
|
138
|
+
listSessions,
|
|
139
|
+
prefix,
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
name: 'qdadm-debug',
|
|
143
|
+
apply: 'serve',
|
|
144
|
+
api,
|
|
145
|
+
transformIndexHtml: {
|
|
146
|
+
order: 'pre',
|
|
147
|
+
handler() {
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
tag: 'script',
|
|
151
|
+
attrs: { type: 'module' },
|
|
152
|
+
injectTo: 'head',
|
|
153
|
+
children: HMR_CLIENT_SCRIPT,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
configureServer(s) {
|
|
159
|
+
server = s;
|
|
160
|
+
s.ws.on('qdadm:debug:reply', (data) => {
|
|
161
|
+
const p = pending.get(data.id);
|
|
162
|
+
if (!p)
|
|
163
|
+
return;
|
|
164
|
+
clearTimeout(p.timeout);
|
|
165
|
+
pending.delete(data.id);
|
|
166
|
+
if (data.ok)
|
|
167
|
+
p.resolve(data.data);
|
|
168
|
+
else
|
|
169
|
+
p.reject(new Error(data.error ?? '[qdadm-debug] unknown error'));
|
|
170
|
+
});
|
|
171
|
+
s.ws.on('qdadm:debug:push', (msg) => {
|
|
172
|
+
if (!msg?.sessionId)
|
|
173
|
+
return;
|
|
174
|
+
const session = ensureSession(msg.sessionId, msg.meta);
|
|
175
|
+
const at = Date.now();
|
|
176
|
+
if (msg.type === 'describe' && msg.data !== undefined) {
|
|
177
|
+
session.describe = { data: msg.data, at };
|
|
178
|
+
}
|
|
179
|
+
else if (msg.type === 'snapshot' && msg.data !== undefined) {
|
|
180
|
+
session.snapshot = { data: msg.data, at };
|
|
181
|
+
}
|
|
182
|
+
else if (msg.type === 'bye') {
|
|
183
|
+
sessions.delete(msg.sessionId);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
s.middlewares.use(async (req, res, next) => {
|
|
187
|
+
if (!req.url || !req.url.startsWith(prefix))
|
|
188
|
+
return next();
|
|
189
|
+
const url = new URL(req.url, 'http://localhost');
|
|
190
|
+
const path = url.pathname.slice(prefix.length);
|
|
191
|
+
const sessionParam = url.searchParams.get('session');
|
|
192
|
+
try {
|
|
193
|
+
if (req.method === 'GET' && (path === '' || path === '/')) {
|
|
194
|
+
return sendJson(res, 200, {
|
|
195
|
+
version: '1',
|
|
196
|
+
description: 'qdadm debug bridge — read collector manifests and snapshots, invoke actions.',
|
|
197
|
+
endpoints: {
|
|
198
|
+
'GET /sessions': 'list connected client sessions',
|
|
199
|
+
'GET /describe.json?session=<id|latest>': 'manifest for every collector (entry shapes + actions)',
|
|
200
|
+
'GET /snapshot.json?session=<id|latest>': 'live JSON snapshot of every collector',
|
|
201
|
+
'POST /call?session=<id|latest>': 'invoke an action — body { collector, action, args? }',
|
|
202
|
+
},
|
|
203
|
+
sessions: listSessions(),
|
|
204
|
+
prefix,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (req.method === 'GET' && path === '/sessions') {
|
|
208
|
+
return sendJson(res, 200, { sessions: listSessions() });
|
|
209
|
+
}
|
|
210
|
+
if (req.method === 'GET' && path === '/describe.json') {
|
|
211
|
+
return await serveCachedOrLive(res, sessionParam, 'describe', 'describe');
|
|
212
|
+
}
|
|
213
|
+
if (req.method === 'GET' && path === '/snapshot.json') {
|
|
214
|
+
return await serveCachedOrLive(res, sessionParam, 'snapshot', 'dump');
|
|
215
|
+
}
|
|
216
|
+
if (req.method === 'POST' && path === '/call') {
|
|
217
|
+
const body = await readBody(req);
|
|
218
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
219
|
+
const session = pickSession(sessionParam);
|
|
220
|
+
if (!session) {
|
|
221
|
+
return sendJson(res, 503, {
|
|
222
|
+
error: 'no connected session',
|
|
223
|
+
sessions: listSessions(),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const data = await ask('call', parsed, session.id);
|
|
227
|
+
res.setHeader('x-qdadm-session', session.id);
|
|
228
|
+
return sendJson(res, 200, data);
|
|
229
|
+
}
|
|
230
|
+
// Unknown subpath: fall through — sibling plugins (e.g.
|
|
231
|
+
// @quazardous/qdadm-mcp) mount their own endpoints under the
|
|
232
|
+
// same prefix (#1398).
|
|
233
|
+
return next();
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
return sendJson(res, 500, { error: e.message });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
export default qdadmDebugPlugin;
|
|
243
|
+
function sendJson(res, status, data) {
|
|
244
|
+
res.statusCode = status;
|
|
245
|
+
res.setHeader('content-type', 'application/json; charset=utf-8');
|
|
246
|
+
res.setHeader('cache-control', 'no-store');
|
|
247
|
+
res.end(JSON.stringify(data, null, 2));
|
|
248
|
+
}
|
|
249
|
+
function readBody(req) {
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
let buf = '';
|
|
252
|
+
req.setEncoding('utf-8');
|
|
253
|
+
req.on('data', (chunk) => {
|
|
254
|
+
buf += chunk;
|
|
255
|
+
});
|
|
256
|
+
req.on('end', () => resolve(buf));
|
|
257
|
+
req.on('error', reject);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Client-side bridge — injected verbatim into index.html as an ES module.
|
|
262
|
+
* Lives in Vite's HMR pipeline (so `import.meta.hot` works here).
|
|
263
|
+
*
|
|
264
|
+
* Each tab gets its own session id (random uuid). Pushes describe + snapshot
|
|
265
|
+
* on every reactive tick of the bridge so the plugin's per-session cache
|
|
266
|
+
* stays fresh. Listens for ad-hoc requests targeted at this session.
|
|
267
|
+
*/
|
|
268
|
+
const HMR_CLIENT_SCRIPT = String.raw `
|
|
269
|
+
// ── Boot capture (#1398): this head module runs BEFORE the app's entry
|
|
270
|
+
// module, so console/window errors from the very first app statement are
|
|
271
|
+
// buffered — visible over MCP even when the app dies before the bridge
|
|
272
|
+
// exists (the class of failure a bridge-only design can never see).
|
|
273
|
+
const __bootAt = Date.now()
|
|
274
|
+
const __bootlog = { errors: [], warns: [], pageErrors: [], rejections: [] }
|
|
275
|
+
function __cap(arr, entry) { arr.push(entry); if (arr.length > 200) arr.shift() }
|
|
276
|
+
function __fmt(args) {
|
|
277
|
+
try {
|
|
278
|
+
return Array.from(args).map((a) => {
|
|
279
|
+
if (a instanceof Error) return a.stack || a.message
|
|
280
|
+
if (typeof a === 'object') { try { return JSON.stringify(a) } catch (e) { return String(a) } }
|
|
281
|
+
return String(a)
|
|
282
|
+
}).join(' ').slice(0, 2000)
|
|
283
|
+
} catch (e) { return '[unformattable]' }
|
|
284
|
+
}
|
|
285
|
+
const __origError = console.error.bind(console)
|
|
286
|
+
const __origWarn = console.warn.bind(console)
|
|
287
|
+
console.error = function () { __cap(__bootlog.errors, { at: Date.now(), msg: __fmt(arguments) }); __origError.apply(null, arguments) }
|
|
288
|
+
console.warn = function () { __cap(__bootlog.warns, { at: Date.now(), msg: __fmt(arguments) }); __origWarn.apply(null, arguments) }
|
|
289
|
+
window.addEventListener('error', (e) => {
|
|
290
|
+
__cap(__bootlog.pageErrors, { at: Date.now(), msg: String(e.message || e), source: e.filename ? e.filename + ':' + e.lineno : undefined })
|
|
291
|
+
})
|
|
292
|
+
window.addEventListener('unhandledrejection', (e) => {
|
|
293
|
+
let msg; try { msg = e.reason && (e.reason.stack || e.reason.message) || String(e.reason) } catch (err) { msg = '[unreadable reason]' }
|
|
294
|
+
__cap(__bootlog.rejections, { at: Date.now(), msg: String(msg).slice(0, 2000) })
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const hot = import.meta.hot
|
|
298
|
+
if (!hot) {
|
|
299
|
+
console.warn('[qdadm-debug] import.meta.hot unavailable in injected script')
|
|
300
|
+
} else {
|
|
301
|
+
const sessionId = crypto.randomUUID()
|
|
302
|
+
const meta = { userAgent: navigator.userAgent, location: location.pathname }
|
|
303
|
+
let bridge = null
|
|
304
|
+
let lastTick = -1
|
|
305
|
+
|
|
306
|
+
const w = /** @type {any} */ (window)
|
|
307
|
+
|
|
308
|
+
// Signal ring buffer — armed once __qdadm.signals is reachable.
|
|
309
|
+
const __signals = []
|
|
310
|
+
let __signalsArmed = false
|
|
311
|
+
function __armSignals() {
|
|
312
|
+
if (__signalsArmed) return
|
|
313
|
+
const q = w.__qdadm
|
|
314
|
+
if (!q || !q.signals || typeof q.signals.on !== 'function') return
|
|
315
|
+
try {
|
|
316
|
+
q.signals.on('**', (event) => { __cap(__signals, { at: Date.now(), name: event && event.name }) ; if (__signals.length > 100) __signals.shift() })
|
|
317
|
+
__signalsArmed = true
|
|
318
|
+
} catch (e) {}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function __sessionInfo() {
|
|
322
|
+
const q = w.__qdadm || {}
|
|
323
|
+
let app = {}
|
|
324
|
+
try { app = (q.kernel && q.kernel.options && q.kernel.options.app) || {} } catch (e) {}
|
|
325
|
+
let route = null
|
|
326
|
+
try { const r = q.router && q.router.currentRoute && q.router.currentRoute.value; route = r ? { name: r.name, fullPath: r.fullPath } : null } catch (e) {}
|
|
327
|
+
return {
|
|
328
|
+
sessionId, bootAt: __bootAt, now: Date.now(), url: location.href,
|
|
329
|
+
app: { name: app.name, version: typeof app.version === 'function' ? app.version() : app.version },
|
|
330
|
+
route, bridgeReady: !!bridge, signalsArmed: __signalsArmed,
|
|
331
|
+
bootlogCounts: { errors: __bootlog.errors.length, warns: __bootlog.warns.length, pageErrors: __bootlog.pageErrors.length, rejections: __bootlog.rejections.length },
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function __routes() {
|
|
336
|
+
const q = w.__qdadm
|
|
337
|
+
if (!q || !q.router) throw new Error('router not ready')
|
|
338
|
+
return q.router.getRoutes().map((r) => ({
|
|
339
|
+
name: r.name, path: r.path,
|
|
340
|
+
meta: { entity: r.meta && r.meta.entity, layout: r.meta && r.meta.layout, requiresAuth: r.meta && r.meta.requiresAuth, public: r.meta && r.meta.public },
|
|
341
|
+
}))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function __manager(entity) {
|
|
345
|
+
const q = w.__qdadm
|
|
346
|
+
if (!q || !q.orchestrator) throw new Error('orchestrator not ready')
|
|
347
|
+
if (!q.orchestrator.isRegistered(entity)) throw new Error('entity not registered: ' + entity + ' (known: ' + q.orchestrator.getRegisteredNames().join(', ') + ')')
|
|
348
|
+
return q.orchestrator.get(entity)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function __entityState(payload) {
|
|
352
|
+
const q = w.__qdadm
|
|
353
|
+
if (!payload || !payload.entity) {
|
|
354
|
+
return { entities: q && q.orchestrator ? q.orchestrator.getRegisteredNames() : [] }
|
|
355
|
+
}
|
|
356
|
+
const m = __manager(payload.entity)
|
|
357
|
+
const safe = (fn) => { try { return fn() } catch (e) { return '[error: ' + (e && e.message) + ']' } }
|
|
358
|
+
const fields = {}
|
|
359
|
+
try {
|
|
360
|
+
const fs = m.fields || m._fields || {}
|
|
361
|
+
for (const k in fs) fields[k] = { type: fs[k] && fs[k].type, label: fs[k] && fs[k].label, required: !!(fs[k] && fs[k].required) }
|
|
362
|
+
} catch (e) {}
|
|
363
|
+
return {
|
|
364
|
+
entity: payload.entity,
|
|
365
|
+
idField: safe(() => m.idField), labelField: safe(() => m.labelField),
|
|
366
|
+
label: safe(() => m.label), labelPlural: safe(() => m.labelPlural),
|
|
367
|
+
can: { create: safe(() => m.canCreate()), read: safe(() => m.canRead()), update: safe(() => m.canUpdate()), delete: safe(() => m.canDelete()) },
|
|
368
|
+
fields,
|
|
369
|
+
storage: safe(() => { const st = m.storage || m._storage; return st ? { kind: st.constructor && st.constructor.name, storageKey: st._storageKey } : null }),
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function __entityCall(payload) {
|
|
374
|
+
if (!payload || !payload.entity || !payload.op) throw new Error('entityCall requires { entity, op, ... }')
|
|
375
|
+
const m = __manager(payload.entity)
|
|
376
|
+
const op = payload.op
|
|
377
|
+
if (op === 'list') return await m.list(payload.params || {})
|
|
378
|
+
if (op === 'get') return await m.get(payload.id)
|
|
379
|
+
if (op === 'create') return await m.create(payload.data || {})
|
|
380
|
+
if (op === 'update') return await m.update(payload.id, payload.data || {})
|
|
381
|
+
if (op === 'delete') { await m.delete(payload.id); return { deleted: payload.id } }
|
|
382
|
+
throw new Error('unknown op "' + op + '" (list|get|create|update|delete)')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function __storageDump(payload) {
|
|
386
|
+
if (!payload || !payload.entity) throw new Error('storageDump requires { entity }')
|
|
387
|
+
const m = __manager(payload.entity)
|
|
388
|
+
const st = m.storage || m._storage
|
|
389
|
+
const key = st && st._storageKey
|
|
390
|
+
if (!key) return { storageKey: null, note: 'storage exposes no localStorage key (' + (st && st.constructor && st.constructor.name) + ')' }
|
|
391
|
+
const raw = localStorage.getItem(key)
|
|
392
|
+
let parsed = null
|
|
393
|
+
try { parsed = raw ? JSON.parse(raw) : null } catch (e) { parsed = '[unparsable]' }
|
|
394
|
+
return { storageKey: key, count: Array.isArray(parsed) ? parsed.length : null, data: parsed }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function helloIfNeeded() {
|
|
398
|
+
try { hot.send('qdadm:debug:push', { type: 'hello', sessionId, meta }) } catch (e) {}
|
|
399
|
+
}
|
|
400
|
+
function pushDescribe() {
|
|
401
|
+
try { hot.send('qdadm:debug:push', { type: 'describe', sessionId, data: bridge.describe() }) }
|
|
402
|
+
catch (e) { console.warn('[qdadm-debug] describe push failed:', e) }
|
|
403
|
+
}
|
|
404
|
+
function pushSnapshot() {
|
|
405
|
+
try { hot.send('qdadm:debug:push', { type: 'snapshot', sessionId, data: bridge.dump() }) }
|
|
406
|
+
catch (e) { console.warn('[qdadm-debug] snapshot push failed:', e) }
|
|
407
|
+
}
|
|
408
|
+
function bye() {
|
|
409
|
+
try { hot.send('qdadm:debug:push', { type: 'bye', sessionId }) } catch (e) {}
|
|
410
|
+
}
|
|
411
|
+
window.addEventListener('beforeunload', bye)
|
|
412
|
+
|
|
413
|
+
hot.on('qdadm:debug:request', async (msg) => {
|
|
414
|
+
// Ignore requests addressed to a different session.
|
|
415
|
+
if (msg && msg.sessionId && msg.sessionId !== sessionId) return
|
|
416
|
+
const id = (msg && msg.id) || ''
|
|
417
|
+
try {
|
|
418
|
+
// Typed queries (#1398): bridge-independent — they must answer even
|
|
419
|
+
// when the app died before the bridge existed.
|
|
420
|
+
if (msg.type === 'bootlog') { hot.send('qdadm:debug:reply', { id, ok: true, data: __bootlog, sessionId }); return }
|
|
421
|
+
if (msg.type === 'sessionInfo') { hot.send('qdadm:debug:reply', { id, ok: true, data: __sessionInfo(), sessionId }); return }
|
|
422
|
+
if (msg.type === 'recentSignals') { __armSignals(); hot.send('qdadm:debug:reply', { id, ok: true, data: { armed: __signalsArmed, events: __signals }, sessionId }); return }
|
|
423
|
+
if (msg.type === 'routes') { hot.send('qdadm:debug:reply', { id, ok: true, data: __routes(), sessionId }); return }
|
|
424
|
+
if (msg.type === 'entityState') { hot.send('qdadm:debug:reply', { id, ok: true, data: __entityState(msg.payload), sessionId }); return }
|
|
425
|
+
if (msg.type === 'entityCall') { const data = await __entityCall(msg.payload); hot.send('qdadm:debug:reply', { id, ok: true, data, sessionId }); return }
|
|
426
|
+
if (msg.type === 'storageDump') { hot.send('qdadm:debug:reply', { id, ok: true, data: __storageDump(msg.payload), sessionId }); return }
|
|
427
|
+
|
|
428
|
+
const b = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
|
|
429
|
+
if (!b) throw new Error('debug bridge not ready')
|
|
430
|
+
let data
|
|
431
|
+
if (msg.type === 'describe') data = b.describe()
|
|
432
|
+
else if (msg.type === 'dump') data = b.dump()
|
|
433
|
+
else if (msg.type === 'call') {
|
|
434
|
+
const p = msg.payload || {}
|
|
435
|
+
if (!p.collector || !p.action) throw new Error('call payload requires { collector, action, args? }')
|
|
436
|
+
data = await b.call(p.collector, p.action, p.args || {})
|
|
437
|
+
} else throw new Error('unknown request type "' + msg.type + '"')
|
|
438
|
+
hot.send('qdadm:debug:reply', { id, ok: true, data, sessionId })
|
|
439
|
+
} catch (e) {
|
|
440
|
+
hot.send('qdadm:debug:reply', { id, ok: false, error: e && e.message ? e.message : String(e), sessionId })
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
let attachTimer = setInterval(() => {
|
|
445
|
+
bridge = w.__qdadm && w.__qdadm.debug && w.__qdadm.debug.bridge
|
|
446
|
+
if (!bridge) return
|
|
447
|
+
clearInterval(attachTimer)
|
|
448
|
+
__armSignals()
|
|
449
|
+
helloIfNeeded()
|
|
450
|
+
pushDescribe()
|
|
451
|
+
pushSnapshot()
|
|
452
|
+
lastTick = bridge.tick && bridge.tick.value
|
|
453
|
+
console.log('[qdadm-debug] HTTP bridge wired — session', sessionId.slice(0, 8))
|
|
454
|
+
setInterval(() => {
|
|
455
|
+
if (!bridge || !bridge.tick) return
|
|
456
|
+
if (bridge.tick.value === lastTick) return
|
|
457
|
+
lastTick = bridge.tick.value
|
|
458
|
+
pushSnapshot()
|
|
459
|
+
}, 250)
|
|
460
|
+
}, 100)
|
|
461
|
+
}
|
|
462
|
+
`;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Plugin } from 'vite';
|
|
2
|
+
export interface QdadmVitePluginOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Extra packages to dedupe on top of the defaults
|
|
5
|
+
* (vue, vue-router, primevue, pinia).
|
|
6
|
+
*/
|
|
7
|
+
dedupe?: string[];
|
|
8
|
+
/**
|
|
9
|
+
* PrimeVue "flavor" (#1393): alias every `primevue/*` import to a
|
|
10
|
+
* compatible fork (e.g. OpenVue). qdadm officially maintains and tests
|
|
11
|
+
* `primevue@4` (MIT) only — a flavor keeping the v4 module layout/API is
|
|
12
|
+
* drop-in; divergences are the consumer's to absorb.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* qdadmVitePlugin({ primevue: { package: 'openvue' } })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
primevue?: {
|
|
19
|
+
/** Package that replaces `primevue` (e.g. 'openvue'). */
|
|
20
|
+
package: string;
|
|
21
|
+
/**
|
|
22
|
+
* Optional package replacing `@primeuix/themes` — only needed if the
|
|
23
|
+
* flavor ships its own themes fork (none does today; OpenVue keeps the
|
|
24
|
+
* MIT @primeuix/* versions).
|
|
25
|
+
*/
|
|
26
|
+
themes?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare function qdadmVitePlugin(options?: QdadmVitePluginOptions): Plugin;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qdadm Vite consumer plugin (#1385)
|
|
3
|
+
*
|
|
4
|
+
* qdadm publishes raw TS/.vue sources. Without help, Vite pre-bundles the
|
|
5
|
+
* host app's `primevue` import into `.vite/deps` while qdadm's raw sources
|
|
6
|
+
* import `node_modules/primevue/...` directly — two module instances whose
|
|
7
|
+
* injection symbols don't match, and the app dies at boot with
|
|
8
|
+
* `Error: No PrimeVue Toast provided!`.
|
|
9
|
+
*
|
|
10
|
+
* This plugin applies the required config in one line:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // vite.config.ts
|
|
14
|
+
* import { qdadmVitePlugin } from '@quazardous/qdadm/vite'
|
|
15
|
+
*
|
|
16
|
+
* export default defineConfig({
|
|
17
|
+
* plugins: [vue(), qdadmVitePlugin()],
|
|
18
|
+
* })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* - `resolve.dedupe` collapses duplicate peer copies (also covers the
|
|
22
|
+
* `file:`/workspace-link scenario described in the package README).
|
|
23
|
+
* - `optimizeDeps.exclude` keeps primevue/@primeuix/themes/qdadm out of the
|
|
24
|
+
* pre-bundle so BOTH pipelines resolve the same raw modules. Excluding
|
|
25
|
+
* qdadm also keeps HMR sane when the package is consumed via a symlink.
|
|
26
|
+
* - qdadm has NO CJS transitives (pluralize is vendored as ESM, #1454), so
|
|
27
|
+
* there is nothing to `optimizeDeps.include` — which also means nothing
|
|
28
|
+
* for the optimizer to fail resolving in file:-linked installs where
|
|
29
|
+
* npm (install-links=false) never materializes qdadm's deps at the
|
|
30
|
+
* consumer root.
|
|
31
|
+
* - When `node_modules/@quazardous/qdadm` is a symlink (file:/workspace
|
|
32
|
+
* link — qdadm's own dev mode against a testbed app), its realpath is
|
|
33
|
+
* appended to `server.fs.allow` so qdadm's `?raw` dynamic imports (i18n
|
|
34
|
+
* YAML defaults) are servable from outside the consumer's workspace
|
|
35
|
+
* root. The workspace root is re-added explicitly because a populated
|
|
36
|
+
* `fs.allow` disables Vite's implicit default.
|
|
37
|
+
*
|
|
38
|
+
* Returned partial config is deep-merged by Vite (arrays concatenate), so
|
|
39
|
+
* consumer overrides in their own `defineConfig` still apply.
|
|
40
|
+
*/
|
|
41
|
+
import fs from 'node:fs';
|
|
42
|
+
import path from 'node:path';
|
|
43
|
+
import { searchForWorkspaceRoot } from 'vite';
|
|
44
|
+
export function qdadmVitePlugin(options = {}) {
|
|
45
|
+
return {
|
|
46
|
+
name: 'qdadm',
|
|
47
|
+
config: (userConfig = {}) => {
|
|
48
|
+
const root = path.resolve(userConfig.root ?? process.cwd());
|
|
49
|
+
// Symlinked install (file:/workspace link)? Allow serving from the
|
|
50
|
+
// link target, which typically lives outside the consumer's root.
|
|
51
|
+
const fsAllow = [];
|
|
52
|
+
try {
|
|
53
|
+
const pkgDir = path.join(root, 'node_modules', '@quazardous', 'qdadm');
|
|
54
|
+
const real = fs.realpathSync(pkgDir);
|
|
55
|
+
if (real !== pkgDir) {
|
|
56
|
+
fsAllow.push(searchForWorkspaceRoot(root), real);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// not installed under root node_modules (aliased/hoisted) — nothing to allow
|
|
61
|
+
}
|
|
62
|
+
// Flavor aliasing (#1393): after the alias, no `primevue`/themes ids
|
|
63
|
+
// remain in the graph — dedupe/exclude lists must name the flavor.
|
|
64
|
+
const flavor = options.primevue ?? null;
|
|
65
|
+
const widgetPkg = flavor?.package ?? 'primevue';
|
|
66
|
+
const themesPkg = flavor?.themes ?? '@primeuix/themes';
|
|
67
|
+
const alias = [];
|
|
68
|
+
if (flavor) {
|
|
69
|
+
alias.push({ find: /^primevue(\/|$)/, replacement: `${flavor.package}$1` });
|
|
70
|
+
if (flavor.themes) {
|
|
71
|
+
alias.push({ find: /^@primeuix\/themes(\/|$)/, replacement: `${flavor.themes}$1` });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// With a flavor, the original packages may still be installed — keep
|
|
75
|
+
// them excluded too, or the optimizer pre-bundles a stray
|
|
76
|
+
// `primevue/*` id that bypassed the alias (split-instance again).
|
|
77
|
+
const widgetExcludes = flavor ? [widgetPkg, 'primevue'] : [widgetPkg];
|
|
78
|
+
const themesExcludes = flavor?.themes ? [themesPkg, '@primeuix/themes'] : [themesPkg];
|
|
79
|
+
return {
|
|
80
|
+
resolve: {
|
|
81
|
+
...(alias.length > 0 ? { alias } : {}),
|
|
82
|
+
dedupe: ['vue', 'vue-router', widgetPkg, 'pinia', ...(options.dedupe ?? [])],
|
|
83
|
+
},
|
|
84
|
+
optimizeDeps: {
|
|
85
|
+
exclude: [...widgetExcludes, ...themesExcludes, '@quazardous/qdadm'],
|
|
86
|
+
},
|
|
87
|
+
...(fsAllow.length > 0 ? { server: { fs: { allow: fsAllow } } } : {}),
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quazardous/qdadm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "Vue 3 framework for admin dashboards with PrimeVue",
|
|
5
5
|
"author": "quazardous",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"lint": "eslint src/",
|
|
24
24
|
"lint:fix": "eslint src/ --fix",
|
|
25
25
|
"format": "prettier --write src/",
|
|
26
|
-
"build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/types"
|
|
26
|
+
"build:types": "vue-tsc --declaration --emitDeclarationOnly --outDir dist/types",
|
|
27
|
+
"build:node": "tsc -p tsconfig.node.json",
|
|
28
|
+
"prepack": "npm run build:node"
|
|
27
29
|
},
|
|
28
30
|
"exports": {
|
|
29
31
|
".": "./src/index.ts",
|
|
@@ -41,14 +43,15 @@
|
|
|
41
43
|
},
|
|
42
44
|
"./styles/variables": "./src/styles/_variables.scss",
|
|
43
45
|
"./gen": "./src/gen/index.ts",
|
|
44
|
-
"./gen/vite-plugin": "./
|
|
45
|
-
"./vite": "./
|
|
46
|
-
"./vite-plugin-debug": "./
|
|
46
|
+
"./gen/vite-plugin": "./dist/node/gen/vite-plugin.js",
|
|
47
|
+
"./vite": "./dist/node/vite/qdadmPlugin.js",
|
|
48
|
+
"./vite-plugin-debug": "./dist/node/vite/qdadmDebugPlugin.js"
|
|
47
49
|
},
|
|
48
50
|
"files": [
|
|
49
51
|
"src",
|
|
50
52
|
"README.md",
|
|
51
|
-
"LICENSE"
|
|
53
|
+
"LICENSE",
|
|
54
|
+
"dist/node"
|
|
52
55
|
],
|
|
53
56
|
"dependencies": {
|
|
54
57
|
"@quazardous/qdcore": "^1.1.0",
|
package/src/auth/index.ts
CHANGED
|
@@ -13,3 +13,12 @@ export {
|
|
|
13
13
|
type ISessionAuthAdapter,
|
|
14
14
|
type LocalStorageAuthConfig,
|
|
15
15
|
} from './SessionAuthAdapter'
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
GoogleOAuthAdapter,
|
|
19
|
+
createGoogleOAuthAdapter,
|
|
20
|
+
createPkceChallenge,
|
|
21
|
+
type GoogleOAuthAdapterOptions,
|
|
22
|
+
type OAuthExchangePayload,
|
|
23
|
+
type PkceChallenge,
|
|
24
|
+
} from './oauth/index'
|