@qwickapps/server 1.7.1 → 1.7.2
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/CHANGELOG.md +13 -0
- package/dist/src/plugins/api-keys/api-keys-plugin.d.ts +5 -2
- package/dist/src/plugins/api-keys/api-keys-plugin.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.js +61 -19
- package/dist/src/plugins/api-keys/api-keys-plugin.js.map +1 -1
- package/dist/src/plugins/api-keys/index.d.ts +0 -4
- package/dist/src/plugins/api-keys/index.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/index.js +2 -3
- package/dist/src/plugins/api-keys/index.js.map +1 -1
- package/dist/src/plugins/api-keys/types.d.ts +9 -3
- package/dist/src/plugins/api-keys/types.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/types.js.map +1 -1
- package/dist/src/plugins/auth/index.d.ts +0 -4
- package/dist/src/plugins/auth/index.d.ts.map +1 -1
- package/dist/src/plugins/auth/index.js +2 -3
- package/dist/src/plugins/auth/index.js.map +1 -1
- package/dist/src/plugins/bans/bans-plugin.d.ts +5 -2
- package/dist/src/plugins/bans/bans-plugin.d.ts.map +1 -1
- package/dist/src/plugins/bans/bans-plugin.js +71 -25
- package/dist/src/plugins/bans/bans-plugin.js.map +1 -1
- package/dist/src/plugins/bans/index.d.ts +0 -4
- package/dist/src/plugins/bans/index.d.ts.map +1 -1
- package/dist/src/plugins/bans/index.js +2 -3
- package/dist/src/plugins/bans/index.js.map +1 -1
- package/dist/src/plugins/bans/types.d.ts +13 -6
- package/dist/src/plugins/bans/types.d.ts.map +1 -1
- package/dist/src/plugins/devices/devices-plugin.d.ts +5 -2
- package/dist/src/plugins/devices/devices-plugin.d.ts.map +1 -1
- package/dist/src/plugins/devices/devices-plugin.js +62 -26
- package/dist/src/plugins/devices/devices-plugin.js.map +1 -1
- package/dist/src/plugins/devices/index.d.ts +0 -4
- package/dist/src/plugins/devices/index.d.ts.map +1 -1
- package/dist/src/plugins/devices/index.js +2 -3
- package/dist/src/plugins/devices/index.js.map +1 -1
- package/dist/src/plugins/entitlements/entitlements-plugin.d.ts +5 -2
- package/dist/src/plugins/entitlements/entitlements-plugin.d.ts.map +1 -1
- package/dist/src/plugins/entitlements/entitlements-plugin.js +78 -41
- package/dist/src/plugins/entitlements/entitlements-plugin.js.map +1 -1
- package/dist/src/plugins/entitlements/index.d.ts +0 -4
- package/dist/src/plugins/entitlements/index.d.ts.map +1 -1
- package/dist/src/plugins/entitlements/index.js +2 -3
- package/dist/src/plugins/entitlements/index.js.map +1 -1
- package/dist/src/plugins/entitlements/types.d.ts +9 -2
- package/dist/src/plugins/entitlements/types.d.ts.map +1 -1
- package/dist/src/plugins/notifications/index.d.ts +0 -4
- package/dist/src/plugins/notifications/index.d.ts.map +1 -1
- package/dist/src/plugins/notifications/index.js +2 -3
- package/dist/src/plugins/notifications/index.js.map +1 -1
- package/dist/src/plugins/notifications/notifications-plugin.d.ts +5 -2
- package/dist/src/plugins/notifications/notifications-plugin.d.ts.map +1 -1
- package/dist/src/plugins/notifications/notifications-plugin.js +45 -13
- package/dist/src/plugins/notifications/notifications-plugin.js.map +1 -1
- package/dist/src/plugins/parental/index.d.ts +0 -4
- package/dist/src/plugins/parental/index.d.ts.map +1 -1
- package/dist/src/plugins/parental/index.js +2 -3
- package/dist/src/plugins/parental/index.js.map +1 -1
- package/dist/src/plugins/parental/parental-plugin.d.ts +5 -2
- package/dist/src/plugins/parental/parental-plugin.d.ts.map +1 -1
- package/dist/src/plugins/parental/parental-plugin.js +60 -24
- package/dist/src/plugins/parental/parental-plugin.js.map +1 -1
- package/dist/src/plugins/preferences/index.d.ts +0 -4
- package/dist/src/plugins/preferences/index.d.ts.map +1 -1
- package/dist/src/plugins/preferences/index.js +2 -3
- package/dist/src/plugins/preferences/index.js.map +1 -1
- package/dist/src/plugins/preferences/preferences-plugin.d.ts +5 -2
- package/dist/src/plugins/preferences/preferences-plugin.d.ts.map +1 -1
- package/dist/src/plugins/preferences/preferences-plugin.js +63 -19
- package/dist/src/plugins/preferences/preferences-plugin.js.map +1 -1
- package/dist/src/plugins/profiles/index.d.ts +0 -4
- package/dist/src/plugins/profiles/index.d.ts.map +1 -1
- package/dist/src/plugins/profiles/index.js +2 -3
- package/dist/src/plugins/profiles/index.js.map +1 -1
- package/dist/src/plugins/profiles/profiles-plugin.d.ts +5 -2
- package/dist/src/plugins/profiles/profiles-plugin.d.ts.map +1 -1
- package/dist/src/plugins/profiles/profiles-plugin.js +60 -26
- package/dist/src/plugins/profiles/profiles-plugin.js.map +1 -1
- package/dist/src/plugins/profiles/types.d.ts +9 -2
- package/dist/src/plugins/profiles/types.d.ts.map +1 -1
- package/dist/src/plugins/qwickbrain/index.d.ts +0 -4
- package/dist/src/plugins/qwickbrain/index.d.ts.map +1 -1
- package/dist/src/plugins/qwickbrain/index.js +2 -3
- package/dist/src/plugins/qwickbrain/index.js.map +1 -1
- package/dist/src/plugins/qwickbrain/qwickbrain-plugin.d.ts.map +1 -1
- package/dist/src/plugins/qwickbrain/qwickbrain-plugin.js +117 -0
- package/dist/src/plugins/qwickbrain/qwickbrain-plugin.js.map +1 -1
- package/dist/src/plugins/rate-limit/index.d.ts +0 -4
- package/dist/src/plugins/rate-limit/index.d.ts.map +1 -1
- package/dist/src/plugins/rate-limit/index.js +2 -3
- package/dist/src/plugins/rate-limit/index.js.map +1 -1
- package/dist/src/plugins/subscriptions/index.d.ts +0 -4
- package/dist/src/plugins/subscriptions/index.d.ts.map +1 -1
- package/dist/src/plugins/subscriptions/index.js +2 -3
- package/dist/src/plugins/subscriptions/index.js.map +1 -1
- package/dist/src/plugins/subscriptions/subscriptions-plugin.d.ts +5 -2
- package/dist/src/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -1
- package/dist/src/plugins/subscriptions/subscriptions-plugin.js +63 -29
- package/dist/src/plugins/subscriptions/subscriptions-plugin.js.map +1 -1
- package/dist/src/plugins/subscriptions/types.d.ts +8 -2
- package/dist/src/plugins/subscriptions/types.d.ts.map +1 -1
- package/dist/src/plugins/tenants/tenants-plugin.d.ts +5 -2
- package/dist/src/plugins/tenants/tenants-plugin.d.ts.map +1 -1
- package/dist/src/plugins/tenants/tenants-plugin.js +91 -58
- package/dist/src/plugins/tenants/tenants-plugin.js.map +1 -1
- package/dist/src/plugins/tenants/types.d.ts +8 -2
- package/dist/src/plugins/tenants/types.d.ts.map +1 -1
- package/dist/src/plugins/usage/index.d.ts +0 -4
- package/dist/src/plugins/usage/index.d.ts.map +1 -1
- package/dist/src/plugins/usage/index.js +2 -3
- package/dist/src/plugins/usage/index.js.map +1 -1
- package/dist/src/plugins/usage/usage-plugin.d.ts +5 -2
- package/dist/src/plugins/usage/usage-plugin.d.ts.map +1 -1
- package/dist/src/plugins/usage/usage-plugin.js +57 -23
- package/dist/src/plugins/usage/usage-plugin.js.map +1 -1
- package/dist/src/plugins/users/types.d.ts +7 -2
- package/dist/src/plugins/users/types.d.ts.map +1 -1
- package/dist/src/plugins/users/users-plugin.d.ts +5 -2
- package/dist/src/plugins/users/users-plugin.d.ts.map +1 -1
- package/dist/src/plugins/users/users-plugin.js +56 -23
- package/dist/src/plugins/users/users-plugin.js.map +1 -1
- package/dist-ui/assets/index-0gzisPdy.js +528 -0
- package/dist-ui/assets/{index-8y0jDGcd.js.map → index-0gzisPdy.js.map} +1 -1
- package/dist-ui/index.html +1 -1
- package/package.json +8 -5
- package/src/plugins/api-keys/api-keys-plugin.ts +64 -20
- package/src/plugins/api-keys/index.ts +2 -5
- package/src/plugins/api-keys/types.ts +9 -3
- package/src/plugins/auth/index.ts +3 -5
- package/src/plugins/bans/bans-plugin.ts +71 -26
- package/src/plugins/bans/index.ts +3 -5
- package/src/plugins/bans/types.ts +13 -6
- package/src/plugins/devices/devices-plugin.ts +62 -27
- package/src/plugins/devices/index.ts +3 -5
- package/src/plugins/entitlements/entitlements-plugin.ts +81 -43
- package/src/plugins/entitlements/index.ts +3 -5
- package/src/plugins/entitlements/types.ts +9 -2
- package/src/plugins/notifications/index.ts +3 -5
- package/src/plugins/notifications/notifications-plugin.ts +48 -19
- package/src/plugins/parental/index.ts +3 -5
- package/src/plugins/parental/parental-plugin.ts +63 -25
- package/src/plugins/preferences/index.ts +3 -5
- package/src/plugins/preferences/preferences-plugin.ts +66 -20
- package/src/plugins/profiles/index.ts +3 -5
- package/src/plugins/profiles/profiles-plugin.ts +60 -27
- package/src/plugins/profiles/types.ts +9 -2
- package/src/plugins/qwickbrain/index.ts +3 -5
- package/src/plugins/qwickbrain/qwickbrain-plugin.ts +135 -0
- package/src/plugins/rate-limit/index.ts +3 -5
- package/src/plugins/subscriptions/index.ts +3 -5
- package/src/plugins/subscriptions/subscriptions-plugin.ts +63 -30
- package/src/plugins/subscriptions/types.ts +8 -2
- package/src/plugins/tenants/tenants-plugin.ts +95 -60
- package/src/plugins/tenants/types.ts +8 -2
- package/src/plugins/usage/index.ts +3 -5
- package/src/plugins/usage/usage-plugin.ts +60 -26
- package/src/plugins/users/types.ts +7 -2
- package/src/plugins/users/users-plugin.ts +56 -24
- package/dist-ui/assets/index-8y0jDGcd.js +0 -528
|
@@ -530,6 +530,141 @@ export function createQwickBrainPlugin(config: QwickBrainPluginConfig): Plugin {
|
|
|
530
530
|
},
|
|
531
531
|
});
|
|
532
532
|
|
|
533
|
+
// POST /mcp/query - LLM Query endpoint (auth required, supports streaming)
|
|
534
|
+
registry.addRoute({
|
|
535
|
+
method: 'post',
|
|
536
|
+
path: `${apiPrefix}/query`,
|
|
537
|
+
pluginId: 'qwickbrain',
|
|
538
|
+
handler: async (req: Request, res: ExpressResponse) => {
|
|
539
|
+
// Check authentication
|
|
540
|
+
const authError = checkAuth(req);
|
|
541
|
+
if (authError) {
|
|
542
|
+
res.status(authError.status).json(authError.body);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const user = getAuthenticatedUser(req);
|
|
547
|
+
|
|
548
|
+
// Check rate limits
|
|
549
|
+
const rateLimitError = checkRateLimits(user?.id);
|
|
550
|
+
if (rateLimitError) {
|
|
551
|
+
Object.entries(rateLimitError.headers).forEach(([key, value]) => {
|
|
552
|
+
res.setHeader(key, value);
|
|
553
|
+
});
|
|
554
|
+
res.status(rateLimitError.status).json(rateLimitError.body);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
if (!connectionStatus.connected) {
|
|
560
|
+
res.status(503).json({
|
|
561
|
+
error: 'QwickBrain not connected',
|
|
562
|
+
details: connectionStatus.error,
|
|
563
|
+
});
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Check if streaming is requested
|
|
568
|
+
const stream = req.query.stream === 'true';
|
|
569
|
+
|
|
570
|
+
// Build query string
|
|
571
|
+
const queryParams = new URLSearchParams();
|
|
572
|
+
if (stream) {
|
|
573
|
+
queryParams.set('stream', 'true');
|
|
574
|
+
}
|
|
575
|
+
const queryString = queryParams.toString();
|
|
576
|
+
const path = `/api/v1/query${queryString ? `?${queryString}` : ''}`;
|
|
577
|
+
|
|
578
|
+
log('LLM query', { userId: user?.id, streaming: stream, query: req.body.query });
|
|
579
|
+
|
|
580
|
+
if (stream) {
|
|
581
|
+
// Streaming mode: pipe SSE stream from QwickBrain to client
|
|
582
|
+
const url = `${config.qwickbrainUrl}${path}`;
|
|
583
|
+
const controller = new AbortController();
|
|
584
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
const fetchResponse = await fetch(url, {
|
|
588
|
+
method: 'POST',
|
|
589
|
+
headers: {
|
|
590
|
+
'Content-Type': 'application/json',
|
|
591
|
+
'Accept': 'text/event-stream',
|
|
592
|
+
},
|
|
593
|
+
body: JSON.stringify(req.body),
|
|
594
|
+
signal: controller.signal,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
clearTimeout(timeoutId);
|
|
598
|
+
|
|
599
|
+
if (!fetchResponse.ok) {
|
|
600
|
+
res.status(fetchResponse.status).json({
|
|
601
|
+
error: 'Query failed',
|
|
602
|
+
status: fetchResponse.status,
|
|
603
|
+
});
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Set SSE headers
|
|
608
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
609
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
610
|
+
res.setHeader('Connection', 'keep-alive');
|
|
611
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
612
|
+
|
|
613
|
+
// Pipe the stream
|
|
614
|
+
const reader = fetchResponse.body?.getReader();
|
|
615
|
+
if (!reader) {
|
|
616
|
+
res.status(500).json({ error: 'No response body' });
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const decoder = new TextDecoder();
|
|
621
|
+
|
|
622
|
+
while (true) {
|
|
623
|
+
const { done, value } = await reader.read();
|
|
624
|
+
if (done) break;
|
|
625
|
+
const text = decoder.decode(value, { stream: true });
|
|
626
|
+
res.write(text);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
res.end();
|
|
630
|
+
} catch (error) {
|
|
631
|
+
clearTimeout(timeoutId);
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
} else {
|
|
635
|
+
// Non-streaming mode: proxy JSON response
|
|
636
|
+
const response = await proxyToQwickBrain(
|
|
637
|
+
config.qwickbrainUrl,
|
|
638
|
+
path,
|
|
639
|
+
{
|
|
640
|
+
method: 'POST',
|
|
641
|
+
body: req.body,
|
|
642
|
+
timeout,
|
|
643
|
+
}
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
if (!response.ok) {
|
|
647
|
+
const errorText = await response.text();
|
|
648
|
+
res.status(response.status).json({
|
|
649
|
+
error: 'Query failed',
|
|
650
|
+
details: errorText,
|
|
651
|
+
});
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const result = await response.json();
|
|
656
|
+
res.json(result);
|
|
657
|
+
}
|
|
658
|
+
} catch (error) {
|
|
659
|
+
log('Error executing query', { error: String(error) });
|
|
660
|
+
res.status(500).json({
|
|
661
|
+
error: 'Query execution failed',
|
|
662
|
+
details: error instanceof Error ? error.message : 'Unknown error',
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
|
|
533
668
|
// GET /mcp/sse - Server-Sent Events endpoint for streaming (auth required)
|
|
534
669
|
registry.addRoute({
|
|
535
670
|
method: 'get',
|
|
@@ -127,8 +127,6 @@ export type {
|
|
|
127
127
|
CheckLimitOptions,
|
|
128
128
|
} from './types.js';
|
|
129
129
|
|
|
130
|
-
// UI Components
|
|
131
|
-
export
|
|
132
|
-
|
|
133
|
-
export type { RateLimitStatusWidgetProps } from './RateLimitStatusWidget.js';
|
|
134
|
-
export type { RateLimitManagementPageProps } from './RateLimitManagementPage.js';
|
|
130
|
+
// UI Components are exported from main package index (@qwickapps/server)
|
|
131
|
+
// Do NOT export here to avoid loading UI dependencies when importing plugins
|
|
132
|
+
|
|
@@ -50,8 +50,6 @@ export type {
|
|
|
50
50
|
// Stores
|
|
51
51
|
export { postgresSubscriptionsStore } from './stores/index.js';
|
|
52
52
|
|
|
53
|
-
// UI Components
|
|
54
|
-
export
|
|
55
|
-
|
|
56
|
-
export { SubscriptionsManagementPage } from './SubscriptionsManagementPage.js';
|
|
57
|
-
export type { SubscriptionsManagementPageProps } from './SubscriptionsManagementPage.js';
|
|
53
|
+
// UI Components are exported from main package index (@qwickapps/server)
|
|
54
|
+
// Do NOT export here to avoid loading UI dependencies when importing plugins
|
|
55
|
+
|
|
@@ -23,22 +23,26 @@ import type {
|
|
|
23
23
|
UpdateUserSubscriptionInput,
|
|
24
24
|
FeatureLimitResult,
|
|
25
25
|
} from './types.js';
|
|
26
|
+
import { hasPostgres, getPostgres } from '../postgres-plugin.js';
|
|
27
|
+
import { postgresSubscriptionsStore } from './stores/index.js';
|
|
26
28
|
|
|
27
29
|
// Store instance for helper access
|
|
28
30
|
let currentStore: SubscriptionsStore | null = null;
|
|
29
31
|
let currentConfig: SubscriptionsPluginConfig | null = null;
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
|
-
* Create the Subscriptions plugin
|
|
34
|
+
* Create the Subscriptions plugin with smart defaults
|
|
35
|
+
*
|
|
36
|
+
* Config is optional - plugin will use defaults and get dependencies from registry.
|
|
37
|
+
* Gracefully handles missing dependencies with clear log messages.
|
|
33
38
|
*/
|
|
34
|
-
export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Plugin {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
console.log(`[SubscriptionsPlugin] ${message}`, data || '');
|
|
39
|
+
export function createSubscriptionsPlugin(config: Partial<SubscriptionsPluginConfig> = {}): Plugin {
|
|
40
|
+
function log(message: string, data?: Record<string, unknown>, isError = false) {
|
|
41
|
+
const prefix = '[SubscriptionsPlugin]';
|
|
42
|
+
if (isError) {
|
|
43
|
+
console.error(`${prefix} ${message}`, data || '');
|
|
44
|
+
} else if (config.debug) {
|
|
45
|
+
console.log(`${prefix} ${message}`, data || '');
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
|
|
@@ -48,15 +52,44 @@ export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Pl
|
|
|
48
52
|
version: '1.0.0',
|
|
49
53
|
|
|
50
54
|
async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
|
|
55
|
+
const logger = registry.getLogger('subscriptions');
|
|
56
|
+
|
|
57
|
+
// Check for postgres in registry
|
|
58
|
+
if (!hasPostgres()) {
|
|
59
|
+
logger.warn('No Database! Subscriptions plugin disabled.');
|
|
60
|
+
registry.registerHealthCheck({
|
|
61
|
+
name: 'subscriptions-store',
|
|
62
|
+
type: 'custom',
|
|
63
|
+
check: async () => ({
|
|
64
|
+
healthy: false,
|
|
65
|
+
details: {
|
|
66
|
+
error: 'PostgreSQL not available',
|
|
67
|
+
state: 'disabled',
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Smart defaults - get dependencies from registry
|
|
75
|
+
const store = config.store ?? postgresSubscriptionsStore({
|
|
76
|
+
pool: () => getPostgres().getPool(),
|
|
77
|
+
autoCreateTables: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const debug = config.debug ?? false;
|
|
81
|
+
const defaultTierSlug = config.defaultTierSlug ?? 'free';
|
|
82
|
+
const apiPrefix = config.api?.prefix ?? '/subscriptions';
|
|
83
|
+
|
|
51
84
|
log('Starting subscriptions plugin');
|
|
52
85
|
|
|
53
86
|
// Initialize the store (creates tables if needed)
|
|
54
|
-
await
|
|
87
|
+
await store.initialize();
|
|
55
88
|
log('Subscriptions plugin migrations complete');
|
|
56
89
|
|
|
57
90
|
// Store references for helper access
|
|
58
|
-
currentStore =
|
|
59
|
-
currentConfig = config;
|
|
91
|
+
currentStore = store;
|
|
92
|
+
currentConfig = { ...config, store, debug, defaultTierSlug };
|
|
60
93
|
|
|
61
94
|
// Register health check
|
|
62
95
|
registry.registerHealthCheck({
|
|
@@ -64,7 +97,7 @@ export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Pl
|
|
|
64
97
|
type: 'custom',
|
|
65
98
|
check: async () => {
|
|
66
99
|
try {
|
|
67
|
-
const tiers = await
|
|
100
|
+
const tiers = await store.listTiers(true);
|
|
68
101
|
return {
|
|
69
102
|
healthy: true,
|
|
70
103
|
details: {
|
|
@@ -88,14 +121,14 @@ export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Pl
|
|
|
88
121
|
handler: async (req: Request, res: Response) => {
|
|
89
122
|
try {
|
|
90
123
|
const activeOnly = req.query.active !== 'false';
|
|
91
|
-
const tiers = await
|
|
124
|
+
const tiers = await store.listTiers(activeOnly);
|
|
92
125
|
|
|
93
126
|
// Include entitlements if requested
|
|
94
127
|
if (req.query.include === 'entitlements') {
|
|
95
128
|
const tiersWithEntitlements = await Promise.all(
|
|
96
129
|
tiers.map(async (tier) => ({
|
|
97
130
|
...tier,
|
|
98
|
-
entitlements: await
|
|
131
|
+
entitlements: await store.getEntitlementsByTier(tier.id),
|
|
99
132
|
}))
|
|
100
133
|
);
|
|
101
134
|
return res.json({ tiers: tiersWithEntitlements });
|
|
@@ -119,16 +152,16 @@ export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Pl
|
|
|
119
152
|
const { idOrSlug } = req.params;
|
|
120
153
|
|
|
121
154
|
// Try by ID first, then by slug
|
|
122
|
-
let tier = await
|
|
155
|
+
let tier = await store.getTierById(idOrSlug);
|
|
123
156
|
if (!tier) {
|
|
124
|
-
tier = await
|
|
157
|
+
tier = await store.getTierBySlug(idOrSlug);
|
|
125
158
|
}
|
|
126
159
|
|
|
127
160
|
if (!tier) {
|
|
128
161
|
return res.status(404).json({ error: 'Tier not found' });
|
|
129
162
|
}
|
|
130
163
|
|
|
131
|
-
const entitlements = await
|
|
164
|
+
const entitlements = await store.getEntitlementsByTier(tier.id);
|
|
132
165
|
res.json({ ...tier, entitlements });
|
|
133
166
|
} catch (error) {
|
|
134
167
|
console.error('[SubscriptionsPlugin] Get tier error:', error);
|
|
@@ -151,12 +184,12 @@ export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Pl
|
|
|
151
184
|
}
|
|
152
185
|
|
|
153
186
|
// Check for duplicate slug
|
|
154
|
-
const existing = await
|
|
187
|
+
const existing = await store.getTierBySlug(input.slug);
|
|
155
188
|
if (existing) {
|
|
156
189
|
return res.status(409).json({ error: 'Tier with this slug already exists' });
|
|
157
190
|
}
|
|
158
191
|
|
|
159
|
-
const tier = await
|
|
192
|
+
const tier = await store.createTier(input);
|
|
160
193
|
res.status(201).json(tier);
|
|
161
194
|
} catch (error) {
|
|
162
195
|
console.error('[SubscriptionsPlugin] Create tier error:', error);
|
|
@@ -173,7 +206,7 @@ export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Pl
|
|
|
173
206
|
handler: async (req: Request, res: Response) => {
|
|
174
207
|
try {
|
|
175
208
|
const input: UpdateTierInput = req.body;
|
|
176
|
-
const tier = await
|
|
209
|
+
const tier = await store.updateTier(req.params.id, input);
|
|
177
210
|
|
|
178
211
|
if (!tier) {
|
|
179
212
|
return res.status(404).json({ error: 'Tier not found' });
|
|
@@ -200,8 +233,8 @@ export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Pl
|
|
|
200
233
|
return res.status(400).json({ error: 'entitlements array is required' });
|
|
201
234
|
}
|
|
202
235
|
|
|
203
|
-
await
|
|
204
|
-
const updatedEntitlements = await
|
|
236
|
+
await store.setTierEntitlements(req.params.id, entitlements);
|
|
237
|
+
const updatedEntitlements = await store.getEntitlementsByTier(req.params.id);
|
|
205
238
|
|
|
206
239
|
res.json({ entitlements: updatedEntitlements });
|
|
207
240
|
} catch (error) {
|
|
@@ -221,14 +254,14 @@ export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Pl
|
|
|
221
254
|
pluginId: 'subscriptions',
|
|
222
255
|
handler: async (req: Request, res: Response) => {
|
|
223
256
|
try {
|
|
224
|
-
const subscription = await
|
|
257
|
+
const subscription = await store.getActiveSubscription(req.params.userId);
|
|
225
258
|
|
|
226
259
|
if (!subscription) {
|
|
227
260
|
return res.status(404).json({ error: 'No active subscription found' });
|
|
228
261
|
}
|
|
229
262
|
|
|
230
263
|
// Get entitlements for the tier
|
|
231
|
-
const entitlements = await
|
|
264
|
+
const entitlements = await store.getEntitlementsByTier(subscription.tier_id);
|
|
232
265
|
|
|
233
266
|
res.json({
|
|
234
267
|
subscription,
|
|
@@ -255,14 +288,14 @@ export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Pl
|
|
|
255
288
|
|
|
256
289
|
if (!input.tier_id) {
|
|
257
290
|
// Use default tier if not specified
|
|
258
|
-
const defaultTier = await
|
|
291
|
+
const defaultTier = await store.getTierBySlug(defaultTierSlug);
|
|
259
292
|
if (!defaultTier) {
|
|
260
293
|
return res.status(400).json({ error: 'tier_id is required (no default tier found)' });
|
|
261
294
|
}
|
|
262
295
|
input.tier_id = defaultTier.id;
|
|
263
296
|
}
|
|
264
297
|
|
|
265
|
-
const subscription = await
|
|
298
|
+
const subscription = await store.createUserSubscription(input);
|
|
266
299
|
res.status(201).json(subscription);
|
|
267
300
|
} catch (error) {
|
|
268
301
|
console.error('[SubscriptionsPlugin] Create subscription error:', error);
|
|
@@ -295,13 +328,13 @@ export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Pl
|
|
|
295
328
|
pluginId: 'subscriptions',
|
|
296
329
|
handler: async (req: Request, res: Response) => {
|
|
297
330
|
try {
|
|
298
|
-
const success = await
|
|
331
|
+
const success = await store.cancelSubscription(req.params.id);
|
|
299
332
|
|
|
300
333
|
if (!success) {
|
|
301
334
|
return res.status(404).json({ error: 'Subscription not found' });
|
|
302
335
|
}
|
|
303
336
|
|
|
304
|
-
const subscription = await
|
|
337
|
+
const subscription = await store.getUserSubscriptionById(req.params.id);
|
|
305
338
|
res.json(subscription);
|
|
306
339
|
} catch (error) {
|
|
307
340
|
console.error('[SubscriptionsPlugin] Cancel subscription error:', error);
|
|
@@ -316,7 +349,7 @@ export function createSubscriptionsPlugin(config: SubscriptionsPluginConfig): Pl
|
|
|
316
349
|
|
|
317
350
|
async onStop(): Promise<void> {
|
|
318
351
|
log('Stopping subscriptions plugin');
|
|
319
|
-
await
|
|
352
|
+
if (currentStore) { await currentStore.shutdown(); };
|
|
320
353
|
currentStore = null;
|
|
321
354
|
currentConfig = null;
|
|
322
355
|
log('Subscriptions plugin stopped');
|
|
@@ -340,10 +340,16 @@ export interface SubscriptionsApiConfig {
|
|
|
340
340
|
|
|
341
341
|
/**
|
|
342
342
|
* Subscriptions plugin configuration
|
|
343
|
+
*
|
|
344
|
+
* All properties are optional - plugin will use smart defaults:
|
|
345
|
+
* - store: Postgres subscriptions store using registry's postgres instance
|
|
346
|
+
* - defaultTierSlug: 'free'
|
|
347
|
+
* - api.prefix: '/subscriptions'
|
|
348
|
+
* - debug: false
|
|
343
349
|
*/
|
|
344
350
|
export interface SubscriptionsPluginConfig {
|
|
345
|
-
/** Subscriptions storage backend */
|
|
346
|
-
store
|
|
351
|
+
/** Subscriptions storage backend (default: postgres subscriptions store from registry) */
|
|
352
|
+
store?: SubscriptionsStore;
|
|
347
353
|
/** Default tier slug for new users (default: 'free') */
|
|
348
354
|
defaultTierSlug?: string;
|
|
349
355
|
/** Whether to auto-create default subscription for new users */
|
|
@@ -23,58 +23,26 @@ import type {
|
|
|
23
23
|
} from './types.js';
|
|
24
24
|
import { getUserById } from '../users/users-plugin.js';
|
|
25
25
|
import { getAuthenticatedUser, isAuthenticated } from '../auth/index.js';
|
|
26
|
+
import { hasPostgres, getPostgres } from '../postgres-plugin.js';
|
|
27
|
+
import { postgresTenantStore } from './stores/index.js';
|
|
26
28
|
|
|
27
29
|
// Store instance and registry for helper access
|
|
28
30
|
let currentStore: TenantStore | null = null;
|
|
29
31
|
let currentRegistry: PluginRegistry | null = null;
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
|
-
* Create the Tenants plugin
|
|
34
|
+
* Create the Tenants plugin with smart defaults
|
|
35
|
+
*
|
|
36
|
+
* Config is optional - plugin will use defaults and get dependencies from registry.
|
|
37
|
+
* Gracefully handles missing dependencies with clear log messages.
|
|
33
38
|
*/
|
|
34
|
-
export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
console.log(`[TenantsPlugin] ${message}`, data || '');
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Helper to check if user has access to a tenant
|
|
47
|
-
*/
|
|
48
|
-
async function canAccessTenant(userId: string, tenantId: string): Promise<boolean> {
|
|
49
|
-
try {
|
|
50
|
-
const membership = await config.store.getTenantForUser(tenantId, userId);
|
|
51
|
-
return membership !== null;
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Helper to check if user has admin/owner role in a tenant
|
|
59
|
-
*/
|
|
60
|
-
async function canManageTenant(userId: string, tenantId: string): Promise<boolean> {
|
|
61
|
-
try {
|
|
62
|
-
const membership = await config.store.getTenantForUser(tenantId, userId);
|
|
63
|
-
return membership !== null && ['owner', 'admin'].includes(membership.user_role);
|
|
64
|
-
} catch {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Helper to check if user is owner of a tenant
|
|
71
|
-
*/
|
|
72
|
-
async function isOwnerOfTenant(userId: string, tenantId: string): Promise<boolean> {
|
|
73
|
-
try {
|
|
74
|
-
const membership = await config.store.getTenantForUser(tenantId, userId);
|
|
75
|
-
return membership !== null && membership.user_role === 'owner';
|
|
76
|
-
} catch {
|
|
77
|
-
return false;
|
|
39
|
+
export function createTenantsPlugin(config: Partial<TenantsPluginConfig> = {}): Plugin {
|
|
40
|
+
function log(message: string, data?: Record<string, unknown>, isError = false) {
|
|
41
|
+
const prefix = '[TenantsPlugin]';
|
|
42
|
+
if (isError) {
|
|
43
|
+
console.error(`${prefix} ${message}`, data || '');
|
|
44
|
+
} else if (config.debug) {
|
|
45
|
+
console.log(`${prefix} ${message}`, data || '');
|
|
78
46
|
}
|
|
79
47
|
}
|
|
80
48
|
|
|
@@ -84,14 +52,43 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
84
52
|
version: '1.0.0',
|
|
85
53
|
|
|
86
54
|
async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
|
|
55
|
+
const logger = registry.getLogger('tenants');
|
|
56
|
+
|
|
57
|
+
// Check for postgres in registry
|
|
58
|
+
if (!hasPostgres()) {
|
|
59
|
+
logger.warn('No Database! Tenants plugin disabled.');
|
|
60
|
+
registry.registerHealthCheck({
|
|
61
|
+
name: 'tenants-store',
|
|
62
|
+
type: 'custom',
|
|
63
|
+
check: async () => ({
|
|
64
|
+
healthy: false,
|
|
65
|
+
details: {
|
|
66
|
+
error: 'PostgreSQL not available',
|
|
67
|
+
state: 'disabled',
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Smart defaults - get dependencies from registry
|
|
75
|
+
const store = config.store ?? postgresTenantStore({
|
|
76
|
+
pool: () => getPostgres().getPool(),
|
|
77
|
+
autoCreateTables: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const debug = config.debug ?? false;
|
|
81
|
+
const apiPrefix = config.apiPrefix ?? '/tenants';
|
|
82
|
+
const apiEnabled = config.apiEnabled ?? true;
|
|
83
|
+
|
|
87
84
|
log('Starting tenants plugin');
|
|
88
85
|
|
|
89
86
|
// Initialize the store (creates tables if needed)
|
|
90
|
-
await
|
|
87
|
+
await store.initialize();
|
|
91
88
|
log('Tenants plugin migrations complete');
|
|
92
89
|
|
|
93
90
|
// Store references for helper access
|
|
94
|
-
currentStore =
|
|
91
|
+
currentStore = store;
|
|
95
92
|
currentRegistry = registry;
|
|
96
93
|
|
|
97
94
|
// Register health check
|
|
@@ -101,7 +98,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
101
98
|
check: async () => {
|
|
102
99
|
try {
|
|
103
100
|
// Simple health check - try to search with limit 1
|
|
104
|
-
await
|
|
101
|
+
await store.search({ limit: 1 });
|
|
105
102
|
return { healthy: true };
|
|
106
103
|
} catch {
|
|
107
104
|
return { healthy: false };
|
|
@@ -109,6 +106,42 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
109
106
|
},
|
|
110
107
|
});
|
|
111
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Helper to check if user has access to a tenant
|
|
111
|
+
*/
|
|
112
|
+
async function canAccessTenant(userId: string, tenantId: string): Promise<boolean> {
|
|
113
|
+
try {
|
|
114
|
+
const membership = await store.getTenantForUser(tenantId, userId);
|
|
115
|
+
return membership !== null;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Helper to check if user has admin/owner role in a tenant
|
|
123
|
+
*/
|
|
124
|
+
async function canManageTenant(userId: string, tenantId: string): Promise<boolean> {
|
|
125
|
+
try {
|
|
126
|
+
const membership = await store.getTenantForUser(tenantId, userId);
|
|
127
|
+
return membership !== null && ['owner', 'admin'].includes(membership.user_role);
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Helper to check if user is owner of a tenant
|
|
135
|
+
*/
|
|
136
|
+
async function isOwnerOfTenant(userId: string, tenantId: string): Promise<boolean> {
|
|
137
|
+
try {
|
|
138
|
+
const membership = await store.getTenantForUser(tenantId, userId);
|
|
139
|
+
return membership !== null && membership.user_role === 'owner';
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
112
145
|
if (!apiEnabled) return;
|
|
113
146
|
|
|
114
147
|
// ========================================================================
|
|
@@ -140,7 +173,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
140
173
|
|
|
141
174
|
// Users can only search their own tenants
|
|
142
175
|
// Get all tenants user belongs to
|
|
143
|
-
const userTenants = await
|
|
176
|
+
const userTenants = await store.getTenantsForUser(user.id);
|
|
144
177
|
res.json({
|
|
145
178
|
tenants: userTenants,
|
|
146
179
|
total: userTenants.length,
|
|
@@ -186,7 +219,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
186
219
|
});
|
|
187
220
|
}
|
|
188
221
|
|
|
189
|
-
const tenant = await
|
|
222
|
+
const tenant = await store.getById(req.params.id);
|
|
190
223
|
if (!tenant) {
|
|
191
224
|
return res.status(404).json({ error: 'Tenant not found' });
|
|
192
225
|
}
|
|
@@ -252,10 +285,10 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
252
285
|
});
|
|
253
286
|
}
|
|
254
287
|
|
|
255
|
-
const tenant = await
|
|
288
|
+
const tenant = await store.create(input);
|
|
256
289
|
|
|
257
290
|
// Automatically add creator as owner
|
|
258
|
-
await
|
|
291
|
+
await store.addMember({
|
|
259
292
|
tenant_id: tenant.id,
|
|
260
293
|
user_id: user.id,
|
|
261
294
|
role: 'owner',
|
|
@@ -306,7 +339,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
306
339
|
metadata: req.body.metadata,
|
|
307
340
|
};
|
|
308
341
|
|
|
309
|
-
const tenant = await
|
|
342
|
+
const tenant = await store.update(req.params.id, input);
|
|
310
343
|
if (!tenant) {
|
|
311
344
|
return res.status(404).json({ error: 'Tenant not found' });
|
|
312
345
|
}
|
|
@@ -351,7 +384,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
351
384
|
});
|
|
352
385
|
}
|
|
353
386
|
|
|
354
|
-
const deleted = await
|
|
387
|
+
const deleted = await store.delete(req.params.id);
|
|
355
388
|
if (!deleted) {
|
|
356
389
|
return res.status(404).json({ error: 'Tenant not found' });
|
|
357
390
|
}
|
|
@@ -400,7 +433,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
400
433
|
});
|
|
401
434
|
}
|
|
402
435
|
|
|
403
|
-
const tenants = await
|
|
436
|
+
const tenants = await store.getTenantsForUser(req.params.userId);
|
|
404
437
|
res.json({ tenants, total: tenants.length });
|
|
405
438
|
} catch (error) {
|
|
406
439
|
console.error('[TenantsPlugin] Get user tenants error:', error);
|
|
@@ -444,7 +477,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
444
477
|
});
|
|
445
478
|
}
|
|
446
479
|
|
|
447
|
-
const members = await
|
|
480
|
+
const members = await store.getMembers(req.params.tenantId);
|
|
448
481
|
res.json({ members, total: members.length });
|
|
449
482
|
} catch (error) {
|
|
450
483
|
console.error('[TenantsPlugin] Get members error:', error);
|
|
@@ -506,7 +539,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
506
539
|
});
|
|
507
540
|
}
|
|
508
541
|
|
|
509
|
-
const membership = await
|
|
542
|
+
const membership = await store.addMember(input);
|
|
510
543
|
log('Member added to tenant', {
|
|
511
544
|
tenantId: input.tenant_id,
|
|
512
545
|
userId: input.user_id,
|
|
@@ -568,7 +601,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
568
601
|
});
|
|
569
602
|
}
|
|
570
603
|
|
|
571
|
-
const membership = await
|
|
604
|
+
const membership = await store.updateMember(req.params.tenantId, req.params.userId, input);
|
|
572
605
|
if (!membership) {
|
|
573
606
|
return res.status(404).json({ error: 'Membership not found' });
|
|
574
607
|
}
|
|
@@ -618,7 +651,7 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
618
651
|
});
|
|
619
652
|
}
|
|
620
653
|
|
|
621
|
-
const deleted = await
|
|
654
|
+
const deleted = await store.removeMember(req.params.tenantId, req.params.userId);
|
|
622
655
|
if (!deleted) {
|
|
623
656
|
return res.status(404).json({ error: 'Membership not found' });
|
|
624
657
|
}
|
|
@@ -641,7 +674,9 @@ export function createTenantsPlugin(config: TenantsPluginConfig): Plugin {
|
|
|
641
674
|
|
|
642
675
|
async onStop(): Promise<void> {
|
|
643
676
|
log('Stopping tenants plugin');
|
|
644
|
-
|
|
677
|
+
if (currentStore) {
|
|
678
|
+
await currentStore.shutdown();
|
|
679
|
+
}
|
|
645
680
|
currentStore = null;
|
|
646
681
|
currentRegistry = null;
|
|
647
682
|
},
|