@unchainedshop/api 4.8.26 → 4.8.27

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.
Files changed (57) hide show
  1. package/lib/adminUiPlugins.d.ts +97 -0
  2. package/lib/adminUiPlugins.js +164 -0
  3. package/lib/auth.d.ts +36 -0
  4. package/lib/auth.js +167 -0
  5. package/lib/express/mountPluginRoutes.d.ts +3 -0
  6. package/lib/express/mountPluginRoutes.js +59 -0
  7. package/lib/express/mountRoutes.d.ts +3 -0
  8. package/lib/express/mountRoutes.js +57 -0
  9. package/lib/fastify/mountPluginRoutes.d.ts +3 -0
  10. package/lib/fastify/mountPluginRoutes.js +62 -0
  11. package/lib/fastify/mountRoutes.d.ts +3 -0
  12. package/lib/fastify/mountRoutes.js +60 -0
  13. package/lib/handlers/createBackchannelLogoutHandler.d.ts +4 -0
  14. package/lib/handlers/createBackchannelLogoutHandler.js +170 -0
  15. package/lib/middleware/createAuthMiddleware.d.ts +29 -0
  16. package/lib/middleware/createAuthMiddleware.js +91 -0
  17. package/lib/resolvers/mutations/accounts/logoutAllSessions.d.ts +6 -0
  18. package/lib/resolvers/mutations/accounts/logoutAllSessions.js +17 -0
  19. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.d.ts +10 -0
  20. package/lib/resolvers/mutations/bulk/bulkAssignProductsToAssortment.js +18 -0
  21. package/lib/resolvers/mutations/bulk/bulkOperation.d.ts +12 -0
  22. package/lib/resolvers/mutations/bulk/bulkOperation.js +14 -0
  23. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.d.ts +9 -0
  24. package/lib/resolvers/mutations/bulk/bulkRemoveAssortments.js +7 -0
  25. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.d.ts +9 -0
  26. package/lib/resolvers/mutations/bulk/bulkRemoveFilters.js +9 -0
  27. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.d.ts +9 -0
  28. package/lib/resolvers/mutations/bulk/bulkRemoveProducts.js +9 -0
  29. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.d.ts +9 -0
  30. package/lib/resolvers/mutations/bulk/bulkRemoveUsers.js +7 -0
  31. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.d.ts +10 -0
  32. package/lib/resolvers/mutations/bulk/bulkSetAssortmentActive.js +9 -0
  33. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.d.ts +10 -0
  34. package/lib/resolvers/mutations/bulk/bulkSetFilterActive.js +10 -0
  35. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.d.ts +10 -0
  36. package/lib/resolvers/mutations/bulk/bulkSetProductStatus.js +17 -0
  37. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.d.ts +10 -0
  38. package/lib/resolvers/mutations/bulk/bulkSetUserRoles.js +14 -0
  39. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.d.ts +11 -0
  40. package/lib/resolvers/mutations/bulk/bulkUpdateAssortmentTags.js +10 -0
  41. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.d.ts +11 -0
  42. package/lib/resolvers/mutations/bulk/bulkUpdateProductTags.js +7 -0
  43. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.d.ts +11 -0
  44. package/lib/resolvers/mutations/bulk/bulkUpdateUserTags.js +7 -0
  45. package/lib/resolvers/queries/search/globalSearch.d.ts +36 -0
  46. package/lib/resolvers/queries/search/globalSearch.js +200 -0
  47. package/lib/resolvers/type/global-search-result-types.d.ts +3 -0
  48. package/lib/resolvers/type/global-search-result-types.js +15 -0
  49. package/lib/resolvers/type/order/order-payment-base.d.ts +7 -0
  50. package/lib/resolvers/type/order/order-payment-base.js +14 -0
  51. package/lib/schema/types/common.d.ts +2 -0
  52. package/lib/schema/types/common.js +19 -0
  53. package/lib/utils/mapServiceError.d.ts +1 -0
  54. package/lib/utils/mapServiceError.js +59 -0
  55. package/lib/utils/maskError.d.ts +1 -0
  56. package/lib/utils/maskError.js +22 -0
  57. package/package.json +24 -24
@@ -0,0 +1,97 @@
1
+ export interface AdminUIThemeTokens {
2
+ surface?: string;
3
+ 'surface-subtle'?: string;
4
+ 'surface-raised'?: string;
5
+ 'surface-input'?: string;
6
+ border?: string;
7
+ 'border-subtle'?: string;
8
+ 'text-primary'?: string;
9
+ 'text-secondary'?: string;
10
+ 'text-muted'?: string;
11
+ 'text-on-dark'?: string;
12
+ accent?: string;
13
+ 'accent-hover'?: string;
14
+ danger?: string;
15
+ 'danger-surface'?: string;
16
+ success?: string;
17
+ warning?: string;
18
+ 'focus-ring'?: string;
19
+ 'text-on-accent'?: string;
20
+ }
21
+ export interface AdminUIThemeConfig {
22
+ light?: AdminUIThemeTokens;
23
+ dark?: AdminUIThemeTokens;
24
+ }
25
+ export interface AdminUIPluginEntityConfig {
26
+ path: string;
27
+ label: string;
28
+ icon?: string;
29
+ requiredRole?: string;
30
+ sortOrder?: number;
31
+ components: {
32
+ list: string;
33
+ detail: string;
34
+ create?: string;
35
+ };
36
+ }
37
+ export interface AdminUIPluginPageConfig {
38
+ path: string;
39
+ label: string;
40
+ icon?: string;
41
+ requiredRole?: string;
42
+ sortOrder?: number;
43
+ component: string;
44
+ }
45
+ export interface AdminUIPluginTabConfig {
46
+ label: string;
47
+ component: string;
48
+ requiredRole?: string;
49
+ }
50
+ export interface AdminUIPluginWidgetConfig {
51
+ component: string;
52
+ width?: 'full' | 'half' | 'third';
53
+ }
54
+ export interface AdminUIPluginSlotConfig {
55
+ component: string;
56
+ }
57
+ export interface AdminUIPluginConfig {
58
+ name: string;
59
+ version?: string;
60
+ bundlePath: string;
61
+ navigation?: {
62
+ label: string;
63
+ icon?: string;
64
+ requiredRole?: string;
65
+ sortOrder?: number;
66
+ };
67
+ slots: {
68
+ entities?: AdminUIPluginEntityConfig[];
69
+ pages?: AdminUIPluginPageConfig[];
70
+ 'dashboard:widgets'?: AdminUIPluginWidgetConfig[];
71
+ 'product:tabs'?: AdminUIPluginTabConfig[];
72
+ 'assortment:tabs'?: AdminUIPluginTabConfig[];
73
+ 'filter:tabs'?: AdminUIPluginTabConfig[];
74
+ 'user:tabs'?: AdminUIPluginTabConfig[];
75
+ 'order:tabs'?: AdminUIPluginTabConfig[];
76
+ [key: string]: AdminUIPluginTabConfig[] | AdminUIPluginSlotConfig[] | AdminUIPluginEntityConfig[] | AdminUIPluginPageConfig[] | AdminUIPluginWidgetConfig[] | undefined;
77
+ };
78
+ }
79
+ interface StaticAsset {
80
+ content: string | (() => string);
81
+ contentType: string;
82
+ cacheControl: string;
83
+ etag?: string;
84
+ }
85
+ export interface PreparedPluginAssets {
86
+ routes: Map<string, StaticAsset>;
87
+ validPlugins: AdminUIPluginConfig[];
88
+ }
89
+ export declare const resolveAdminUIPath: () => string | null;
90
+ export declare const parseBundleExports: (bundle: string) => Set<string> | null;
91
+ export declare function preparePluginAssets(plugins: AdminUIPluginConfig[], log: {
92
+ info: (...args: any[]) => void;
93
+ warn: (...args: any[]) => void;
94
+ }, options?: {
95
+ devMode?: boolean;
96
+ }): PreparedPluginAssets;
97
+ export {};
@@ -0,0 +1,164 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ const PLUGIN_NAME_RE = /^[a-z0-9]([a-z0-9_-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9_-]*[a-z0-9])?)*$/i;
5
+ const IMMUTABLE_CACHE = 'public, max-age=31536000, immutable';
6
+ export const resolveAdminUIPath = () => {
7
+ try {
8
+ const staticURL = import.meta.resolve('@unchainedshop/admin-ui');
9
+ return new URL(staticURL).pathname.split('/').slice(0, -1).join('/');
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ };
15
+ const contentHash = (content) => createHash('sha256').update(content).digest('hex').slice(0, 8);
16
+ export const parseBundleExports = (bundle) => {
17
+ const returnMatch = bundle.match(/return __toCommonJS\(([\w$]+)\);/);
18
+ if (!returnMatch)
19
+ return null;
20
+ const blockMatch = bundle.match(new RegExp(`__export\\(${returnMatch[1].replace(/\$/g, '\\$')},\\s*\\{([\\s\\S]*?)\\}\\);`));
21
+ if (!blockMatch)
22
+ return null;
23
+ const names = new Set();
24
+ for (const m of blockMatch[1].matchAll(/(?:^|,)\s*(?:"([^"]+)"|([\w$]+)):\s*\(\)\s*=>/g)) {
25
+ names.add(m[1] ?? m[2]);
26
+ }
27
+ return names.size > 0 ? names : null;
28
+ };
29
+ const collectReferencedComponents = (plugin) => {
30
+ const names = new Set();
31
+ for (const [slotId, configs] of Object.entries(plugin.slots || {})) {
32
+ if (!Array.isArray(configs))
33
+ continue;
34
+ for (const config of configs) {
35
+ if (slotId === 'entities') {
36
+ const components = config.components;
37
+ if (components?.list)
38
+ names.add(components.list);
39
+ if (components?.detail)
40
+ names.add(components.detail);
41
+ if (components?.create)
42
+ names.add(components.create);
43
+ }
44
+ else if (typeof config.component === 'string') {
45
+ names.add(config.component);
46
+ }
47
+ }
48
+ }
49
+ return [...names];
50
+ };
51
+ export function preparePluginAssets(plugins, log, options = {}) {
52
+ const { devMode = false } = options;
53
+ const routes = new Map();
54
+ const devCacheControl = 'no-cache, no-store, must-revalidate';
55
+ const validPlugins = plugins.filter((p) => {
56
+ if (!PLUGIN_NAME_RE.test(p.name)) {
57
+ log.warn(`Skipping admin-ui plugin with invalid name: "${p.name}"`);
58
+ return false;
59
+ }
60
+ return true;
61
+ });
62
+ const seenNames = new Set();
63
+ for (const plugin of validPlugins) {
64
+ if (seenNames.has(plugin.name)) {
65
+ log.warn(`Duplicate admin-ui plugin name "${plugin.name}": later entries override earlier ones`);
66
+ }
67
+ seenNames.add(plugin.name);
68
+ }
69
+ if (validPlugins.length > 0) {
70
+ const pluginList = validPlugins
71
+ .map((p) => `${p.name}${p.version ? `@${p.version}` : ''}`)
72
+ .join(', ');
73
+ log.info(`Loading ${validPlugins.length} admin-ui plugin(s): ${pluginList}`);
74
+ }
75
+ const resolvedBundlePaths = new Map();
76
+ const pluginBundles = new Map();
77
+ for (const plugin of validPlugins) {
78
+ try {
79
+ const bundlePath = resolve(plugin.bundlePath);
80
+ const content = readFileSync(bundlePath, 'utf-8');
81
+ resolvedBundlePaths.set(plugin.name, bundlePath);
82
+ pluginBundles.set(plugin.name, { content, hash: contentHash(content) });
83
+ }
84
+ catch (err) {
85
+ log.warn(`Failed to read bundle for plugin "${plugin.name}" at ${plugin.bundlePath}: ${err.message}`);
86
+ }
87
+ }
88
+ const pluginsWithBundles = validPlugins.filter((p) => pluginBundles.has(p.name));
89
+ for (const plugin of pluginsWithBundles) {
90
+ const exportNames = parseBundleExports(pluginBundles.get(plugin.name).content);
91
+ if (!exportNames)
92
+ continue;
93
+ const missing = collectReferencedComponents(plugin).filter((name) => !exportNames.has(name));
94
+ if (missing.length > 0) {
95
+ log.warn(`admin-ui plugin "${plugin.name}" references component(s) not exported by its bundle: ${missing.join(', ')}. Exported: ${[...exportNames].join(', ')}`);
96
+ }
97
+ }
98
+ const bundleMtimes = new Map();
99
+ const bundleHashes = new Map();
100
+ for (const [name, bundle] of pluginBundles) {
101
+ bundleHashes.set(name, bundle.hash);
102
+ try {
103
+ bundleMtimes.set(name, statSync(resolvedBundlePaths.get(name)).mtimeMs);
104
+ }
105
+ catch {
106
+ }
107
+ }
108
+ const refreshBundleHash = (name) => {
109
+ const bundlePath = resolvedBundlePaths.get(name);
110
+ try {
111
+ const currentMtime = statSync(bundlePath).mtimeMs;
112
+ const cachedMtime = bundleMtimes.get(name);
113
+ if (cachedMtime !== undefined && cachedMtime === currentMtime) {
114
+ return bundleHashes.get(name);
115
+ }
116
+ const hash = contentHash(readFileSync(bundlePath, 'utf-8'));
117
+ bundleMtimes.set(name, currentMtime);
118
+ bundleHashes.set(name, hash);
119
+ return hash;
120
+ }
121
+ catch {
122
+ return bundleHashes.get(name) ?? pluginBundles.get(name).hash;
123
+ }
124
+ };
125
+ const buildManifestJSON = () => JSON.stringify(pluginsWithBundles.map((plugin) => ({
126
+ ...Object.fromEntries(Object.entries(plugin).filter(([k]) => k !== 'bundlePath')),
127
+ bundleUrl: `/admin-plugins/${plugin.name}.js?v=${devMode ? refreshBundleHash(plugin.name) : pluginBundles.get(plugin.name).hash}`,
128
+ })));
129
+ if (devMode) {
130
+ routes.set('/admin-ui-plugins.json', {
131
+ content: buildManifestJSON,
132
+ contentType: 'application/json',
133
+ cacheControl: devCacheControl,
134
+ });
135
+ }
136
+ else {
137
+ const manifestJSON = buildManifestJSON();
138
+ routes.set('/admin-ui-plugins.json', {
139
+ content: manifestJSON,
140
+ contentType: 'application/json',
141
+ cacheControl: 'public, max-age=0, must-revalidate',
142
+ etag: `"${contentHash(manifestJSON)}"`,
143
+ });
144
+ }
145
+ for (const plugin of pluginsWithBundles) {
146
+ const bundlePath = resolvedBundlePaths.get(plugin.name);
147
+ if (devMode) {
148
+ routes.set(`/admin-plugins/${plugin.name}.js`, {
149
+ content: () => readFileSync(bundlePath, 'utf-8'),
150
+ contentType: 'application/javascript',
151
+ cacheControl: devCacheControl,
152
+ });
153
+ }
154
+ else {
155
+ const { content } = pluginBundles.get(plugin.name);
156
+ routes.set(`/admin-plugins/${plugin.name}.js`, {
157
+ content,
158
+ contentType: 'application/javascript',
159
+ cacheControl: IMMUTABLE_CACHE,
160
+ });
161
+ }
162
+ }
163
+ return { routes, validPlugins };
164
+ }
package/lib/auth.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ export interface AccessTokenPayload {
2
+ iss: string;
3
+ sub: string;
4
+ ver: number;
5
+ imp?: string;
6
+ jti?: string;
7
+ iat?: number;
8
+ exp?: number;
9
+ }
10
+ export interface OIDCProviderConfig {
11
+ issuer: string;
12
+ jwksUri?: string;
13
+ audience?: string | string[];
14
+ }
15
+ export interface AuthConfig {
16
+ oidcProviders?: OIDCProviderConfig[];
17
+ }
18
+ export declare function signAccessToken(userId: string, tokenVersion: number, options?: {
19
+ impersonatorId?: string;
20
+ }): Promise<{
21
+ token: string;
22
+ expires: Date;
23
+ }>;
24
+ export declare function verifyLocalToken(token: string): Promise<AccessTokenPayload | null>;
25
+ export declare function verifyOIDCToken(token: string, providers: OIDCProviderConfig[]): Promise<{
26
+ userId: string;
27
+ roles?: string[];
28
+ } | null>;
29
+ export interface AuthHandlerResult {
30
+ userId?: string;
31
+ tokenVersion?: number;
32
+ impersonatorId?: string;
33
+ accessToken?: string;
34
+ isApiKey?: boolean;
35
+ }
36
+ export declare function createAuthHandler(config?: AuthConfig): (token: string) => Promise<AuthHandlerResult>;
package/lib/auth.js ADDED
@@ -0,0 +1,167 @@
1
+ import { createLogger } from '@unchainedshop/logger';
2
+ import * as jose from 'jose';
3
+ const logger = createLogger('unchained:api:auth');
4
+ const { UNCHAINED_TOKEN_SECRET, UNCHAINED_TOKEN_EXPIRY_SECONDS = '3600', UNCHAINED_TOKEN_ISSUER = 'unchained-engine', } = process.env;
5
+ const MIN_SECRET_LENGTH = 32;
6
+ const jwksCache = new Map();
7
+ function validateSecretStrength(secret) {
8
+ if (secret.length < MIN_SECRET_LENGTH) {
9
+ throw new Error(`UNCHAINED_TOKEN_SECRET must be at least ${MIN_SECRET_LENGTH} characters (256 bits) for security. ` +
10
+ `Current length: ${secret.length}. Generate a secure secret with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"`);
11
+ }
12
+ }
13
+ export async function signAccessToken(userId, tokenVersion, options) {
14
+ if (!UNCHAINED_TOKEN_SECRET) {
15
+ throw new Error('UNCHAINED_TOKEN_SECRET environment variable is required');
16
+ }
17
+ validateSecretStrength(UNCHAINED_TOKEN_SECRET);
18
+ const expirySeconds = parseInt(UNCHAINED_TOKEN_EXPIRY_SECONDS, 10);
19
+ const now = Math.floor(Date.now() / 1000);
20
+ const payload = {
21
+ sub: userId,
22
+ ver: tokenVersion,
23
+ jti: crypto.randomUUID(),
24
+ };
25
+ if (options?.impersonatorId) {
26
+ payload.imp = options.impersonatorId;
27
+ }
28
+ const secret = new TextEncoder().encode(UNCHAINED_TOKEN_SECRET);
29
+ const token = await new jose.SignJWT(payload)
30
+ .setProtectedHeader({ alg: 'HS256' })
31
+ .setIssuedAt(now)
32
+ .setIssuer(UNCHAINED_TOKEN_ISSUER)
33
+ .setExpirationTime(now + expirySeconds)
34
+ .sign(secret);
35
+ const expires = new Date((now + expirySeconds) * 1000);
36
+ return { token, expires };
37
+ }
38
+ export async function verifyLocalToken(token) {
39
+ if (!UNCHAINED_TOKEN_SECRET) {
40
+ logger.warn('UNCHAINED_TOKEN_SECRET not set, cannot verify local tokens');
41
+ return null;
42
+ }
43
+ try {
44
+ validateSecretStrength(UNCHAINED_TOKEN_SECRET);
45
+ const secret = new TextEncoder().encode(UNCHAINED_TOKEN_SECRET);
46
+ const { payload } = await jose.jwtVerify(token, secret, {
47
+ algorithms: ['HS256'],
48
+ issuer: UNCHAINED_TOKEN_ISSUER,
49
+ });
50
+ return payload;
51
+ }
52
+ catch (error) {
53
+ if (error instanceof jose.errors.JWTExpired) {
54
+ logger.debug('Token expired');
55
+ }
56
+ else if (error instanceof jose.errors.JWTInvalid ||
57
+ error instanceof jose.errors.JWSInvalid ||
58
+ error instanceof jose.errors.JWSSignatureVerificationFailed ||
59
+ error instanceof jose.errors.JWTClaimValidationFailed) {
60
+ logger.debug('Invalid token signature or claims');
61
+ }
62
+ else {
63
+ logger.error('Token verification error:', {
64
+ message: error.message,
65
+ name: error.name,
66
+ });
67
+ }
68
+ return null;
69
+ }
70
+ }
71
+ function getJWKS(jwksUri) {
72
+ let jwks = jwksCache.get(jwksUri);
73
+ if (!jwks) {
74
+ jwks = jose.createRemoteJWKSet(new URL(jwksUri), {
75
+ cooldownDuration: 30000,
76
+ cacheMaxAge: 600000,
77
+ });
78
+ jwksCache.set(jwksUri, jwks);
79
+ }
80
+ return jwks;
81
+ }
82
+ export async function verifyOIDCToken(token, providers) {
83
+ let decodedPayload;
84
+ try {
85
+ const parts = token.split('.');
86
+ if (parts.length !== 3) {
87
+ logger.debug('Invalid JWT format');
88
+ return null;
89
+ }
90
+ decodedPayload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
91
+ }
92
+ catch {
93
+ logger.debug('Failed to decode JWT');
94
+ return null;
95
+ }
96
+ const { iss, sub } = decodedPayload;
97
+ if (!iss || !sub || typeof iss !== 'string' || typeof sub !== 'string') {
98
+ logger.debug('OIDC token missing issuer or subject');
99
+ return null;
100
+ }
101
+ const provider = providers.find((p) => p.issuer === iss);
102
+ if (!provider) {
103
+ logger.debug('No matching OIDC provider for issuer:', { iss });
104
+ return null;
105
+ }
106
+ const jwksUri = provider.jwksUri || `${provider.issuer}/.well-known/jwks.json`;
107
+ try {
108
+ const JWKS = getJWKS(jwksUri);
109
+ const verifyOptions = {
110
+ issuer: provider.issuer,
111
+ };
112
+ if (provider.audience) {
113
+ verifyOptions.audience = provider.audience;
114
+ }
115
+ const { payload } = await jose.jwtVerify(token, JWKS, verifyOptions);
116
+ logger.debug('OIDC token verified successfully', { issuer: iss, subject: sub });
117
+ return {
118
+ userId: payload.sub,
119
+ roles: payload.roles,
120
+ };
121
+ }
122
+ catch (error) {
123
+ if (error instanceof jose.errors.JWTExpired) {
124
+ logger.debug('OIDC token expired');
125
+ }
126
+ else if (error instanceof jose.errors.JWTClaimValidationFailed) {
127
+ logger.debug('OIDC token claim validation failed:', { message: error.message });
128
+ }
129
+ else if (error instanceof jose.errors.JWSSignatureVerificationFailed) {
130
+ logger.debug('OIDC token signature verification failed');
131
+ }
132
+ else {
133
+ logger.error('OIDC token verification failed:', {
134
+ message: error.message,
135
+ name: error.name,
136
+ });
137
+ }
138
+ return null;
139
+ }
140
+ }
141
+ export function createAuthHandler(config) {
142
+ return async function verifyToken(token) {
143
+ if (!token) {
144
+ return {};
145
+ }
146
+ const localPayload = await verifyLocalToken(token);
147
+ if (localPayload) {
148
+ return {
149
+ userId: localPayload.sub,
150
+ tokenVersion: localPayload.ver,
151
+ impersonatorId: localPayload.imp,
152
+ };
153
+ }
154
+ if (config?.oidcProviders?.length) {
155
+ const oidcResult = await verifyOIDCToken(token, config.oidcProviders);
156
+ if (oidcResult) {
157
+ return {
158
+ userId: oidcResult.userId,
159
+ };
160
+ }
161
+ }
162
+ return {
163
+ accessToken: token,
164
+ isApiKey: true,
165
+ };
166
+ };
167
+ }
@@ -0,0 +1,3 @@
1
+ import type { Express } from 'express';
2
+ import type { UnchainedCore } from '@unchainedshop/core';
3
+ export declare function mountPluginRoutes(app: Express, unchainedAPI: UnchainedCore): void;
@@ -0,0 +1,59 @@
1
+ import { pluginRegistry } from '@unchainedshop/core';
2
+ import { createServerAdapter } from '@whatwg-node/server';
3
+ import { createLogger } from '@unchainedshop/logger';
4
+ const logger = createLogger('express');
5
+ export function mountPluginRoutes(app, unchainedAPI) {
6
+ const routes = pluginRegistry.getRoutes();
7
+ if (routes.length > 0) {
8
+ const endpoints = routes.map((r) => `${r.method} ${r.path}`).join(', ');
9
+ logger.info(`Mounting ${routes.length} plugin route(s): ${endpoints}`);
10
+ }
11
+ for (const route of routes) {
12
+ const adapter = createServerAdapter(async (request, serverContext) => {
13
+ const context = {
14
+ ...unchainedAPI,
15
+ ...serverContext.unchainedContext,
16
+ params: serverContext.params || {},
17
+ rawRequest: serverContext.rawRequest,
18
+ };
19
+ try {
20
+ return await route.handler(request, context);
21
+ }
22
+ catch (error) {
23
+ logger.error(`Error in plugin route handler ${route.method} ${route.path}`, {
24
+ error: error instanceof Error ? error.message : String(error),
25
+ });
26
+ return new Response(JSON.stringify({
27
+ error: error instanceof Error ? error.message : 'Internal Server Error',
28
+ }), {
29
+ status: 500,
30
+ headers: { 'Content-Type': 'application/json' },
31
+ });
32
+ }
33
+ });
34
+ const method = route.method.toLowerCase();
35
+ const expressHandler = async (req, res) => {
36
+ try {
37
+ await adapter.handleNodeRequestAndResponse(req, res, {
38
+ unchainedContext: req.unchainedContext,
39
+ params: req.params,
40
+ rawRequest: req,
41
+ });
42
+ }
43
+ catch (error) {
44
+ logger.error(`Error handling request for ${route.method} ${route.path}`, {
45
+ error: error instanceof Error ? error.message : String(error),
46
+ });
47
+ if (!res.headersSent) {
48
+ res.status(500).json({ error: 'Internal Server Error' });
49
+ }
50
+ }
51
+ };
52
+ if (method === 'all') {
53
+ app.use(route.path, expressHandler);
54
+ }
55
+ else {
56
+ app[method](route.path, expressHandler);
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,3 @@
1
+ import type { Express } from 'express';
2
+ import type { UnchainedCore, PluginHttpRoute } from '@unchainedshop/core';
3
+ export declare function mountRoutes(app: Express, unchainedAPI: UnchainedCore, routes: PluginHttpRoute[]): void;
@@ -0,0 +1,57 @@
1
+ import { createServerAdapter } from '@whatwg-node/server';
2
+ import { createLogger } from '@unchainedshop/logger';
3
+ const logger = createLogger('express');
4
+ export function mountRoutes(app, unchainedAPI, routes) {
5
+ if (routes.length === 0)
6
+ return;
7
+ const endpoints = routes.map((r) => `${r.method} ${r.path}`).join(', ');
8
+ logger.info(`Mounting ${routes.length} route(s): ${endpoints}`);
9
+ for (const route of routes) {
10
+ const adapter = createServerAdapter(async (request, serverContext) => {
11
+ const context = {
12
+ ...unchainedAPI,
13
+ ...serverContext.unchainedContext,
14
+ params: serverContext.params || {},
15
+ rawRequest: serverContext.rawRequest,
16
+ };
17
+ try {
18
+ return await route.handler(request, context);
19
+ }
20
+ catch (error) {
21
+ logger.error(`Error in route handler ${route.method} ${route.path}`, {
22
+ error: error instanceof Error ? error.message : String(error),
23
+ });
24
+ return new Response(JSON.stringify({
25
+ error: error instanceof Error ? error.message : 'Internal Server Error',
26
+ }), {
27
+ status: 500,
28
+ headers: { 'Content-Type': 'application/json' },
29
+ });
30
+ }
31
+ });
32
+ const method = route.method.toLowerCase();
33
+ const expressHandler = async (req, res) => {
34
+ try {
35
+ await adapter.handleNodeRequestAndResponse(req, res, {
36
+ unchainedContext: req.unchainedContext,
37
+ params: req.params,
38
+ rawRequest: req,
39
+ });
40
+ }
41
+ catch (error) {
42
+ logger.error(`Error handling request for ${route.method} ${route.path}`, {
43
+ error: error instanceof Error ? error.message : String(error),
44
+ });
45
+ if (!res.headersSent) {
46
+ res.status(500).json({ error: 'Internal Server Error' });
47
+ }
48
+ }
49
+ };
50
+ if (method === 'all') {
51
+ app.use(route.path, expressHandler);
52
+ }
53
+ else {
54
+ app[method](route.path, expressHandler);
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { UnchainedCore } from '@unchainedshop/core';
3
+ export declare function mountPluginRoutes(fastify: FastifyInstance, unchainedAPI: UnchainedCore): void;
@@ -0,0 +1,62 @@
1
+ import { pluginRegistry } from '@unchainedshop/core';
2
+ import { createServerAdapter } from '@whatwg-node/server';
3
+ export function mountPluginRoutes(fastify, unchainedAPI) {
4
+ const routes = pluginRegistry.getRoutes();
5
+ if (routes.length === 0)
6
+ return;
7
+ const endpoints = routes.map((r) => `${r.method} ${r.path}`).join(', ');
8
+ fastify.log.info(`Mounting ${routes.length} plugin route(s): ${endpoints}`);
9
+ fastify.register((scope, opts, registered) => {
10
+ scope.removeAllContentTypeParsers();
11
+ scope.addContentTypeParser('*', function (request, payload, done) {
12
+ done(null);
13
+ });
14
+ for (const route of routes) {
15
+ const adapter = createServerAdapter(async (request, serverContext) => {
16
+ const context = {
17
+ ...unchainedAPI,
18
+ ...serverContext.unchainedContext,
19
+ params: serverContext.params || {},
20
+ rawRequest: serverContext.rawRequest,
21
+ };
22
+ try {
23
+ return await route.handler(request, context);
24
+ }
25
+ catch (error) {
26
+ fastify.log.error(`Error in plugin route handler ${route.method} ${route.path}: ${error instanceof Error ? error.message : String(error)}`);
27
+ return new Response(JSON.stringify({
28
+ error: error instanceof Error ? error.message : 'Internal Server Error',
29
+ }), {
30
+ status: 500,
31
+ headers: { 'Content-Type': 'application/json' },
32
+ });
33
+ }
34
+ });
35
+ let methods;
36
+ if (route.method === 'ALL') {
37
+ methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
38
+ }
39
+ else {
40
+ methods = [route.method];
41
+ }
42
+ scope.route({
43
+ url: route.path,
44
+ method: methods,
45
+ handler: async (req, reply) => {
46
+ const response = await adapter.handleNodeRequestAndResponse(req.raw, reply.raw, {
47
+ unchainedContext: req.unchainedContext,
48
+ params: req.params,
49
+ rawRequest: req.raw,
50
+ });
51
+ response.headers.forEach((value, key) => {
52
+ reply.header(key, value);
53
+ });
54
+ reply.status(response.status);
55
+ reply.send(response.body || undefined);
56
+ return reply;
57
+ },
58
+ });
59
+ }
60
+ registered();
61
+ });
62
+ }
@@ -0,0 +1,3 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { UnchainedCore, PluginHttpRoute } from '@unchainedshop/core';
3
+ export declare function mountRoutes(fastify: FastifyInstance, unchainedAPI: UnchainedCore, routes: PluginHttpRoute[]): void;