@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.
- package/dist/auth/index.d.cts +165 -0
- package/dist/auth/index.d.ts +165 -0
- package/dist/auth/index.js +443 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/index.mjs +406 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/billing/index.d.cts +34 -0
- package/dist/billing/index.d.ts +34 -0
- package/dist/billing/index.js +254 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/billing/index.mjs +225 -0
- package/dist/billing/index.mjs.map +1 -0
- package/dist/config/index.d.cts +12 -0
- package/dist/config/index.d.ts +12 -0
- package/dist/config/index.js +186 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.mjs +156 -0
- package/dist/config/index.mjs.map +1 -0
- package/dist/database/index.d.cts +33 -0
- package/dist/database/index.d.ts +33 -0
- package/dist/database/index.js +127 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/index.mjs +95 -0
- package/dist/database/index.mjs.map +1 -0
- package/dist/graphql/index.d.cts +36 -0
- package/dist/graphql/index.d.ts +36 -0
- package/dist/graphql/index.js +209 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/graphql/index.mjs +179 -0
- package/dist/graphql/index.mjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +10 -0
- package/dist/index.mjs.map +1 -0
- package/dist/multi-store/index.d.cts +11 -0
- package/dist/multi-store/index.d.ts +11 -0
- package/dist/multi-store/index.js +473 -0
- package/dist/multi-store/index.js.map +1 -0
- package/dist/multi-store/index.mjs +447 -0
- package/dist/multi-store/index.mjs.map +1 -0
- package/dist/multi-tenant/index.d.cts +23 -0
- package/dist/multi-tenant/index.d.ts +23 -0
- package/dist/multi-tenant/index.js +69 -0
- package/dist/multi-tenant/index.js.map +1 -0
- package/dist/multi-tenant/index.mjs +41 -0
- package/dist/multi-tenant/index.mjs.map +1 -0
- package/dist/performance/index.d.cts +34 -0
- package/dist/performance/index.d.ts +34 -0
- package/dist/performance/index.js +319 -0
- package/dist/performance/index.js.map +1 -0
- package/dist/performance/index.mjs +290 -0
- package/dist/performance/index.mjs.map +1 -0
- package/dist/platform/index.d.cts +25 -0
- package/dist/platform/index.d.ts +25 -0
- package/dist/platform/index.js +91 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/index.mjs +62 -0
- package/dist/platform/index.mjs.map +1 -0
- package/dist/rbac/index.d.cts +24 -0
- package/dist/rbac/index.d.ts +24 -0
- package/dist/rbac/index.js +267 -0
- package/dist/rbac/index.js.map +1 -0
- package/dist/rbac/index.mjs +236 -0
- package/dist/rbac/index.mjs.map +1 -0
- package/dist/schema-CM7mHj_H.d.cts +53 -0
- package/dist/schema-CM7mHj_H.d.ts +53 -0
- package/dist/security/index.d.cts +47 -0
- package/dist/security/index.d.ts +47 -0
- package/dist/security/index.js +505 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/index.mjs +474 -0
- package/dist/security/index.mjs.map +1 -0
- package/dist/session-storage/index.d.cts +70 -0
- package/dist/session-storage/index.d.ts +70 -0
- package/dist/session-storage/index.js +271 -0
- package/dist/session-storage/index.js.map +1 -0
- package/dist/session-storage/index.mjs +242 -0
- package/dist/session-storage/index.mjs.map +1 -0
- package/dist/webhooks/index.d.cts +89 -0
- package/dist/webhooks/index.d.ts +89 -0
- package/dist/webhooks/index.js +380 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/index.mjs +348 -0
- package/dist/webhooks/index.mjs.map +1 -0
- package/package.json +119 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/session-storage/index.ts
|
|
21
|
+
var session_storage_exports = {};
|
|
22
|
+
__export(session_storage_exports, {
|
|
23
|
+
DualSessionStorage: () => DualSessionStorage,
|
|
24
|
+
PrismaSessionStorage: () => PrismaSessionStorage,
|
|
25
|
+
RedisSessionStorage: () => RedisSessionStorage
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(session_storage_exports);
|
|
28
|
+
|
|
29
|
+
// src/session-storage/prisma-session-storage.ts
|
|
30
|
+
var import_client = require("@prisma/client");
|
|
31
|
+
|
|
32
|
+
// src/database/utils.ts
|
|
33
|
+
function recordToSession(record) {
|
|
34
|
+
const session = {
|
|
35
|
+
id: record.id,
|
|
36
|
+
shop: record.shopDomain,
|
|
37
|
+
state: record.state,
|
|
38
|
+
isOnline: record.isOnline,
|
|
39
|
+
scope: record.scope,
|
|
40
|
+
expires: record.expires ?? null,
|
|
41
|
+
createdAt: record.createdAt,
|
|
42
|
+
updatedAt: record.updatedAt
|
|
43
|
+
};
|
|
44
|
+
if (record.accessToken) {
|
|
45
|
+
session.accessToken = record.accessToken;
|
|
46
|
+
}
|
|
47
|
+
if (record.refreshToken) {
|
|
48
|
+
session.refreshToken = record.refreshToken;
|
|
49
|
+
}
|
|
50
|
+
if (record.refreshTokenExpiresAt) {
|
|
51
|
+
session.refreshTokenExpiresAt = record.refreshTokenExpiresAt;
|
|
52
|
+
}
|
|
53
|
+
if (record.onlineAccessInfo) {
|
|
54
|
+
session.onlineAccessInfo = record.onlineAccessInfo;
|
|
55
|
+
}
|
|
56
|
+
return session;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/session-storage/prisma-session-storage.ts
|
|
60
|
+
var PrismaSessionStorage = class {
|
|
61
|
+
constructor(prisma) {
|
|
62
|
+
this.prisma = prisma;
|
|
63
|
+
}
|
|
64
|
+
async storeSession(session) {
|
|
65
|
+
const onlineAccessInfo = session.onlineAccessInfo ? session.onlineAccessInfo : import_client.Prisma.JsonNull;
|
|
66
|
+
const data = {
|
|
67
|
+
shopDomain: session.shop,
|
|
68
|
+
state: session.state,
|
|
69
|
+
isOnline: session.isOnline,
|
|
70
|
+
scope: session.scope,
|
|
71
|
+
expires: session.expires,
|
|
72
|
+
accessToken: session.accessToken ?? null,
|
|
73
|
+
refreshToken: session.refreshToken ?? null,
|
|
74
|
+
refreshTokenExpiresAt: session.refreshTokenExpiresAt ?? null,
|
|
75
|
+
onlineAccessInfo
|
|
76
|
+
};
|
|
77
|
+
await this.prisma.session.upsert({
|
|
78
|
+
where: { id: session.id },
|
|
79
|
+
create: {
|
|
80
|
+
id: session.id,
|
|
81
|
+
...data
|
|
82
|
+
},
|
|
83
|
+
update: data
|
|
84
|
+
});
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
async loadSession(id) {
|
|
88
|
+
const record = await this.prisma.session.findUnique({ where: { id } });
|
|
89
|
+
if (!record) return void 0;
|
|
90
|
+
return recordToSession(record);
|
|
91
|
+
}
|
|
92
|
+
async deleteSession(id) {
|
|
93
|
+
try {
|
|
94
|
+
await this.prisma.session.delete({ where: { id } });
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async deleteSessions(ids) {
|
|
101
|
+
await this.prisma.session.deleteMany({ where: { id: { in: ids } } });
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
async findSessionsByShop(shop) {
|
|
105
|
+
const records = await this.prisma.session.findMany({
|
|
106
|
+
where: { shopDomain: shop }
|
|
107
|
+
});
|
|
108
|
+
return records.map(recordToSession);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/session-storage/redis-session-storage.ts
|
|
113
|
+
var RedisSessionStorage = class {
|
|
114
|
+
constructor(redis, options) {
|
|
115
|
+
this.redis = redis;
|
|
116
|
+
this.prefix = options?.prefix ?? "uniforge:session:";
|
|
117
|
+
this.ttl = options?.ttl;
|
|
118
|
+
}
|
|
119
|
+
prefix;
|
|
120
|
+
ttl;
|
|
121
|
+
async storeSession(session) {
|
|
122
|
+
const key = this.sessionKey(session.id);
|
|
123
|
+
const data = this.serialize(session);
|
|
124
|
+
const pipeline = this.redis.pipeline();
|
|
125
|
+
if (this.ttl) {
|
|
126
|
+
pipeline.set(key, data, "EX", this.ttl);
|
|
127
|
+
} else {
|
|
128
|
+
pipeline.set(key, data);
|
|
129
|
+
}
|
|
130
|
+
pipeline.sadd(this.shopIndexKey(session.shop), session.id);
|
|
131
|
+
await pipeline.exec();
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
async loadSession(id) {
|
|
135
|
+
const data = await this.redis.get(this.sessionKey(id));
|
|
136
|
+
if (!data) return void 0;
|
|
137
|
+
return this.deserialize(data);
|
|
138
|
+
}
|
|
139
|
+
async deleteSession(id) {
|
|
140
|
+
const session = await this.loadSession(id);
|
|
141
|
+
const pipeline = this.redis.pipeline();
|
|
142
|
+
pipeline.del(this.sessionKey(id));
|
|
143
|
+
if (session) {
|
|
144
|
+
pipeline.srem(this.shopIndexKey(session.shop), id);
|
|
145
|
+
}
|
|
146
|
+
await pipeline.exec();
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
async deleteSessions(ids) {
|
|
150
|
+
if (ids.length === 0) return true;
|
|
151
|
+
const sessions = await Promise.all(ids.map((id) => this.loadSession(id)));
|
|
152
|
+
const pipeline = this.redis.pipeline();
|
|
153
|
+
for (const id of ids) {
|
|
154
|
+
pipeline.del(this.sessionKey(id));
|
|
155
|
+
}
|
|
156
|
+
for (const session of sessions) {
|
|
157
|
+
if (session) {
|
|
158
|
+
pipeline.srem(this.shopIndexKey(session.shop), session.id);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await pipeline.exec();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
async findSessionsByShop(shop) {
|
|
165
|
+
const ids = await this.redis.smembers(this.shopIndexKey(shop));
|
|
166
|
+
if (ids.length === 0) return [];
|
|
167
|
+
const keys = ids.map((id) => this.sessionKey(id));
|
|
168
|
+
const results = await this.redis.mget(...keys);
|
|
169
|
+
const sessions = [];
|
|
170
|
+
for (const data of results) {
|
|
171
|
+
if (data) {
|
|
172
|
+
sessions.push(this.deserialize(data));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return sessions;
|
|
176
|
+
}
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Helpers
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
sessionKey(id) {
|
|
181
|
+
return `${this.prefix}${id}`;
|
|
182
|
+
}
|
|
183
|
+
shopIndexKey(shopDomain) {
|
|
184
|
+
return `${this.prefix}shop:${shopDomain}`;
|
|
185
|
+
}
|
|
186
|
+
serialize(session) {
|
|
187
|
+
return JSON.stringify({
|
|
188
|
+
...session,
|
|
189
|
+
expires: session.expires?.toISOString() ?? null,
|
|
190
|
+
refreshTokenExpiresAt: session.refreshTokenExpiresAt?.toISOString(),
|
|
191
|
+
createdAt: session.createdAt.toISOString(),
|
|
192
|
+
updatedAt: session.updatedAt.toISOString()
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
deserialize(data) {
|
|
196
|
+
const raw = JSON.parse(data);
|
|
197
|
+
const session = {
|
|
198
|
+
id: raw.id,
|
|
199
|
+
shop: raw.shop,
|
|
200
|
+
state: raw.state,
|
|
201
|
+
isOnline: raw.isOnline,
|
|
202
|
+
scope: raw.scope,
|
|
203
|
+
expires: raw.expires ? new Date(raw.expires) : null,
|
|
204
|
+
createdAt: new Date(raw.createdAt),
|
|
205
|
+
updatedAt: new Date(raw.updatedAt)
|
|
206
|
+
};
|
|
207
|
+
if (raw.accessToken) {
|
|
208
|
+
session.accessToken = raw.accessToken;
|
|
209
|
+
}
|
|
210
|
+
if (raw.refreshToken) {
|
|
211
|
+
session.refreshToken = raw.refreshToken;
|
|
212
|
+
}
|
|
213
|
+
if (raw.refreshTokenExpiresAt) {
|
|
214
|
+
session.refreshTokenExpiresAt = new Date(raw.refreshTokenExpiresAt);
|
|
215
|
+
}
|
|
216
|
+
if (raw.onlineAccessInfo) {
|
|
217
|
+
session.onlineAccessInfo = raw.onlineAccessInfo;
|
|
218
|
+
}
|
|
219
|
+
return session;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// src/session-storage/dual-session-storage.ts
|
|
224
|
+
var DualSessionStorage = class {
|
|
225
|
+
constructor(primary, fallback) {
|
|
226
|
+
this.primary = primary;
|
|
227
|
+
this.fallback = fallback;
|
|
228
|
+
}
|
|
229
|
+
async storeSession(session) {
|
|
230
|
+
const [primaryOk, fallbackOk] = await Promise.all([
|
|
231
|
+
this.primary.storeSession(session),
|
|
232
|
+
this.fallback.storeSession(session)
|
|
233
|
+
]);
|
|
234
|
+
return primaryOk && fallbackOk;
|
|
235
|
+
}
|
|
236
|
+
async loadSession(id) {
|
|
237
|
+
const session = await this.primary.loadSession(id);
|
|
238
|
+
if (session) return session;
|
|
239
|
+
const fallbackSession = await this.fallback.loadSession(id);
|
|
240
|
+
if (fallbackSession) {
|
|
241
|
+
await this.primary.storeSession(fallbackSession);
|
|
242
|
+
}
|
|
243
|
+
return fallbackSession;
|
|
244
|
+
}
|
|
245
|
+
async deleteSession(id) {
|
|
246
|
+
const [primaryOk, fallbackOk] = await Promise.all([
|
|
247
|
+
this.primary.deleteSession(id),
|
|
248
|
+
this.fallback.deleteSession(id)
|
|
249
|
+
]);
|
|
250
|
+
return primaryOk && fallbackOk;
|
|
251
|
+
}
|
|
252
|
+
async deleteSessions(ids) {
|
|
253
|
+
const [primaryOk, fallbackOk] = await Promise.all([
|
|
254
|
+
this.primary.deleteSessions(ids),
|
|
255
|
+
this.fallback.deleteSessions(ids)
|
|
256
|
+
]);
|
|
257
|
+
return primaryOk && fallbackOk;
|
|
258
|
+
}
|
|
259
|
+
async findSessionsByShop(shop) {
|
|
260
|
+
const sessions = await this.primary.findSessionsByShop(shop);
|
|
261
|
+
if (sessions.length > 0) return sessions;
|
|
262
|
+
return this.fallback.findSessionsByShop(shop);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
266
|
+
0 && (module.exports = {
|
|
267
|
+
DualSessionStorage,
|
|
268
|
+
PrismaSessionStorage,
|
|
269
|
+
RedisSessionStorage
|
|
270
|
+
});
|
|
271
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/session-storage/index.ts","../../src/session-storage/prisma-session-storage.ts","../../src/database/utils.ts","../../src/session-storage/redis-session-storage.ts","../../src/session-storage/dual-session-storage.ts"],"sourcesContent":["/**\n * @uniforge/core - Session Storage\n *\n * Pluggable session storage implementations (Prisma, Redis, Dual).\n */\n\nexport { PrismaSessionStorage } from './prisma-session-storage';\nexport { RedisSessionStorage } from './redis-session-storage';\nexport type { RedisSessionStorageOptions } from './redis-session-storage';\nexport { DualSessionStorage } from './dual-session-storage';\n","/**\n * Prisma-backed session storage.\n *\n * Persists sessions to PostgreSQL via PrismaClient.\n */\n\nimport { Prisma } from '@prisma/client';\nimport type { PrismaClient } from '@prisma/client';\nimport type { Session, SessionStorage } from '@uniforge/platform-core/auth';\nimport { recordToSession } from '../database/utils';\n\nexport class PrismaSessionStorage implements SessionStorage {\n constructor(private readonly prisma: PrismaClient) {}\n\n async storeSession(session: Session): Promise<boolean> {\n const onlineAccessInfo: Prisma.NullableJsonNullValueInput | Prisma.InputJsonValue =\n session.onlineAccessInfo\n ? (session.onlineAccessInfo as unknown as Prisma.InputJsonValue)\n : Prisma.JsonNull;\n\n const data = {\n shopDomain: session.shop,\n state: session.state,\n isOnline: session.isOnline,\n scope: session.scope,\n expires: session.expires,\n accessToken: session.accessToken ?? null,\n refreshToken: session.refreshToken ?? null,\n refreshTokenExpiresAt: session.refreshTokenExpiresAt ?? null,\n onlineAccessInfo,\n };\n\n await this.prisma.session.upsert({\n where: { id: session.id },\n create: {\n id: session.id,\n ...data,\n },\n update: data,\n });\n return true;\n }\n\n async loadSession(id: string): Promise<Session | undefined> {\n const record = await this.prisma.session.findUnique({ where: { id } });\n if (!record) return undefined;\n return recordToSession(record);\n }\n\n async deleteSession(id: string): Promise<boolean> {\n try {\n await this.prisma.session.delete({ where: { id } });\n return true;\n } catch {\n return false;\n }\n }\n\n async deleteSessions(ids: string[]): Promise<boolean> {\n await this.prisma.session.deleteMany({ where: { id: { in: ids } } });\n return true;\n }\n\n async findSessionsByShop(shop: string): Promise<Session[]> {\n const records = await this.prisma.session.findMany({\n where: { shopDomain: shop },\n });\n return records.map(recordToSession);\n }\n}\n","/**\n * Converters between platform-core domain types and Prisma record types.\n *\n * The Prisma models use camelCase field names (shopDomain, isOnline, etc.)\n * while platform-core types use slightly different shapes (e.g. session.shop\n * vs record.shopDomain, onlineAccessInfo as object vs JSON).\n */\n\nimport type { Session, Shop } from '@uniforge/platform-core/auth';\nimport type {\n Shop as PrismaShop,\n Session as PrismaSession,\n Prisma,\n} from '@prisma/client';\n\n// ---------------------------------------------------------------------------\n// Shop converters\n// ---------------------------------------------------------------------------\n\n/** Convert a platform-core Shop to a Prisma-compatible record. */\nexport function shopToRecord(\n shop: Shop,\n): Omit<PrismaShop, 'id'> {\n return {\n shopDomain: shop.shopDomain,\n isInstalled: shop.isInstalled,\n installedAt: shop.installedAt,\n uninstalledAt: shop.uninstalledAt,\n scopes: shop.scopes,\n shopifyPlan: shop.shopifyPlan,\n createdAt: shop.createdAt,\n updatedAt: shop.updatedAt,\n };\n}\n\n/** Convert a Prisma Shop record to a platform-core Shop. */\nexport function recordToShop(record: PrismaShop): Shop {\n return {\n shopDomain: record.shopDomain,\n isInstalled: record.isInstalled,\n installedAt: record.installedAt ?? null,\n uninstalledAt: record.uninstalledAt ?? null,\n scopes: record.scopes,\n shopifyPlan: record.shopifyPlan ?? null,\n createdAt: record.createdAt,\n updatedAt: record.updatedAt,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Session converters\n// ---------------------------------------------------------------------------\n\n/** Convert a platform-core Session to a Prisma-compatible create/update input. */\nexport function sessionToRecord(\n session: Session,\n): Omit<PrismaSession, 'createdAt' | 'updatedAt'> {\n const record: Omit<PrismaSession, 'createdAt' | 'updatedAt'> = {\n id: session.id,\n shopDomain: session.shop,\n state: session.state,\n isOnline: session.isOnline,\n scope: session.scope,\n expires: session.expires,\n accessToken: session.accessToken ?? null,\n refreshToken: session.refreshToken ?? null,\n refreshTokenExpiresAt: session.refreshTokenExpiresAt ?? null,\n onlineAccessInfo: session.onlineAccessInfo\n ? (session.onlineAccessInfo as unknown as Prisma.JsonValue)\n : null,\n };\n\n return record;\n}\n\n/** Convert a Prisma Session record to a platform-core Session. */\nexport function recordToSession(record: PrismaSession): Session {\n const session: Session = {\n id: record.id,\n shop: record.shopDomain,\n state: record.state,\n isOnline: record.isOnline,\n scope: record.scope,\n expires: record.expires ?? null,\n createdAt: record.createdAt,\n updatedAt: record.updatedAt,\n };\n\n if (record.accessToken) {\n session.accessToken = record.accessToken;\n }\n if (record.refreshToken) {\n session.refreshToken = record.refreshToken;\n }\n if (record.refreshTokenExpiresAt) {\n session.refreshTokenExpiresAt = record.refreshTokenExpiresAt;\n }\n if (record.onlineAccessInfo) {\n session.onlineAccessInfo = record.onlineAccessInfo as unknown as NonNullable<Session['onlineAccessInfo']>;\n }\n\n return session;\n}\n","/**\n * Redis-backed session storage.\n *\n * Stores sessions as JSON strings in Redis with an optional TTL.\n * Maintains a secondary index (Redis SET) per shop domain so that\n * findSessionsByShop is efficient.\n */\n\nimport type Redis from 'ioredis';\nimport type { Session, SessionStorage } from '@uniforge/platform-core/auth';\n\nexport interface RedisSessionStorageOptions {\n /** Key prefix for session keys. Defaults to `uniforge:session:`. */\n prefix?: string;\n /** TTL for session keys in seconds. No expiry if omitted. */\n ttl?: number;\n}\n\nexport class RedisSessionStorage implements SessionStorage {\n private readonly prefix: string;\n private readonly ttl: number | undefined;\n\n constructor(\n private readonly redis: Redis,\n options?: RedisSessionStorageOptions,\n ) {\n this.prefix = options?.prefix ?? 'uniforge:session:';\n this.ttl = options?.ttl;\n }\n\n async storeSession(session: Session): Promise<boolean> {\n const key = this.sessionKey(session.id);\n const data = this.serialize(session);\n\n const pipeline = this.redis.pipeline();\n if (this.ttl) {\n pipeline.set(key, data, 'EX', this.ttl);\n } else {\n pipeline.set(key, data);\n }\n pipeline.sadd(this.shopIndexKey(session.shop), session.id);\n await pipeline.exec();\n return true;\n }\n\n async loadSession(id: string): Promise<Session | undefined> {\n const data = await this.redis.get(this.sessionKey(id));\n if (!data) return undefined;\n return this.deserialize(data);\n }\n\n async deleteSession(id: string): Promise<boolean> {\n const session = await this.loadSession(id);\n const pipeline = this.redis.pipeline();\n pipeline.del(this.sessionKey(id));\n if (session) {\n pipeline.srem(this.shopIndexKey(session.shop), id);\n }\n await pipeline.exec();\n return true;\n }\n\n async deleteSessions(ids: string[]): Promise<boolean> {\n if (ids.length === 0) return true;\n\n // Load sessions first to find their shops for index cleanup\n const sessions = await Promise.all(ids.map((id) => this.loadSession(id)));\n\n const pipeline = this.redis.pipeline();\n for (const id of ids) {\n pipeline.del(this.sessionKey(id));\n }\n for (const session of sessions) {\n if (session) {\n pipeline.srem(this.shopIndexKey(session.shop), session.id);\n }\n }\n await pipeline.exec();\n return true;\n }\n\n async findSessionsByShop(shop: string): Promise<Session[]> {\n const ids = await this.redis.smembers(this.shopIndexKey(shop));\n if (ids.length === 0) return [];\n\n const keys = ids.map((id) => this.sessionKey(id));\n const results = await this.redis.mget(...keys);\n\n const sessions: Session[] = [];\n for (const data of results) {\n if (data) {\n sessions.push(this.deserialize(data));\n }\n }\n return sessions;\n }\n\n // ---------------------------------------------------------------------------\n // Helpers\n // ---------------------------------------------------------------------------\n\n private sessionKey(id: string): string {\n return `${this.prefix}${id}`;\n }\n\n private shopIndexKey(shopDomain: string): string {\n return `${this.prefix}shop:${shopDomain}`;\n }\n\n private serialize(session: Session): string {\n return JSON.stringify({\n ...session,\n expires: session.expires?.toISOString() ?? null,\n refreshTokenExpiresAt: session.refreshTokenExpiresAt?.toISOString(),\n createdAt: session.createdAt.toISOString(),\n updatedAt: session.updatedAt.toISOString(),\n });\n }\n\n private deserialize(data: string): Session {\n const raw = JSON.parse(data) as Record<string, unknown>;\n\n const session: Session = {\n id: raw.id as string,\n shop: raw.shop as string,\n state: raw.state as string,\n isOnline: raw.isOnline as boolean,\n scope: raw.scope as string,\n expires: raw.expires ? new Date(raw.expires as string) : null,\n createdAt: new Date(raw.createdAt as string),\n updatedAt: new Date(raw.updatedAt as string),\n };\n\n if (raw.accessToken) {\n session.accessToken = raw.accessToken as string;\n }\n if (raw.refreshToken) {\n session.refreshToken = raw.refreshToken as string;\n }\n if (raw.refreshTokenExpiresAt) {\n session.refreshTokenExpiresAt = new Date(raw.refreshTokenExpiresAt as string);\n }\n if (raw.onlineAccessInfo) {\n session.onlineAccessInfo = raw.onlineAccessInfo as NonNullable<Session['onlineAccessInfo']>;\n }\n\n return session;\n }\n}\n","/**\n * Dual-layer session storage.\n *\n * Writes to both primary (typically Redis) and fallback (typically Prisma)\n * storage. Reads try primary first, falling back to the secondary store\n * and back-filling primary on a hit.\n */\n\nimport type { Session, SessionStorage } from '@uniforge/platform-core/auth';\n\nexport class DualSessionStorage implements SessionStorage {\n constructor(\n private readonly primary: SessionStorage,\n private readonly fallback: SessionStorage,\n ) {}\n\n async storeSession(session: Session): Promise<boolean> {\n const [primaryOk, fallbackOk] = await Promise.all([\n this.primary.storeSession(session),\n this.fallback.storeSession(session),\n ]);\n return primaryOk && fallbackOk;\n }\n\n async loadSession(id: string): Promise<Session | undefined> {\n const session = await this.primary.loadSession(id);\n if (session) return session;\n\n const fallbackSession = await this.fallback.loadSession(id);\n if (fallbackSession) {\n // Back-fill primary\n await this.primary.storeSession(fallbackSession);\n }\n return fallbackSession;\n }\n\n async deleteSession(id: string): Promise<boolean> {\n const [primaryOk, fallbackOk] = await Promise.all([\n this.primary.deleteSession(id),\n this.fallback.deleteSession(id),\n ]);\n return primaryOk && fallbackOk;\n }\n\n async deleteSessions(ids: string[]): Promise<boolean> {\n const [primaryOk, fallbackOk] = await Promise.all([\n this.primary.deleteSessions(ids),\n this.fallback.deleteSessions(ids),\n ]);\n return primaryOk && fallbackOk;\n }\n\n async findSessionsByShop(shop: string): Promise<Session[]> {\n const sessions = await this.primary.findSessionsByShop(shop);\n if (sessions.length > 0) return sessions;\n return this.fallback.findSessionsByShop(shop);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// src/session-storage/prisma-session-storage.ts
|
|
2
|
+
import { Prisma } from "@prisma/client";
|
|
3
|
+
|
|
4
|
+
// src/database/utils.ts
|
|
5
|
+
function recordToSession(record) {
|
|
6
|
+
const session = {
|
|
7
|
+
id: record.id,
|
|
8
|
+
shop: record.shopDomain,
|
|
9
|
+
state: record.state,
|
|
10
|
+
isOnline: record.isOnline,
|
|
11
|
+
scope: record.scope,
|
|
12
|
+
expires: record.expires ?? null,
|
|
13
|
+
createdAt: record.createdAt,
|
|
14
|
+
updatedAt: record.updatedAt
|
|
15
|
+
};
|
|
16
|
+
if (record.accessToken) {
|
|
17
|
+
session.accessToken = record.accessToken;
|
|
18
|
+
}
|
|
19
|
+
if (record.refreshToken) {
|
|
20
|
+
session.refreshToken = record.refreshToken;
|
|
21
|
+
}
|
|
22
|
+
if (record.refreshTokenExpiresAt) {
|
|
23
|
+
session.refreshTokenExpiresAt = record.refreshTokenExpiresAt;
|
|
24
|
+
}
|
|
25
|
+
if (record.onlineAccessInfo) {
|
|
26
|
+
session.onlineAccessInfo = record.onlineAccessInfo;
|
|
27
|
+
}
|
|
28
|
+
return session;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/session-storage/prisma-session-storage.ts
|
|
32
|
+
var PrismaSessionStorage = class {
|
|
33
|
+
constructor(prisma) {
|
|
34
|
+
this.prisma = prisma;
|
|
35
|
+
}
|
|
36
|
+
async storeSession(session) {
|
|
37
|
+
const onlineAccessInfo = session.onlineAccessInfo ? session.onlineAccessInfo : Prisma.JsonNull;
|
|
38
|
+
const data = {
|
|
39
|
+
shopDomain: session.shop,
|
|
40
|
+
state: session.state,
|
|
41
|
+
isOnline: session.isOnline,
|
|
42
|
+
scope: session.scope,
|
|
43
|
+
expires: session.expires,
|
|
44
|
+
accessToken: session.accessToken ?? null,
|
|
45
|
+
refreshToken: session.refreshToken ?? null,
|
|
46
|
+
refreshTokenExpiresAt: session.refreshTokenExpiresAt ?? null,
|
|
47
|
+
onlineAccessInfo
|
|
48
|
+
};
|
|
49
|
+
await this.prisma.session.upsert({
|
|
50
|
+
where: { id: session.id },
|
|
51
|
+
create: {
|
|
52
|
+
id: session.id,
|
|
53
|
+
...data
|
|
54
|
+
},
|
|
55
|
+
update: data
|
|
56
|
+
});
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
async loadSession(id) {
|
|
60
|
+
const record = await this.prisma.session.findUnique({ where: { id } });
|
|
61
|
+
if (!record) return void 0;
|
|
62
|
+
return recordToSession(record);
|
|
63
|
+
}
|
|
64
|
+
async deleteSession(id) {
|
|
65
|
+
try {
|
|
66
|
+
await this.prisma.session.delete({ where: { id } });
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async deleteSessions(ids) {
|
|
73
|
+
await this.prisma.session.deleteMany({ where: { id: { in: ids } } });
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
async findSessionsByShop(shop) {
|
|
77
|
+
const records = await this.prisma.session.findMany({
|
|
78
|
+
where: { shopDomain: shop }
|
|
79
|
+
});
|
|
80
|
+
return records.map(recordToSession);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/session-storage/redis-session-storage.ts
|
|
85
|
+
var RedisSessionStorage = class {
|
|
86
|
+
constructor(redis, options) {
|
|
87
|
+
this.redis = redis;
|
|
88
|
+
this.prefix = options?.prefix ?? "uniforge:session:";
|
|
89
|
+
this.ttl = options?.ttl;
|
|
90
|
+
}
|
|
91
|
+
prefix;
|
|
92
|
+
ttl;
|
|
93
|
+
async storeSession(session) {
|
|
94
|
+
const key = this.sessionKey(session.id);
|
|
95
|
+
const data = this.serialize(session);
|
|
96
|
+
const pipeline = this.redis.pipeline();
|
|
97
|
+
if (this.ttl) {
|
|
98
|
+
pipeline.set(key, data, "EX", this.ttl);
|
|
99
|
+
} else {
|
|
100
|
+
pipeline.set(key, data);
|
|
101
|
+
}
|
|
102
|
+
pipeline.sadd(this.shopIndexKey(session.shop), session.id);
|
|
103
|
+
await pipeline.exec();
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
async loadSession(id) {
|
|
107
|
+
const data = await this.redis.get(this.sessionKey(id));
|
|
108
|
+
if (!data) return void 0;
|
|
109
|
+
return this.deserialize(data);
|
|
110
|
+
}
|
|
111
|
+
async deleteSession(id) {
|
|
112
|
+
const session = await this.loadSession(id);
|
|
113
|
+
const pipeline = this.redis.pipeline();
|
|
114
|
+
pipeline.del(this.sessionKey(id));
|
|
115
|
+
if (session) {
|
|
116
|
+
pipeline.srem(this.shopIndexKey(session.shop), id);
|
|
117
|
+
}
|
|
118
|
+
await pipeline.exec();
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
async deleteSessions(ids) {
|
|
122
|
+
if (ids.length === 0) return true;
|
|
123
|
+
const sessions = await Promise.all(ids.map((id) => this.loadSession(id)));
|
|
124
|
+
const pipeline = this.redis.pipeline();
|
|
125
|
+
for (const id of ids) {
|
|
126
|
+
pipeline.del(this.sessionKey(id));
|
|
127
|
+
}
|
|
128
|
+
for (const session of sessions) {
|
|
129
|
+
if (session) {
|
|
130
|
+
pipeline.srem(this.shopIndexKey(session.shop), session.id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
await pipeline.exec();
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
async findSessionsByShop(shop) {
|
|
137
|
+
const ids = await this.redis.smembers(this.shopIndexKey(shop));
|
|
138
|
+
if (ids.length === 0) return [];
|
|
139
|
+
const keys = ids.map((id) => this.sessionKey(id));
|
|
140
|
+
const results = await this.redis.mget(...keys);
|
|
141
|
+
const sessions = [];
|
|
142
|
+
for (const data of results) {
|
|
143
|
+
if (data) {
|
|
144
|
+
sessions.push(this.deserialize(data));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return sessions;
|
|
148
|
+
}
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Helpers
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
sessionKey(id) {
|
|
153
|
+
return `${this.prefix}${id}`;
|
|
154
|
+
}
|
|
155
|
+
shopIndexKey(shopDomain) {
|
|
156
|
+
return `${this.prefix}shop:${shopDomain}`;
|
|
157
|
+
}
|
|
158
|
+
serialize(session) {
|
|
159
|
+
return JSON.stringify({
|
|
160
|
+
...session,
|
|
161
|
+
expires: session.expires?.toISOString() ?? null,
|
|
162
|
+
refreshTokenExpiresAt: session.refreshTokenExpiresAt?.toISOString(),
|
|
163
|
+
createdAt: session.createdAt.toISOString(),
|
|
164
|
+
updatedAt: session.updatedAt.toISOString()
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
deserialize(data) {
|
|
168
|
+
const raw = JSON.parse(data);
|
|
169
|
+
const session = {
|
|
170
|
+
id: raw.id,
|
|
171
|
+
shop: raw.shop,
|
|
172
|
+
state: raw.state,
|
|
173
|
+
isOnline: raw.isOnline,
|
|
174
|
+
scope: raw.scope,
|
|
175
|
+
expires: raw.expires ? new Date(raw.expires) : null,
|
|
176
|
+
createdAt: new Date(raw.createdAt),
|
|
177
|
+
updatedAt: new Date(raw.updatedAt)
|
|
178
|
+
};
|
|
179
|
+
if (raw.accessToken) {
|
|
180
|
+
session.accessToken = raw.accessToken;
|
|
181
|
+
}
|
|
182
|
+
if (raw.refreshToken) {
|
|
183
|
+
session.refreshToken = raw.refreshToken;
|
|
184
|
+
}
|
|
185
|
+
if (raw.refreshTokenExpiresAt) {
|
|
186
|
+
session.refreshTokenExpiresAt = new Date(raw.refreshTokenExpiresAt);
|
|
187
|
+
}
|
|
188
|
+
if (raw.onlineAccessInfo) {
|
|
189
|
+
session.onlineAccessInfo = raw.onlineAccessInfo;
|
|
190
|
+
}
|
|
191
|
+
return session;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/session-storage/dual-session-storage.ts
|
|
196
|
+
var DualSessionStorage = class {
|
|
197
|
+
constructor(primary, fallback) {
|
|
198
|
+
this.primary = primary;
|
|
199
|
+
this.fallback = fallback;
|
|
200
|
+
}
|
|
201
|
+
async storeSession(session) {
|
|
202
|
+
const [primaryOk, fallbackOk] = await Promise.all([
|
|
203
|
+
this.primary.storeSession(session),
|
|
204
|
+
this.fallback.storeSession(session)
|
|
205
|
+
]);
|
|
206
|
+
return primaryOk && fallbackOk;
|
|
207
|
+
}
|
|
208
|
+
async loadSession(id) {
|
|
209
|
+
const session = await this.primary.loadSession(id);
|
|
210
|
+
if (session) return session;
|
|
211
|
+
const fallbackSession = await this.fallback.loadSession(id);
|
|
212
|
+
if (fallbackSession) {
|
|
213
|
+
await this.primary.storeSession(fallbackSession);
|
|
214
|
+
}
|
|
215
|
+
return fallbackSession;
|
|
216
|
+
}
|
|
217
|
+
async deleteSession(id) {
|
|
218
|
+
const [primaryOk, fallbackOk] = await Promise.all([
|
|
219
|
+
this.primary.deleteSession(id),
|
|
220
|
+
this.fallback.deleteSession(id)
|
|
221
|
+
]);
|
|
222
|
+
return primaryOk && fallbackOk;
|
|
223
|
+
}
|
|
224
|
+
async deleteSessions(ids) {
|
|
225
|
+
const [primaryOk, fallbackOk] = await Promise.all([
|
|
226
|
+
this.primary.deleteSessions(ids),
|
|
227
|
+
this.fallback.deleteSessions(ids)
|
|
228
|
+
]);
|
|
229
|
+
return primaryOk && fallbackOk;
|
|
230
|
+
}
|
|
231
|
+
async findSessionsByShop(shop) {
|
|
232
|
+
const sessions = await this.primary.findSessionsByShop(shop);
|
|
233
|
+
if (sessions.length > 0) return sessions;
|
|
234
|
+
return this.fallback.findSessionsByShop(shop);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
export {
|
|
238
|
+
DualSessionStorage,
|
|
239
|
+
PrismaSessionStorage,
|
|
240
|
+
RedisSessionStorage
|
|
241
|
+
};
|
|
242
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/session-storage/prisma-session-storage.ts","../../src/database/utils.ts","../../src/session-storage/redis-session-storage.ts","../../src/session-storage/dual-session-storage.ts"],"sourcesContent":["/**\n * Prisma-backed session storage.\n *\n * Persists sessions to PostgreSQL via PrismaClient.\n */\n\nimport { Prisma } from '@prisma/client';\nimport type { PrismaClient } from '@prisma/client';\nimport type { Session, SessionStorage } from '@uniforge/platform-core/auth';\nimport { recordToSession } from '../database/utils';\n\nexport class PrismaSessionStorage implements SessionStorage {\n constructor(private readonly prisma: PrismaClient) {}\n\n async storeSession(session: Session): Promise<boolean> {\n const onlineAccessInfo: Prisma.NullableJsonNullValueInput | Prisma.InputJsonValue =\n session.onlineAccessInfo\n ? (session.onlineAccessInfo as unknown as Prisma.InputJsonValue)\n : Prisma.JsonNull;\n\n const data = {\n shopDomain: session.shop,\n state: session.state,\n isOnline: session.isOnline,\n scope: session.scope,\n expires: session.expires,\n accessToken: session.accessToken ?? null,\n refreshToken: session.refreshToken ?? null,\n refreshTokenExpiresAt: session.refreshTokenExpiresAt ?? null,\n onlineAccessInfo,\n };\n\n await this.prisma.session.upsert({\n where: { id: session.id },\n create: {\n id: session.id,\n ...data,\n },\n update: data,\n });\n return true;\n }\n\n async loadSession(id: string): Promise<Session | undefined> {\n const record = await this.prisma.session.findUnique({ where: { id } });\n if (!record) return undefined;\n return recordToSession(record);\n }\n\n async deleteSession(id: string): Promise<boolean> {\n try {\n await this.prisma.session.delete({ where: { id } });\n return true;\n } catch {\n return false;\n }\n }\n\n async deleteSessions(ids: string[]): Promise<boolean> {\n await this.prisma.session.deleteMany({ where: { id: { in: ids } } });\n return true;\n }\n\n async findSessionsByShop(shop: string): Promise<Session[]> {\n const records = await this.prisma.session.findMany({\n where: { shopDomain: shop },\n });\n return records.map(recordToSession);\n }\n}\n","/**\n * Converters between platform-core domain types and Prisma record types.\n *\n * The Prisma models use camelCase field names (shopDomain, isOnline, etc.)\n * while platform-core types use slightly different shapes (e.g. session.shop\n * vs record.shopDomain, onlineAccessInfo as object vs JSON).\n */\n\nimport type { Session, Shop } from '@uniforge/platform-core/auth';\nimport type {\n Shop as PrismaShop,\n Session as PrismaSession,\n Prisma,\n} from '@prisma/client';\n\n// ---------------------------------------------------------------------------\n// Shop converters\n// ---------------------------------------------------------------------------\n\n/** Convert a platform-core Shop to a Prisma-compatible record. */\nexport function shopToRecord(\n shop: Shop,\n): Omit<PrismaShop, 'id'> {\n return {\n shopDomain: shop.shopDomain,\n isInstalled: shop.isInstalled,\n installedAt: shop.installedAt,\n uninstalledAt: shop.uninstalledAt,\n scopes: shop.scopes,\n shopifyPlan: shop.shopifyPlan,\n createdAt: shop.createdAt,\n updatedAt: shop.updatedAt,\n };\n}\n\n/** Convert a Prisma Shop record to a platform-core Shop. */\nexport function recordToShop(record: PrismaShop): Shop {\n return {\n shopDomain: record.shopDomain,\n isInstalled: record.isInstalled,\n installedAt: record.installedAt ?? null,\n uninstalledAt: record.uninstalledAt ?? null,\n scopes: record.scopes,\n shopifyPlan: record.shopifyPlan ?? null,\n createdAt: record.createdAt,\n updatedAt: record.updatedAt,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Session converters\n// ---------------------------------------------------------------------------\n\n/** Convert a platform-core Session to a Prisma-compatible create/update input. */\nexport function sessionToRecord(\n session: Session,\n): Omit<PrismaSession, 'createdAt' | 'updatedAt'> {\n const record: Omit<PrismaSession, 'createdAt' | 'updatedAt'> = {\n id: session.id,\n shopDomain: session.shop,\n state: session.state,\n isOnline: session.isOnline,\n scope: session.scope,\n expires: session.expires,\n accessToken: session.accessToken ?? null,\n refreshToken: session.refreshToken ?? null,\n refreshTokenExpiresAt: session.refreshTokenExpiresAt ?? null,\n onlineAccessInfo: session.onlineAccessInfo\n ? (session.onlineAccessInfo as unknown as Prisma.JsonValue)\n : null,\n };\n\n return record;\n}\n\n/** Convert a Prisma Session record to a platform-core Session. */\nexport function recordToSession(record: PrismaSession): Session {\n const session: Session = {\n id: record.id,\n shop: record.shopDomain,\n state: record.state,\n isOnline: record.isOnline,\n scope: record.scope,\n expires: record.expires ?? null,\n createdAt: record.createdAt,\n updatedAt: record.updatedAt,\n };\n\n if (record.accessToken) {\n session.accessToken = record.accessToken;\n }\n if (record.refreshToken) {\n session.refreshToken = record.refreshToken;\n }\n if (record.refreshTokenExpiresAt) {\n session.refreshTokenExpiresAt = record.refreshTokenExpiresAt;\n }\n if (record.onlineAccessInfo) {\n session.onlineAccessInfo = record.onlineAccessInfo as unknown as NonNullable<Session['onlineAccessInfo']>;\n }\n\n return session;\n}\n","/**\n * Redis-backed session storage.\n *\n * Stores sessions as JSON strings in Redis with an optional TTL.\n * Maintains a secondary index (Redis SET) per shop domain so that\n * findSessionsByShop is efficient.\n */\n\nimport type Redis from 'ioredis';\nimport type { Session, SessionStorage } from '@uniforge/platform-core/auth';\n\nexport interface RedisSessionStorageOptions {\n /** Key prefix for session keys. Defaults to `uniforge:session:`. */\n prefix?: string;\n /** TTL for session keys in seconds. No expiry if omitted. */\n ttl?: number;\n}\n\nexport class RedisSessionStorage implements SessionStorage {\n private readonly prefix: string;\n private readonly ttl: number | undefined;\n\n constructor(\n private readonly redis: Redis,\n options?: RedisSessionStorageOptions,\n ) {\n this.prefix = options?.prefix ?? 'uniforge:session:';\n this.ttl = options?.ttl;\n }\n\n async storeSession(session: Session): Promise<boolean> {\n const key = this.sessionKey(session.id);\n const data = this.serialize(session);\n\n const pipeline = this.redis.pipeline();\n if (this.ttl) {\n pipeline.set(key, data, 'EX', this.ttl);\n } else {\n pipeline.set(key, data);\n }\n pipeline.sadd(this.shopIndexKey(session.shop), session.id);\n await pipeline.exec();\n return true;\n }\n\n async loadSession(id: string): Promise<Session | undefined> {\n const data = await this.redis.get(this.sessionKey(id));\n if (!data) return undefined;\n return this.deserialize(data);\n }\n\n async deleteSession(id: string): Promise<boolean> {\n const session = await this.loadSession(id);\n const pipeline = this.redis.pipeline();\n pipeline.del(this.sessionKey(id));\n if (session) {\n pipeline.srem(this.shopIndexKey(session.shop), id);\n }\n await pipeline.exec();\n return true;\n }\n\n async deleteSessions(ids: string[]): Promise<boolean> {\n if (ids.length === 0) return true;\n\n // Load sessions first to find their shops for index cleanup\n const sessions = await Promise.all(ids.map((id) => this.loadSession(id)));\n\n const pipeline = this.redis.pipeline();\n for (const id of ids) {\n pipeline.del(this.sessionKey(id));\n }\n for (const session of sessions) {\n if (session) {\n pipeline.srem(this.shopIndexKey(session.shop), session.id);\n }\n }\n await pipeline.exec();\n return true;\n }\n\n async findSessionsByShop(shop: string): Promise<Session[]> {\n const ids = await this.redis.smembers(this.shopIndexKey(shop));\n if (ids.length === 0) return [];\n\n const keys = ids.map((id) => this.sessionKey(id));\n const results = await this.redis.mget(...keys);\n\n const sessions: Session[] = [];\n for (const data of results) {\n if (data) {\n sessions.push(this.deserialize(data));\n }\n }\n return sessions;\n }\n\n // ---------------------------------------------------------------------------\n // Helpers\n // ---------------------------------------------------------------------------\n\n private sessionKey(id: string): string {\n return `${this.prefix}${id}`;\n }\n\n private shopIndexKey(shopDomain: string): string {\n return `${this.prefix}shop:${shopDomain}`;\n }\n\n private serialize(session: Session): string {\n return JSON.stringify({\n ...session,\n expires: session.expires?.toISOString() ?? null,\n refreshTokenExpiresAt: session.refreshTokenExpiresAt?.toISOString(),\n createdAt: session.createdAt.toISOString(),\n updatedAt: session.updatedAt.toISOString(),\n });\n }\n\n private deserialize(data: string): Session {\n const raw = JSON.parse(data) as Record<string, unknown>;\n\n const session: Session = {\n id: raw.id as string,\n shop: raw.shop as string,\n state: raw.state as string,\n isOnline: raw.isOnline as boolean,\n scope: raw.scope as string,\n expires: raw.expires ? new Date(raw.expires as string) : null,\n createdAt: new Date(raw.createdAt as string),\n updatedAt: new Date(raw.updatedAt as string),\n };\n\n if (raw.accessToken) {\n session.accessToken = raw.accessToken as string;\n }\n if (raw.refreshToken) {\n session.refreshToken = raw.refreshToken as string;\n }\n if (raw.refreshTokenExpiresAt) {\n session.refreshTokenExpiresAt = new Date(raw.refreshTokenExpiresAt as string);\n }\n if (raw.onlineAccessInfo) {\n session.onlineAccessInfo = raw.onlineAccessInfo as NonNullable<Session['onlineAccessInfo']>;\n }\n\n return session;\n }\n}\n","/**\n * Dual-layer session storage.\n *\n * Writes to both primary (typically Redis) and fallback (typically Prisma)\n * storage. Reads try primary first, falling back to the secondary store\n * and back-filling primary on a hit.\n */\n\nimport type { Session, SessionStorage } from '@uniforge/platform-core/auth';\n\nexport class DualSessionStorage implements SessionStorage {\n constructor(\n private readonly primary: SessionStorage,\n private readonly fallback: SessionStorage,\n ) {}\n\n async storeSession(session: Session): Promise<boolean> {\n const [primaryOk, fallbackOk] = await Promise.all([\n this.primary.storeSession(session),\n this.fallback.storeSession(session),\n ]);\n return primaryOk && fallbackOk;\n }\n\n async loadSession(id: string): Promise<Session | undefined> {\n const session = await this.primary.loadSession(id);\n if (session) return session;\n\n const fallbackSession = await this.fallback.loadSession(id);\n if (fallbackSession) {\n // Back-fill primary\n await this.primary.storeSession(fallbackSession);\n }\n return fallbackSession;\n }\n\n async deleteSession(id: string): Promise<boolean> {\n const [primaryOk, fallbackOk] = await Promise.all([\n this.primary.deleteSession(id),\n this.fallback.deleteSession(id),\n ]);\n return primaryOk && fallbackOk;\n }\n\n async deleteSessions(ids: string[]): Promise<boolean> {\n const [primaryOk, fallbackOk] = await Promise.all([\n this.primary.deleteSessions(ids),\n this.fallback.deleteSessions(ids),\n ]);\n return primaryOk && fallbackOk;\n }\n\n async findSessionsByShop(shop: string): Promise<Session[]> {\n const sessions = await this.primary.findSessionsByShop(shop);\n if (sessions.length > 0) return sessions;\n return this.fallback.findSessionsByShop(shop);\n }\n}\n"],"mappings":";AAMA,SAAS,cAAc;;;ACsEhB,SAAS,gBAAgB,QAAgC;AAC9D,QAAM,UAAmB;AAAA,IACvB,IAAI,OAAO;AAAA,IACX,MAAM,OAAO;AAAA,IACb,OAAO,OAAO;AAAA,IACd,UAAU,OAAO;AAAA,IACjB,OAAO,OAAO;AAAA,IACd,SAAS,OAAO,WAAW;AAAA,IAC3B,WAAW,OAAO;AAAA,IAClB,WAAW,OAAO;AAAA,EACpB;AAEA,MAAI,OAAO,aAAa;AACtB,YAAQ,cAAc,OAAO;AAAA,EAC/B;AACA,MAAI,OAAO,cAAc;AACvB,YAAQ,eAAe,OAAO;AAAA,EAChC;AACA,MAAI,OAAO,uBAAuB;AAChC,YAAQ,wBAAwB,OAAO;AAAA,EACzC;AACA,MAAI,OAAO,kBAAkB;AAC3B,YAAQ,mBAAmB,OAAO;AAAA,EACpC;AAEA,SAAO;AACT;;;AD3FO,IAAM,uBAAN,MAAqD;AAAA,EAC1D,YAA6B,QAAsB;AAAtB;AAAA,EAAuB;AAAA,EAEpD,MAAM,aAAa,SAAoC;AACrD,UAAM,mBACJ,QAAQ,mBACH,QAAQ,mBACT,OAAO;AAEb,UAAM,OAAO;AAAA,MACX,YAAY,QAAQ;AAAA,MACpB,OAAO,QAAQ;AAAA,MACf,UAAU,QAAQ;AAAA,MAClB,OAAO,QAAQ;AAAA,MACf,SAAS,QAAQ;AAAA,MACjB,aAAa,QAAQ,eAAe;AAAA,MACpC,cAAc,QAAQ,gBAAgB;AAAA,MACtC,uBAAuB,QAAQ,yBAAyB;AAAA,MACxD;AAAA,IACF;AAEA,UAAM,KAAK,OAAO,QAAQ,OAAO;AAAA,MAC/B,OAAO,EAAE,IAAI,QAAQ,GAAG;AAAA,MACxB,QAAQ;AAAA,QACN,IAAI,QAAQ;AAAA,QACZ,GAAG;AAAA,MACL;AAAA,MACA,QAAQ;AAAA,IACV,CAAC;AACD,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,YAAY,IAA0C;AAC1D,UAAM,SAAS,MAAM,KAAK,OAAO,QAAQ,WAAW,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AACrE,QAAI,CAAC,OAAQ,QAAO;AACpB,WAAO,gBAAgB,MAAM;AAAA,EAC/B;AAAA,EAEA,MAAM,cAAc,IAA8B;AAChD,QAAI;AACF,YAAM,KAAK,OAAO,QAAQ,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC;AAClD,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,KAAiC;AACpD,UAAM,KAAK,OAAO,QAAQ,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;AACnE,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,mBAAmB,MAAkC;AACzD,UAAM,UAAU,MAAM,KAAK,OAAO,QAAQ,SAAS;AAAA,MACjD,OAAO,EAAE,YAAY,KAAK;AAAA,IAC5B,CAAC;AACD,WAAO,QAAQ,IAAI,eAAe;AAAA,EACpC;AACF;;;AEnDO,IAAM,sBAAN,MAAoD;AAAA,EAIzD,YACmB,OACjB,SACA;AAFiB;AAGjB,SAAK,SAAS,SAAS,UAAU;AACjC,SAAK,MAAM,SAAS;AAAA,EACtB;AAAA,EATiB;AAAA,EACA;AAAA,EAUjB,MAAM,aAAa,SAAoC;AACrD,UAAM,MAAM,KAAK,WAAW,QAAQ,EAAE;AACtC,UAAM,OAAO,KAAK,UAAU,OAAO;AAEnC,UAAM,WAAW,KAAK,MAAM,SAAS;AACrC,QAAI,KAAK,KAAK;AACZ,eAAS,IAAI,KAAK,MAAM,MAAM,KAAK,GAAG;AAAA,IACxC,OAAO;AACL,eAAS,IAAI,KAAK,IAAI;AAAA,IACxB;AACA,aAAS,KAAK,KAAK,aAAa,QAAQ,IAAI,GAAG,QAAQ,EAAE;AACzD,UAAM,SAAS,KAAK;AACpB,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,YAAY,IAA0C;AAC1D,UAAM,OAAO,MAAM,KAAK,MAAM,IAAI,KAAK,WAAW,EAAE,CAAC;AACrD,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO,KAAK,YAAY,IAAI;AAAA,EAC9B;AAAA,EAEA,MAAM,cAAc,IAA8B;AAChD,UAAM,UAAU,MAAM,KAAK,YAAY,EAAE;AACzC,UAAM,WAAW,KAAK,MAAM,SAAS;AACrC,aAAS,IAAI,KAAK,WAAW,EAAE,CAAC;AAChC,QAAI,SAAS;AACX,eAAS,KAAK,KAAK,aAAa,QAAQ,IAAI,GAAG,EAAE;AAAA,IACnD;AACA,UAAM,SAAS,KAAK;AACpB,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,eAAe,KAAiC;AACpD,QAAI,IAAI,WAAW,EAAG,QAAO;AAG7B,UAAM,WAAW,MAAM,QAAQ,IAAI,IAAI,IAAI,CAAC,OAAO,KAAK,YAAY,EAAE,CAAC,CAAC;AAExE,UAAM,WAAW,KAAK,MAAM,SAAS;AACrC,eAAW,MAAM,KAAK;AACpB,eAAS,IAAI,KAAK,WAAW,EAAE,CAAC;AAAA,IAClC;AACA,eAAW,WAAW,UAAU;AAC9B,UAAI,SAAS;AACX,iBAAS,KAAK,KAAK,aAAa,QAAQ,IAAI,GAAG,QAAQ,EAAE;AAAA,MAC3D;AAAA,IACF;AACA,UAAM,SAAS,KAAK;AACpB,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,mBAAmB,MAAkC;AACzD,UAAM,MAAM,MAAM,KAAK,MAAM,SAAS,KAAK,aAAa,IAAI,CAAC;AAC7D,QAAI,IAAI,WAAW,EAAG,QAAO,CAAC;AAE9B,UAAM,OAAO,IAAI,IAAI,CAAC,OAAO,KAAK,WAAW,EAAE,CAAC;AAChD,UAAM,UAAU,MAAM,KAAK,MAAM,KAAK,GAAG,IAAI;AAE7C,UAAM,WAAsB,CAAC;AAC7B,eAAW,QAAQ,SAAS;AAC1B,UAAI,MAAM;AACR,iBAAS,KAAK,KAAK,YAAY,IAAI,CAAC;AAAA,MACtC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAMQ,WAAW,IAAoB;AACrC,WAAO,GAAG,KAAK,MAAM,GAAG,EAAE;AAAA,EAC5B;AAAA,EAEQ,aAAa,YAA4B;AAC/C,WAAO,GAAG,KAAK,MAAM,QAAQ,UAAU;AAAA,EACzC;AAAA,EAEQ,UAAU,SAA0B;AAC1C,WAAO,KAAK,UAAU;AAAA,MACpB,GAAG;AAAA,MACH,SAAS,QAAQ,SAAS,YAAY,KAAK;AAAA,MAC3C,uBAAuB,QAAQ,uBAAuB,YAAY;AAAA,MAClE,WAAW,QAAQ,UAAU,YAAY;AAAA,MACzC,WAAW,QAAQ,UAAU,YAAY;AAAA,IAC3C,CAAC;AAAA,EACH;AAAA,EAEQ,YAAY,MAAuB;AACzC,UAAM,MAAM,KAAK,MAAM,IAAI;AAE3B,UAAM,UAAmB;AAAA,MACvB,IAAI,IAAI;AAAA,MACR,MAAM,IAAI;AAAA,MACV,OAAO,IAAI;AAAA,MACX,UAAU,IAAI;AAAA,MACd,OAAO,IAAI;AAAA,MACX,SAAS,IAAI,UAAU,IAAI,KAAK,IAAI,OAAiB,IAAI;AAAA,MACzD,WAAW,IAAI,KAAK,IAAI,SAAmB;AAAA,MAC3C,WAAW,IAAI,KAAK,IAAI,SAAmB;AAAA,IAC7C;AAEA,QAAI,IAAI,aAAa;AACnB,cAAQ,cAAc,IAAI;AAAA,IAC5B;AACA,QAAI,IAAI,cAAc;AACpB,cAAQ,eAAe,IAAI;AAAA,IAC7B;AACA,QAAI,IAAI,uBAAuB;AAC7B,cAAQ,wBAAwB,IAAI,KAAK,IAAI,qBAA+B;AAAA,IAC9E;AACA,QAAI,IAAI,kBAAkB;AACxB,cAAQ,mBAAmB,IAAI;AAAA,IACjC;AAEA,WAAO;AAAA,EACT;AACF;;;AC1IO,IAAM,qBAAN,MAAmD;AAAA,EACxD,YACmB,SACA,UACjB;AAFiB;AACA;AAAA,EAChB;AAAA,EAEH,MAAM,aAAa,SAAoC;AACrD,UAAM,CAAC,WAAW,UAAU,IAAI,MAAM,QAAQ,IAAI;AAAA,MAChD,KAAK,QAAQ,aAAa,OAAO;AAAA,MACjC,KAAK,SAAS,aAAa,OAAO;AAAA,IACpC,CAAC;AACD,WAAO,aAAa;AAAA,EACtB;AAAA,EAEA,MAAM,YAAY,IAA0C;AAC1D,UAAM,UAAU,MAAM,KAAK,QAAQ,YAAY,EAAE;AACjD,QAAI,QAAS,QAAO;AAEpB,UAAM,kBAAkB,MAAM,KAAK,SAAS,YAAY,EAAE;AAC1D,QAAI,iBAAiB;AAEnB,YAAM,KAAK,QAAQ,aAAa,eAAe;AAAA,IACjD;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,cAAc,IAA8B;AAChD,UAAM,CAAC,WAAW,UAAU,IAAI,MAAM,QAAQ,IAAI;AAAA,MAChD,KAAK,QAAQ,cAAc,EAAE;AAAA,MAC7B,KAAK,SAAS,cAAc,EAAE;AAAA,IAChC,CAAC;AACD,WAAO,aAAa;AAAA,EACtB;AAAA,EAEA,MAAM,eAAe,KAAiC;AACpD,UAAM,CAAC,WAAW,UAAU,IAAI,MAAM,QAAQ,IAAI;AAAA,MAChD,KAAK,QAAQ,eAAe,GAAG;AAAA,MAC/B,KAAK,SAAS,eAAe,GAAG;AAAA,IAClC,CAAC;AACD,WAAO,aAAa;AAAA,EACtB;AAAA,EAEA,MAAM,mBAAmB,MAAkC;AACzD,UAAM,WAAW,MAAM,KAAK,QAAQ,mBAAmB,IAAI;AAC3D,QAAI,SAAS,SAAS,EAAG,QAAO;AAChC,WAAO,KAAK,SAAS,mBAAmB,IAAI;AAAA,EAC9C;AACF;","names":[]}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { WebhookValidator, WebhookQueueConfig, WebhookQueue, WebhookHandler, GdprHandler } from '@uniforge/platform-core/webhooks';
|
|
2
|
+
import Redis from 'ioredis';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Webhook HMAC validation.
|
|
6
|
+
*
|
|
7
|
+
* Validates incoming Shopify webhook requests by computing
|
|
8
|
+
* HMAC-SHA256 of the raw body and comparing with the header value.
|
|
9
|
+
* Uses base64 encoding (Shopify webhooks use base64, not hex).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
declare function createWebhookValidator(): WebhookValidator;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Redis-backed webhook queue.
|
|
16
|
+
*
|
|
17
|
+
* Stores webhook jobs in Redis with sorted sets for ordered processing.
|
|
18
|
+
* Supports exponential backoff retries and dead letter queue.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
declare function createWebhookQueue(redis: Redis, config?: WebhookQueueConfig): WebhookQueue;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Webhook handler registry.
|
|
25
|
+
*
|
|
26
|
+
* A simple in-memory registry mapping webhook topics to their handlers.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
interface WebhookRegistry {
|
|
30
|
+
register(topic: string, handler: WebhookHandler): void;
|
|
31
|
+
unregister(topic: string): void;
|
|
32
|
+
getHandler(topic: string): WebhookHandler | undefined;
|
|
33
|
+
listTopics(): string[];
|
|
34
|
+
}
|
|
35
|
+
declare function createWebhookRegistry(): WebhookRegistry;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Webhook queue processor.
|
|
39
|
+
*
|
|
40
|
+
* Polls the webhook queue for pending jobs and dispatches them
|
|
41
|
+
* to the appropriate handler. Handles success/failure and requeue.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
interface WebhookProcessorConfig {
|
|
45
|
+
pollIntervalMs?: number;
|
|
46
|
+
}
|
|
47
|
+
interface WebhookProcessor {
|
|
48
|
+
processNext(): Promise<boolean>;
|
|
49
|
+
start(): void;
|
|
50
|
+
stop(): void;
|
|
51
|
+
}
|
|
52
|
+
declare function createWebhookProcessor(queue: WebhookQueue, handlers: Map<string, WebhookHandler> | WebhookRegistry, config?: WebhookProcessorConfig): WebhookProcessor;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* GDPR webhook helpers.
|
|
56
|
+
*
|
|
57
|
+
* Wraps a GdprHandler implementation into standard WebhookHandler
|
|
58
|
+
* entries that can be registered with the webhook registry.
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
declare function createGdprHandlers(handler: GdprHandler): Map<string, WebhookHandler>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Webhook middleware.
|
|
65
|
+
*
|
|
66
|
+
* Processes incoming webhook HTTP requests: validates HMAC,
|
|
67
|
+
* extracts headers, and either enqueues for async processing
|
|
68
|
+
* or dispatches synchronously to the handler.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
interface WebhookRequest {
|
|
72
|
+
rawBody: Buffer;
|
|
73
|
+
headers: Record<string, string>;
|
|
74
|
+
}
|
|
75
|
+
interface WebhookMiddlewareResult {
|
|
76
|
+
statusCode: 200 | 401 | 404 | 500;
|
|
77
|
+
body?: string;
|
|
78
|
+
}
|
|
79
|
+
interface WebhookMiddlewareConfig {
|
|
80
|
+
validator: WebhookValidator;
|
|
81
|
+
secret: string;
|
|
82
|
+
registry: WebhookRegistry;
|
|
83
|
+
queue?: WebhookQueue;
|
|
84
|
+
}
|
|
85
|
+
declare function createWebhookMiddleware(config: WebhookMiddlewareConfig): {
|
|
86
|
+
processWebhook(request: WebhookRequest): Promise<WebhookMiddlewareResult>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export { type WebhookMiddlewareConfig, type WebhookMiddlewareResult, type WebhookProcessor, type WebhookProcessorConfig, type WebhookRegistry, type WebhookRequest, createGdprHandlers, createWebhookMiddleware, createWebhookProcessor, createWebhookQueue, createWebhookRegistry, createWebhookValidator };
|