@uniforge/core 0.1.0-alpha.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.
Files changed (87) hide show
  1. package/dist/auth/index.d.cts +165 -0
  2. package/dist/auth/index.d.ts +165 -0
  3. package/dist/auth/index.js +443 -0
  4. package/dist/auth/index.js.map +1 -0
  5. package/dist/auth/index.mjs +406 -0
  6. package/dist/auth/index.mjs.map +1 -0
  7. package/dist/billing/index.d.cts +34 -0
  8. package/dist/billing/index.d.ts +34 -0
  9. package/dist/billing/index.js +254 -0
  10. package/dist/billing/index.js.map +1 -0
  11. package/dist/billing/index.mjs +225 -0
  12. package/dist/billing/index.mjs.map +1 -0
  13. package/dist/config/index.d.cts +12 -0
  14. package/dist/config/index.d.ts +12 -0
  15. package/dist/config/index.js +186 -0
  16. package/dist/config/index.js.map +1 -0
  17. package/dist/config/index.mjs +156 -0
  18. package/dist/config/index.mjs.map +1 -0
  19. package/dist/database/index.d.cts +33 -0
  20. package/dist/database/index.d.ts +33 -0
  21. package/dist/database/index.js +127 -0
  22. package/dist/database/index.js.map +1 -0
  23. package/dist/database/index.mjs +95 -0
  24. package/dist/database/index.mjs.map +1 -0
  25. package/dist/graphql/index.d.cts +36 -0
  26. package/dist/graphql/index.d.ts +36 -0
  27. package/dist/graphql/index.js +209 -0
  28. package/dist/graphql/index.js.map +1 -0
  29. package/dist/graphql/index.mjs +179 -0
  30. package/dist/graphql/index.mjs.map +1 -0
  31. package/dist/index.d.cts +16 -0
  32. package/dist/index.d.ts +16 -0
  33. package/dist/index.js +36 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/index.mjs +10 -0
  36. package/dist/index.mjs.map +1 -0
  37. package/dist/multi-store/index.d.cts +11 -0
  38. package/dist/multi-store/index.d.ts +11 -0
  39. package/dist/multi-store/index.js +473 -0
  40. package/dist/multi-store/index.js.map +1 -0
  41. package/dist/multi-store/index.mjs +447 -0
  42. package/dist/multi-store/index.mjs.map +1 -0
  43. package/dist/multi-tenant/index.d.cts +23 -0
  44. package/dist/multi-tenant/index.d.ts +23 -0
  45. package/dist/multi-tenant/index.js +69 -0
  46. package/dist/multi-tenant/index.js.map +1 -0
  47. package/dist/multi-tenant/index.mjs +41 -0
  48. package/dist/multi-tenant/index.mjs.map +1 -0
  49. package/dist/performance/index.d.cts +34 -0
  50. package/dist/performance/index.d.ts +34 -0
  51. package/dist/performance/index.js +319 -0
  52. package/dist/performance/index.js.map +1 -0
  53. package/dist/performance/index.mjs +290 -0
  54. package/dist/performance/index.mjs.map +1 -0
  55. package/dist/platform/index.d.cts +25 -0
  56. package/dist/platform/index.d.ts +25 -0
  57. package/dist/platform/index.js +91 -0
  58. package/dist/platform/index.js.map +1 -0
  59. package/dist/platform/index.mjs +62 -0
  60. package/dist/platform/index.mjs.map +1 -0
  61. package/dist/rbac/index.d.cts +24 -0
  62. package/dist/rbac/index.d.ts +24 -0
  63. package/dist/rbac/index.js +267 -0
  64. package/dist/rbac/index.js.map +1 -0
  65. package/dist/rbac/index.mjs +236 -0
  66. package/dist/rbac/index.mjs.map +1 -0
  67. package/dist/schema-CM7mHj_H.d.cts +53 -0
  68. package/dist/schema-CM7mHj_H.d.ts +53 -0
  69. package/dist/security/index.d.cts +47 -0
  70. package/dist/security/index.d.ts +47 -0
  71. package/dist/security/index.js +505 -0
  72. package/dist/security/index.js.map +1 -0
  73. package/dist/security/index.mjs +474 -0
  74. package/dist/security/index.mjs.map +1 -0
  75. package/dist/session-storage/index.d.cts +70 -0
  76. package/dist/session-storage/index.d.ts +70 -0
  77. package/dist/session-storage/index.js +271 -0
  78. package/dist/session-storage/index.js.map +1 -0
  79. package/dist/session-storage/index.mjs +242 -0
  80. package/dist/session-storage/index.mjs.map +1 -0
  81. package/dist/webhooks/index.d.cts +89 -0
  82. package/dist/webhooks/index.d.ts +89 -0
  83. package/dist/webhooks/index.js +380 -0
  84. package/dist/webhooks/index.js.map +1 -0
  85. package/dist/webhooks/index.mjs +348 -0
  86. package/dist/webhooks/index.mjs.map +1 -0
  87. package/package.json +119 -0
@@ -0,0 +1,406 @@
1
+ // src/auth/events.ts
2
+ import { randomUUID } from "crypto";
3
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
4
+ "accessToken",
5
+ "refreshToken",
6
+ "sessionToken",
7
+ "apiKey",
8
+ "apiSecretKey"
9
+ ]);
10
+ function createAuthEvent(input) {
11
+ if (input.metadata) {
12
+ for (const key of Object.keys(input.metadata)) {
13
+ if (SENSITIVE_KEYS.has(key)) {
14
+ throw new Error(
15
+ `Metadata contains sensitive key "${key}". Auth event metadata must never contain tokens or credentials.`
16
+ );
17
+ }
18
+ }
19
+ }
20
+ return {
21
+ id: randomUUID(),
22
+ type: input.type,
23
+ shopDomain: input.shopDomain,
24
+ sessionId: input.sessionId ?? null,
25
+ userId: input.userId ?? null,
26
+ outcome: input.outcome,
27
+ metadata: input.metadata ?? {},
28
+ timestamp: /* @__PURE__ */ new Date()
29
+ };
30
+ }
31
+
32
+ // src/auth/config.ts
33
+ function createAuthConfig(input) {
34
+ if (!input.apiKey) {
35
+ throw new Error("apiKey is required and must not be empty.");
36
+ }
37
+ if (!input.apiSecretKey) {
38
+ throw new Error("apiSecretKey is required and must not be empty.");
39
+ }
40
+ if (!input.scopes || input.scopes.length === 0) {
41
+ throw new Error("scopes must contain at least one scope.");
42
+ }
43
+ if (!input.hostName) {
44
+ throw new Error("hostName is required and must not be empty.");
45
+ }
46
+ if (!input.encryption.keys || input.encryption.keys.length === 0) {
47
+ throw new Error("Encryption config must contain at least one key.");
48
+ }
49
+ for (const encKey of input.encryption.keys) {
50
+ const keyValue = encKey.key;
51
+ if (Buffer.isBuffer(keyValue)) {
52
+ if (keyValue.length !== 32) {
53
+ throw new Error(
54
+ `Encryption key "${encKey.id}" must be 32 bytes, got ${keyValue.length}.`
55
+ );
56
+ }
57
+ } else if (typeof keyValue === "string") {
58
+ if (keyValue.length !== 64 || !/^[0-9a-fA-F]+$/.test(keyValue)) {
59
+ throw new Error(
60
+ `Encryption key "${encKey.id}" must be 32 bytes (64-char hex string).`
61
+ );
62
+ }
63
+ }
64
+ }
65
+ const config = {
66
+ apiKey: input.apiKey,
67
+ apiSecretKey: input.apiSecretKey,
68
+ scopes: input.scopes,
69
+ hostName: input.hostName,
70
+ apiVersion: input.apiVersion,
71
+ isEmbeddedApp: input.isEmbeddedApp ?? true,
72
+ sessionStorage: input.sessionStorage,
73
+ encryption: input.encryption,
74
+ session: {
75
+ expirationTimeoutSeconds: input.session?.expirationTimeoutSeconds ?? 86400,
76
+ useExpiringOfflineTokens: input.session?.useExpiringOfflineTokens ?? false
77
+ },
78
+ tokenRefresh: {
79
+ maxRetries: input.tokenRefresh?.maxRetries ?? 3,
80
+ refreshBufferSeconds: input.tokenRefresh?.refreshBufferSeconds ?? 300
81
+ }
82
+ };
83
+ if (input.eventHandler) {
84
+ config.eventHandler = input.eventHandler;
85
+ }
86
+ return config;
87
+ }
88
+
89
+ // src/auth/encryption.ts
90
+ import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
91
+ var ALGORITHM = "aes-256-gcm";
92
+ var IV_LENGTH = 12;
93
+ var AUTH_TAG_LENGTH = 16;
94
+ var TokenEncryptionServiceImpl = class {
95
+ keys;
96
+ activeKeyId;
97
+ constructor(config) {
98
+ if (config.keys.length === 0) {
99
+ throw new Error("EncryptionConfig must contain at least one key.");
100
+ }
101
+ this.keys = /* @__PURE__ */ new Map();
102
+ for (const key of config.keys) {
103
+ this.keys.set(key.id, this.toBuffer(key));
104
+ }
105
+ this.activeKeyId = config.keys[0].id;
106
+ }
107
+ /** Encrypt plaintext using the active (first) key. Generates a unique IV per call. */
108
+ encrypt(plaintext) {
109
+ const key = this.keys.get(this.activeKeyId);
110
+ const iv = randomBytes(IV_LENGTH);
111
+ const cipher = createCipheriv(ALGORITHM, key, iv, {
112
+ authTagLength: AUTH_TAG_LENGTH
113
+ });
114
+ const encrypted = Buffer.concat([
115
+ cipher.update(plaintext, "utf-8"),
116
+ cipher.final()
117
+ ]);
118
+ const authTag = cipher.getAuthTag();
119
+ return {
120
+ ciphertext: encrypted.toString("base64"),
121
+ iv: iv.toString("base64"),
122
+ authTag: authTag.toString("base64"),
123
+ keyId: this.activeKeyId
124
+ };
125
+ }
126
+ /** Decrypt an encrypted payload. Looks up the key by keyId from the registry. */
127
+ decrypt(payload) {
128
+ const key = this.keys.get(payload.keyId);
129
+ if (!key) {
130
+ throw new Error(
131
+ `Encryption key "${payload.keyId}" not found in key registry. Available keys: ${[...this.keys.keys()].join(", ")}`
132
+ );
133
+ }
134
+ const decipher = createDecipheriv(
135
+ ALGORITHM,
136
+ key,
137
+ Buffer.from(payload.iv, "base64"),
138
+ { authTagLength: AUTH_TAG_LENGTH }
139
+ );
140
+ decipher.setAuthTag(Buffer.from(payload.authTag, "base64"));
141
+ const decrypted = Buffer.concat([
142
+ decipher.update(Buffer.from(payload.ciphertext, "base64")),
143
+ decipher.final()
144
+ ]);
145
+ return decrypted.toString("utf-8");
146
+ }
147
+ /** Check if a payload was encrypted with the currently active key. */
148
+ isEncryptedWithActiveKey(payload) {
149
+ return payload.keyId === this.activeKeyId;
150
+ }
151
+ toBuffer(key) {
152
+ if (Buffer.isBuffer(key.key)) {
153
+ return key.key;
154
+ }
155
+ return Buffer.from(key.key, "hex");
156
+ }
157
+ };
158
+
159
+ // src/auth/encrypted-session-storage.ts
160
+ var EncryptedSessionStorage = class {
161
+ constructor(inner, encryption) {
162
+ this.inner = inner;
163
+ this.encryption = encryption;
164
+ }
165
+ /** Store a session with encrypted tokens. */
166
+ async storeSession(session) {
167
+ const encrypted = this.encryptTokens(session);
168
+ return this.inner.storeSession(encrypted);
169
+ }
170
+ /** Load a session and decrypt its tokens. */
171
+ async loadSession(id) {
172
+ const session = await this.inner.loadSession(id);
173
+ if (!session) return void 0;
174
+ return this.decryptTokens(session);
175
+ }
176
+ /** Delete a session by ID (pass-through). */
177
+ async deleteSession(id) {
178
+ return this.inner.deleteSession(id);
179
+ }
180
+ /** Delete multiple sessions by ID (pass-through). */
181
+ async deleteSessions(ids) {
182
+ return this.inner.deleteSessions(ids);
183
+ }
184
+ /** Find sessions by shop and decrypt their tokens. */
185
+ async findSessionsByShop(shop) {
186
+ const sessions = await this.inner.findSessionsByShop(shop);
187
+ return sessions.map((session) => this.decryptTokens(session));
188
+ }
189
+ encryptTokens(session) {
190
+ const result = { ...session };
191
+ if (result.accessToken) {
192
+ const payload = this.encryption.encrypt(result.accessToken);
193
+ result.accessToken = JSON.stringify(payload);
194
+ }
195
+ if (result.refreshToken) {
196
+ const payload = this.encryption.encrypt(result.refreshToken);
197
+ result.refreshToken = JSON.stringify(payload);
198
+ }
199
+ return result;
200
+ }
201
+ decryptTokens(session) {
202
+ const result = { ...session };
203
+ if (result.accessToken) {
204
+ const payload = JSON.parse(result.accessToken);
205
+ result.accessToken = this.encryption.decrypt(payload);
206
+ }
207
+ if (result.refreshToken) {
208
+ const payload = JSON.parse(result.refreshToken);
209
+ result.refreshToken = this.encryption.decrypt(payload);
210
+ }
211
+ return result;
212
+ }
213
+ };
214
+
215
+ // src/auth/session-utils.ts
216
+ var DEFAULT_EXPIRATION_TIMEOUT_SECONDS = 86400;
217
+ var DEFAULT_REFRESH_BUFFER_SECONDS = 300;
218
+ function isSessionExpired(session, _config) {
219
+ if (!session.expires) {
220
+ return false;
221
+ }
222
+ return session.expires.getTime() <= Date.now();
223
+ }
224
+ function shouldRefreshToken(session, config) {
225
+ if (!session.expires) {
226
+ return false;
227
+ }
228
+ const bufferSeconds = config?.refreshBufferSeconds ?? DEFAULT_REFRESH_BUFFER_SECONDS;
229
+ const bufferMs = bufferSeconds * 1e3;
230
+ const threshold = Date.now() + bufferMs;
231
+ return session.expires.getTime() <= threshold;
232
+ }
233
+ function getSessionExpirationDate(config) {
234
+ const timeoutSeconds = config?.expirationTimeoutSeconds ?? DEFAULT_EXPIRATION_TIMEOUT_SECONDS;
235
+ return new Date(Date.now() + timeoutSeconds * 1e3);
236
+ }
237
+
238
+ // src/auth/route-matcher.ts
239
+ var ALWAYS_PUBLIC_PATTERNS = ["/auth/callback", "/auth/*/callback"];
240
+ function isPublicRoute(url, config) {
241
+ const path = normalizePath(url);
242
+ if (matchesAny(path, ALWAYS_PUBLIC_PATTERNS)) {
243
+ return true;
244
+ }
245
+ const publicRoutes = config.publicRoutes ?? [];
246
+ if (matchesAny(path, publicRoutes)) {
247
+ return true;
248
+ }
249
+ return false;
250
+ }
251
+ function normalizePath(url) {
252
+ const questionIdx = url.indexOf("?");
253
+ let path = questionIdx >= 0 ? url.slice(0, questionIdx) : url;
254
+ if (path.length > 1 && path.endsWith("/")) {
255
+ path = path.slice(0, -1);
256
+ }
257
+ return path;
258
+ }
259
+ function matchesAny(path, patterns) {
260
+ return patterns.some((pattern) => matchGlob(path, pattern));
261
+ }
262
+ function matchGlob(path, pattern) {
263
+ if (pattern === path) {
264
+ return true;
265
+ }
266
+ if (pattern.endsWith("/**")) {
267
+ const prefix = pattern.slice(0, -3);
268
+ return path.startsWith(prefix + "/");
269
+ }
270
+ if (pattern.includes("*") && !pattern.includes("**")) {
271
+ const regex = patternToRegex(pattern);
272
+ return regex.test(path);
273
+ }
274
+ return false;
275
+ }
276
+ function patternToRegex(pattern) {
277
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]+");
278
+ return new RegExp(`^${escaped}$`);
279
+ }
280
+
281
+ // src/auth/middleware.ts
282
+ import { isValidShopDomain } from "@uniforge/platform-core/auth";
283
+ function createAuthMiddleware(config, routeConfig) {
284
+ const effectiveRouteConfig = routeConfig ?? {};
285
+ return {
286
+ async authenticate(request) {
287
+ if (isPublicRoute(request.url, effectiveRouteConfig)) {
288
+ return {
289
+ authenticated: true,
290
+ session: {},
291
+ shopContext: {
292
+ shopDomain: "",
293
+ accessToken: "",
294
+ scopes: [],
295
+ session: {}
296
+ }
297
+ };
298
+ }
299
+ const shop = extractShopFromRequest(request);
300
+ if (!shop) {
301
+ return {
302
+ authenticated: false,
303
+ error: {
304
+ code: "INVALID_SHOP",
305
+ message: "Could not determine shop domain. Provide shop in query parameter or x-shopify-shop-domain header.",
306
+ statusCode: 400
307
+ }
308
+ };
309
+ }
310
+ const sessionId = `offline_${shop}`;
311
+ const session = await config.sessionStorage.loadSession(sessionId);
312
+ if (!session) {
313
+ if (config.isEmbeddedApp) {
314
+ await emitUnauthorizedEvent(config, shop, "SESSION_NOT_FOUND");
315
+ return {
316
+ authenticated: false,
317
+ error: {
318
+ code: "SESSION_NOT_FOUND",
319
+ message: "No session found. Ensure the session token is provided in the Authorization header.",
320
+ statusCode: 401
321
+ }
322
+ };
323
+ }
324
+ const redirectUrl = buildOAuthRedirectUrl(config, shop);
325
+ await emitUnauthorizedEvent(config, shop, "SESSION_NOT_FOUND");
326
+ return {
327
+ authenticated: false,
328
+ redirectUrl,
329
+ statusCode: 302
330
+ };
331
+ }
332
+ if (session.expires && session.expires.getTime() <= Date.now()) {
333
+ await emitUnauthorizedEvent(config, shop, "SESSION_EXPIRED");
334
+ return {
335
+ authenticated: false,
336
+ error: {
337
+ code: "SESSION_EXPIRED",
338
+ message: "Session has expired. Re-authentication is required.",
339
+ statusCode: 401
340
+ }
341
+ };
342
+ }
343
+ const shopContext = {
344
+ shopDomain: session.shop,
345
+ accessToken: session.accessToken ?? "",
346
+ scopes: session.scope ? session.scope.split(",") : [],
347
+ session
348
+ };
349
+ if (config.eventHandler) {
350
+ const event = createAuthEvent({
351
+ type: "middleware_authorized",
352
+ shopDomain: shop,
353
+ outcome: "success",
354
+ sessionId: session.id
355
+ });
356
+ await Promise.resolve(config.eventHandler.onAuthEvent(event));
357
+ }
358
+ return {
359
+ authenticated: true,
360
+ session,
361
+ shopContext
362
+ };
363
+ }
364
+ };
365
+ }
366
+ function extractShopFromRequest(request) {
367
+ const shopFromQuery = request.query["shop"];
368
+ if (shopFromQuery && isValidShopDomain(shopFromQuery)) {
369
+ return shopFromQuery;
370
+ }
371
+ const shopFromHeader = request.headers["x-shopify-shop-domain"];
372
+ if (shopFromHeader && typeof shopFromHeader === "string" && isValidShopDomain(shopFromHeader)) {
373
+ return shopFromHeader;
374
+ }
375
+ return null;
376
+ }
377
+ function buildOAuthRedirectUrl(config, shop) {
378
+ const scopes = config.scopes.join(",");
379
+ const redirectUri = `https://${config.hostName}/auth/callback`;
380
+ return `https://${shop}/admin/oauth/authorize?client_id=${config.apiKey}&scope=${scopes}&redirect_uri=${encodeURIComponent(redirectUri)}`;
381
+ }
382
+ async function emitUnauthorizedEvent(config, shop, reason) {
383
+ if (config.eventHandler) {
384
+ const event = createAuthEvent({
385
+ type: "middleware_unauthorized",
386
+ shopDomain: shop,
387
+ outcome: "failure",
388
+ metadata: { reason }
389
+ });
390
+ await Promise.resolve(config.eventHandler.onAuthEvent(event));
391
+ }
392
+ }
393
+ export {
394
+ EncryptedSessionStorage,
395
+ TokenEncryptionServiceImpl,
396
+ createAuthConfig,
397
+ createAuthEvent,
398
+ createAuthMiddleware,
399
+ getSessionExpirationDate,
400
+ isPublicRoute,
401
+ isSessionExpired,
402
+ matchGlob,
403
+ normalizePath,
404
+ shouldRefreshToken
405
+ };
406
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/events.ts","../../src/auth/config.ts","../../src/auth/encryption.ts","../../src/auth/encrypted-session-storage.ts","../../src/auth/session-utils.ts","../../src/auth/route-matcher.ts","../../src/auth/middleware.ts"],"sourcesContent":["/**\n * Auth event factory.\n *\n * Creates structured authentication events with automatic UUID generation,\n * timestamp setting, and sensitive metadata key rejection.\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { AuthEvent, AuthEventType } from '@uniforge/platform-core/auth';\n\nconst SENSITIVE_KEYS = new Set([\n 'accessToken',\n 'refreshToken',\n 'sessionToken',\n 'apiKey',\n 'apiSecretKey',\n]);\n\n/** Input parameters for creating an auth event. */\nexport interface CreateAuthEventInput {\n type: AuthEventType;\n shopDomain: string;\n outcome: 'success' | 'failure';\n sessionId?: string | null;\n userId?: number | null;\n metadata?: Record<string, string>;\n}\n\n/** Create a structured auth event with auto-generated ID and timestamp. Rejects sensitive metadata keys. */\nexport function createAuthEvent(input: CreateAuthEventInput): AuthEvent {\n if (input.metadata) {\n for (const key of Object.keys(input.metadata)) {\n if (SENSITIVE_KEYS.has(key)) {\n throw new Error(\n `Metadata contains sensitive key \"${key}\". Auth event metadata must never contain tokens or credentials.`,\n );\n }\n }\n }\n\n return {\n id: randomUUID(),\n type: input.type,\n shopDomain: input.shopDomain,\n sessionId: input.sessionId ?? null,\n userId: input.userId ?? null,\n outcome: input.outcome,\n metadata: input.metadata ?? {},\n timestamp: new Date(),\n };\n}\n","/**\n * Auth config factory with validation and defaults.\n */\n\nimport type { AuthConfig } from '@uniforge/platform-core/auth';\n\n/** Input parameters for creating a validated auth configuration. */\nexport interface CreateAuthConfigInput {\n apiKey: string;\n apiSecretKey: string;\n scopes: string[];\n hostName: string;\n apiVersion: string;\n isEmbeddedApp?: boolean;\n sessionStorage: AuthConfig['sessionStorage'];\n encryption: AuthConfig['encryption'];\n eventHandler?: AuthConfig['eventHandler'];\n session?: AuthConfig['session'];\n tokenRefresh?: AuthConfig['tokenRefresh'];\n}\n\n/** Create a validated AuthConfig with sensible defaults. Throws on invalid input. */\nexport function createAuthConfig(input: CreateAuthConfigInput): AuthConfig {\n if (!input.apiKey) {\n throw new Error('apiKey is required and must not be empty.');\n }\n if (!input.apiSecretKey) {\n throw new Error('apiSecretKey is required and must not be empty.');\n }\n if (!input.scopes || input.scopes.length === 0) {\n throw new Error('scopes must contain at least one scope.');\n }\n if (!input.hostName) {\n throw new Error('hostName is required and must not be empty.');\n }\n\n if (!input.encryption.keys || input.encryption.keys.length === 0) {\n throw new Error('Encryption config must contain at least one key.');\n }\n\n for (const encKey of input.encryption.keys) {\n const keyValue = encKey.key;\n if (Buffer.isBuffer(keyValue)) {\n if (keyValue.length !== 32) {\n throw new Error(\n `Encryption key \"${encKey.id}\" must be 32 bytes, got ${keyValue.length}.`,\n );\n }\n } else if (typeof keyValue === 'string') {\n if (keyValue.length !== 64 || !/^[0-9a-fA-F]+$/.test(keyValue)) {\n throw new Error(\n `Encryption key \"${encKey.id}\" must be 32 bytes (64-char hex string).`,\n );\n }\n }\n }\n\n const config: AuthConfig = {\n apiKey: input.apiKey,\n apiSecretKey: input.apiSecretKey,\n scopes: input.scopes,\n hostName: input.hostName,\n apiVersion: input.apiVersion,\n isEmbeddedApp: input.isEmbeddedApp ?? true,\n sessionStorage: input.sessionStorage,\n encryption: input.encryption,\n session: {\n expirationTimeoutSeconds:\n input.session?.expirationTimeoutSeconds ?? 86400,\n useExpiringOfflineTokens:\n input.session?.useExpiringOfflineTokens ?? false,\n },\n tokenRefresh: {\n maxRetries: input.tokenRefresh?.maxRetries ?? 3,\n refreshBufferSeconds: input.tokenRefresh?.refreshBufferSeconds ?? 300,\n },\n };\n\n if (input.eventHandler) {\n config.eventHandler = input.eventHandler;\n }\n\n return config;\n}\n","/**\n * AES-256-GCM token encryption with key rotation.\n *\n * Implements TokenEncryptionService using Node.js built-in crypto.\n * Each encrypt call generates a unique 12-byte random IV.\n * Key rotation is supported via a key registry — decrypt looks up\n * the key by keyId, while encrypt always uses the active (first) key.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport type {\n EncryptionConfig,\n EncryptionKey,\n EncryptedPayload,\n TokenEncryptionService,\n} from '@uniforge/platform-core/auth';\n\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_LENGTH = 12;\nconst AUTH_TAG_LENGTH = 16;\n\nexport class TokenEncryptionServiceImpl implements TokenEncryptionService {\n private readonly keys: Map<string, Buffer>;\n private readonly activeKeyId: string;\n\n constructor(config: EncryptionConfig) {\n if (config.keys.length === 0) {\n throw new Error('EncryptionConfig must contain at least one key.');\n }\n this.keys = new Map();\n for (const key of config.keys) {\n this.keys.set(key.id, this.toBuffer(key));\n }\n this.activeKeyId = config.keys[0]!.id;\n }\n\n /** Encrypt plaintext using the active (first) key. Generates a unique IV per call. */\n encrypt(plaintext: string): EncryptedPayload {\n const key = this.keys.get(this.activeKeyId)!;\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv(ALGORITHM, key, iv, {\n authTagLength: AUTH_TAG_LENGTH,\n });\n\n const encrypted = Buffer.concat([\n cipher.update(plaintext, 'utf-8'),\n cipher.final(),\n ]);\n\n const authTag = cipher.getAuthTag();\n\n return {\n ciphertext: encrypted.toString('base64'),\n iv: iv.toString('base64'),\n authTag: authTag.toString('base64'),\n keyId: this.activeKeyId,\n };\n }\n\n /** Decrypt an encrypted payload. Looks up the key by keyId from the registry. */\n decrypt(payload: EncryptedPayload): string {\n const key = this.keys.get(payload.keyId);\n if (!key) {\n throw new Error(\n `Encryption key \"${payload.keyId}\" not found in key registry. Available keys: ${[...this.keys.keys()].join(', ')}`,\n );\n }\n\n const decipher = createDecipheriv(\n ALGORITHM,\n key,\n Buffer.from(payload.iv, 'base64'),\n { authTagLength: AUTH_TAG_LENGTH },\n );\n decipher.setAuthTag(Buffer.from(payload.authTag, 'base64'));\n\n const decrypted = Buffer.concat([\n decipher.update(Buffer.from(payload.ciphertext, 'base64')),\n decipher.final(),\n ]);\n\n return decrypted.toString('utf-8');\n }\n\n /** Check if a payload was encrypted with the currently active key. */\n isEncryptedWithActiveKey(payload: EncryptedPayload): boolean {\n return payload.keyId === this.activeKeyId;\n }\n\n private toBuffer(key: EncryptionKey): Buffer {\n if (Buffer.isBuffer(key.key)) {\n return key.key;\n }\n return Buffer.from(key.key, 'hex');\n }\n}\n","/**\n * Encrypting session storage decorator.\n *\n * Wraps any SessionStorage implementation, transparently encrypting\n * accessToken and refreshToken on store, and decrypting on load/find.\n * Delete operations pass through unchanged.\n */\n\nimport type {\n Session,\n SessionStorage,\n TokenEncryptionService,\n} from '@uniforge/platform-core/auth';\n\nexport class EncryptedSessionStorage implements SessionStorage {\n constructor(\n private readonly inner: SessionStorage,\n private readonly encryption: TokenEncryptionService,\n ) {}\n\n /** Store a session with encrypted tokens. */\n async storeSession(session: Session): Promise<boolean> {\n const encrypted = this.encryptTokens(session);\n return this.inner.storeSession(encrypted);\n }\n\n /** Load a session and decrypt its tokens. */\n async loadSession(id: string): Promise<Session | undefined> {\n const session = await this.inner.loadSession(id);\n if (!session) return undefined;\n return this.decryptTokens(session);\n }\n\n /** Delete a session by ID (pass-through). */\n async deleteSession(id: string): Promise<boolean> {\n return this.inner.deleteSession(id);\n }\n\n /** Delete multiple sessions by ID (pass-through). */\n async deleteSessions(ids: string[]): Promise<boolean> {\n return this.inner.deleteSessions(ids);\n }\n\n /** Find sessions by shop and decrypt their tokens. */\n async findSessionsByShop(shop: string): Promise<Session[]> {\n const sessions = await this.inner.findSessionsByShop(shop);\n return sessions.map((session) => this.decryptTokens(session));\n }\n\n private encryptTokens(session: Session): Session {\n const result = { ...session };\n\n if (result.accessToken) {\n const payload = this.encryption.encrypt(result.accessToken);\n result.accessToken = JSON.stringify(payload);\n }\n\n if (result.refreshToken) {\n const payload = this.encryption.encrypt(result.refreshToken);\n result.refreshToken = JSON.stringify(payload);\n }\n\n return result;\n }\n\n private decryptTokens(session: Session): Session {\n const result = { ...session };\n\n if (result.accessToken) {\n const payload = JSON.parse(result.accessToken);\n result.accessToken = this.encryption.decrypt(payload);\n }\n\n if (result.refreshToken) {\n const payload = JSON.parse(result.refreshToken);\n result.refreshToken = this.encryption.decrypt(payload);\n }\n\n return result;\n }\n}\n","/**\n * Session expiration and token refresh utilities.\n *\n * Platform-agnostic helpers for determining session validity\n * and whether tokens should be proactively refreshed.\n */\n\nimport type {\n Session,\n SessionConfig,\n TokenRefreshConfig,\n} from '@uniforge/platform-core/auth';\n\nconst DEFAULT_EXPIRATION_TIMEOUT_SECONDS = 86400; // 24 hours\nconst DEFAULT_REFRESH_BUFFER_SECONDS = 300; // 5 minutes\n\n/**\n * Check if a session has expired.\n *\n * Returns true if the session's `expires` date is in the past (or now).\n * Non-expiring sessions (null expires) are never considered expired.\n */\nexport function isSessionExpired(\n session: Session,\n _config?: SessionConfig,\n): boolean {\n if (!session.expires) {\n return false; // Non-expiring offline session\n }\n return session.expires.getTime() <= Date.now();\n}\n\n/**\n * Check if a session's token should be proactively refreshed.\n *\n * Returns true if the session will expire within the refresh buffer window.\n * Non-expiring sessions (null expires) never need refresh.\n */\nexport function shouldRefreshToken(\n session: Session,\n config?: TokenRefreshConfig,\n): boolean {\n if (!session.expires) {\n return false; // Non-expiring session\n }\n\n const bufferSeconds =\n config?.refreshBufferSeconds ?? DEFAULT_REFRESH_BUFFER_SECONDS;\n const bufferMs = bufferSeconds * 1000;\n const threshold = Date.now() + bufferMs;\n\n return session.expires.getTime() <= threshold;\n}\n\n/**\n * Calculate a session expiration date based on config.\n *\n * Returns a Date that is `expirationTimeoutSeconds` from now.\n */\nexport function getSessionExpirationDate(\n config?: SessionConfig,\n): Date {\n const timeoutSeconds =\n config?.expirationTimeoutSeconds ?? DEFAULT_EXPIRATION_TIMEOUT_SECONDS;\n return new Date(Date.now() + timeoutSeconds * 1000);\n}\n","/**\n * Route matcher utility for auth middleware.\n *\n * Determines whether a given URL path is public (no auth required)\n * or protected (auth required) based on glob pattern configuration.\n */\n\nimport type { RouteProtectionConfig } from '@uniforge/platform-core/auth';\n\n/** Auth callback paths that are always treated as public. */\nconst ALWAYS_PUBLIC_PATTERNS = ['/auth/callback', '/auth/*/callback'];\n\n/**\n * Check if a URL path is a public route that does not require authentication.\n *\n * Public routes take precedence over protected routes.\n * Auth callback paths (/auth/callback, /auth/STAR/callback) are always public.\n */\nexport function isPublicRoute(\n url: string,\n config: RouteProtectionConfig,\n): boolean {\n const path = normalizePath(url);\n\n // Auth callback routes are always public\n if (matchesAny(path, ALWAYS_PUBLIC_PATTERNS)) {\n return true;\n }\n\n const publicRoutes = config.publicRoutes ?? [];\n\n // Check explicit public routes (takes precedence)\n if (matchesAny(path, publicRoutes)) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Normalize a URL path by stripping query parameters and trailing slashes.\n */\nexport function normalizePath(url: string): string {\n // Strip query string\n const questionIdx = url.indexOf('?');\n let path = questionIdx >= 0 ? url.slice(0, questionIdx) : url;\n\n // Strip trailing slash (but keep root /)\n if (path.length > 1 && path.endsWith('/')) {\n path = path.slice(0, -1);\n }\n\n return path;\n}\n\n/**\n * Check if a path matches any of the given patterns.\n */\nfunction matchesAny(path: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchGlob(path, pattern));\n}\n\n/**\n * Simple glob pattern matcher supporting:\n * - Exact matches: `/health` matches `/health`\n * - Single segment wildcard `*`: `/admin/*` matches `/admin/foo` but not `/admin/foo/bar`\n * - Multi-segment wildcard `**`: `/api/**` matches `/api/foo`, `/api/foo/bar`, etc.\n */\nexport function matchGlob(path: string, pattern: string): boolean {\n // Exact match\n if (pattern === path) {\n return true;\n }\n\n // Handle ** (matches any number of path segments)\n if (pattern.endsWith('/**')) {\n const prefix = pattern.slice(0, -3); // Remove /**\n return path.startsWith(prefix + '/');\n }\n\n // Handle * (matches exactly one path segment)\n if (pattern.includes('*') && !pattern.includes('**')) {\n const regex = patternToRegex(pattern);\n return regex.test(path);\n }\n\n return false;\n}\n\n/**\n * Convert a glob pattern with single `*` wildcards to a regex.\n * `*` matches one path segment (no slashes).\n */\nfunction patternToRegex(pattern: string): RegExp {\n const escaped = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*/g, '[^/]+');\n return new RegExp(`^${escaped}$`);\n}\n","/**\n * Authentication middleware factory.\n *\n * Creates a framework-agnostic auth middleware that:\n * - Checks if the route is public (skip auth)\n * - Extracts shop domain from the request\n * - Loads and validates the session from storage\n * - Constructs ShopContext on success\n * - Returns redirect or error on failure\n * - Emits middleware auth events\n */\n\nimport type {\n AuthConfig,\n AuthRequest,\n AuthResult,\n AuthMiddleware,\n RouteProtectionConfig,\n Session,\n} from '@uniforge/platform-core/auth';\nimport { isValidShopDomain } from '@uniforge/platform-core/auth';\nimport { createAuthEvent } from './events';\nimport { isPublicRoute } from './route-matcher';\n\n/**\n * Create an auth middleware instance.\n *\n * @param config - The auth configuration\n * @param routeConfig - Optional route protection configuration. If not provided, all routes are protected.\n */\nexport function createAuthMiddleware(\n config: AuthConfig,\n routeConfig?: RouteProtectionConfig,\n): AuthMiddleware {\n const effectiveRouteConfig: RouteProtectionConfig = routeConfig ?? {};\n\n return {\n async authenticate(request: AuthRequest): Promise<AuthResult> {\n // Check if the route is public\n if (isPublicRoute(request.url, effectiveRouteConfig)) {\n return {\n authenticated: true,\n session: {} as Session,\n shopContext: {\n shopDomain: '',\n accessToken: '',\n scopes: [],\n session: {} as Session,\n },\n };\n }\n\n // Extract shop domain\n const shop = extractShopFromRequest(request);\n if (!shop) {\n return {\n authenticated: false,\n error: {\n code: 'INVALID_SHOP',\n message:\n 'Could not determine shop domain. Provide shop in query parameter or x-shopify-shop-domain header.',\n statusCode: 400,\n },\n };\n }\n\n // Try to load existing session\n const sessionId = `offline_${shop}`;\n const session = await config.sessionStorage.loadSession(sessionId);\n\n if (!session) {\n // No session found\n if (config.isEmbeddedApp) {\n await emitUnauthorizedEvent(config, shop, 'SESSION_NOT_FOUND');\n return {\n authenticated: false,\n error: {\n code: 'SESSION_NOT_FOUND',\n message:\n 'No session found. Ensure the session token is provided in the Authorization header.',\n statusCode: 401,\n },\n };\n }\n\n // Non-embedded: redirect to OAuth\n const redirectUrl = buildOAuthRedirectUrl(config, shop);\n await emitUnauthorizedEvent(config, shop, 'SESSION_NOT_FOUND');\n return {\n authenticated: false,\n redirectUrl,\n statusCode: 302,\n };\n }\n\n // Check session expiration\n if (session.expires && session.expires.getTime() <= Date.now()) {\n await emitUnauthorizedEvent(config, shop, 'SESSION_EXPIRED');\n return {\n authenticated: false,\n error: {\n code: 'SESSION_EXPIRED',\n message:\n 'Session has expired. Re-authentication is required.',\n statusCode: 401,\n },\n };\n }\n\n // Session is valid — build shop context\n const shopContext = {\n shopDomain: session.shop,\n accessToken: session.accessToken ?? '',\n scopes: session.scope ? session.scope.split(',') : [],\n session,\n };\n\n // Emit authorized event\n if (config.eventHandler) {\n const event = createAuthEvent({\n type: 'middleware_authorized',\n shopDomain: shop,\n outcome: 'success',\n sessionId: session.id,\n });\n await Promise.resolve(config.eventHandler.onAuthEvent(event));\n }\n\n return {\n authenticated: true,\n session,\n shopContext,\n };\n },\n };\n}\n\n/**\n * Extract shop domain from request query params or headers.\n */\nfunction extractShopFromRequest(request: AuthRequest): string | null {\n const shopFromQuery = request.query['shop'];\n if (shopFromQuery && isValidShopDomain(shopFromQuery)) {\n return shopFromQuery;\n }\n\n const shopFromHeader = request.headers['x-shopify-shop-domain'];\n if (\n shopFromHeader &&\n typeof shopFromHeader === 'string' &&\n isValidShopDomain(shopFromHeader)\n ) {\n return shopFromHeader;\n }\n\n return null;\n}\n\n/**\n * Build an OAuth redirect URL for non-embedded apps.\n */\nfunction buildOAuthRedirectUrl(config: AuthConfig, shop: string): string {\n const scopes = config.scopes.join(',');\n const redirectUri = `https://${config.hostName}/auth/callback`;\n return `https://${shop}/admin/oauth/authorize?client_id=${config.apiKey}&scope=${scopes}&redirect_uri=${encodeURIComponent(redirectUri)}`;\n}\n\n/**\n * Emit a middleware_unauthorized event.\n */\nasync function emitUnauthorizedEvent(\n config: AuthConfig,\n shop: string,\n reason: string,\n): Promise<void> {\n if (config.eventHandler) {\n const event = createAuthEvent({\n type: 'middleware_unauthorized',\n shopDomain: shop,\n outcome: 'failure',\n metadata: { reason },\n });\n await Promise.resolve(config.eventHandler.onAuthEvent(event));\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,34 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ import { BillingService, PlanRegistry, FeatureGateConfig, FeatureGateMiddleware } from '@uniforge/platform-core/billing';
3
+
4
+ /**
5
+ * Core billing service implementation with Prisma persistence.
6
+ *
7
+ * Provides subscription management and usage record tracking
8
+ * using the PrismaClient for database operations.
9
+ */
10
+
11
+ /** Create a BillingService backed by Prisma. */
12
+ declare function createBillingService(prisma: PrismaClient): BillingService;
13
+
14
+ /**
15
+ * Billing plan registry.
16
+ *
17
+ * In-memory registry for managing available billing plans,
18
+ * following the same pattern as the webhook handler registry.
19
+ */
20
+
21
+ /** Create a PlanRegistry that stores plans in memory. */
22
+ declare function createPlanRegistry(): PlanRegistry;
23
+
24
+ /**
25
+ * Feature gate middleware for plan-based access control.
26
+ *
27
+ * Checks whether a shop's active billing plan includes a given feature.
28
+ * Falls back to free features when no active plan exists.
29
+ */
30
+
31
+ /** Create a FeatureGateMiddleware from the given configuration. */
32
+ declare function createFeatureGateMiddleware(config: FeatureGateConfig): FeatureGateMiddleware;
33
+
34
+ export { createBillingService, createFeatureGateMiddleware, createPlanRegistry };
@@ -0,0 +1,34 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ import { BillingService, PlanRegistry, FeatureGateConfig, FeatureGateMiddleware } from '@uniforge/platform-core/billing';
3
+
4
+ /**
5
+ * Core billing service implementation with Prisma persistence.
6
+ *
7
+ * Provides subscription management and usage record tracking
8
+ * using the PrismaClient for database operations.
9
+ */
10
+
11
+ /** Create a BillingService backed by Prisma. */
12
+ declare function createBillingService(prisma: PrismaClient): BillingService;
13
+
14
+ /**
15
+ * Billing plan registry.
16
+ *
17
+ * In-memory registry for managing available billing plans,
18
+ * following the same pattern as the webhook handler registry.
19
+ */
20
+
21
+ /** Create a PlanRegistry that stores plans in memory. */
22
+ declare function createPlanRegistry(): PlanRegistry;
23
+
24
+ /**
25
+ * Feature gate middleware for plan-based access control.
26
+ *
27
+ * Checks whether a shop's active billing plan includes a given feature.
28
+ * Falls back to free features when no active plan exists.
29
+ */
30
+
31
+ /** Create a FeatureGateMiddleware from the given configuration. */
32
+ declare function createFeatureGateMiddleware(config: FeatureGateConfig): FeatureGateMiddleware;
33
+
34
+ export { createBillingService, createFeatureGateMiddleware, createPlanRegistry };