@m5kdev/backend 0.1.1 → 0.1.3
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +18 -0
- package/dist/src/lib/posthog.js +7 -0
- package/dist/src/lib/sentry.js +9 -0
- package/dist/src/modules/access/access.repository.js +32 -0
- package/dist/src/modules/access/access.service.js +51 -0
- package/dist/src/modules/access/access.test.js +182 -0
- package/dist/src/modules/access/access.utils.js +20 -0
- package/dist/src/modules/ai/ai.db.js +39 -0
- package/dist/src/modules/ai/ai.prompt.js +30 -0
- package/dist/src/modules/ai/ai.repository.js +26 -0
- package/dist/src/modules/ai/ai.router.js +132 -0
- package/dist/src/modules/ai/ai.service.js +207 -0
- package/dist/src/modules/ai/ai.trpc.d.ts +5 -5
- package/dist/src/modules/ai/ai.trpc.js +20 -0
- package/dist/src/modules/ai/ideogram/ideogram.constants.js +167 -0
- package/dist/src/modules/ai/ideogram/ideogram.dto.js +49 -0
- package/dist/src/modules/ai/ideogram/ideogram.prompt.js +860 -0
- package/dist/src/modules/ai/ideogram/ideogram.repository.js +46 -0
- package/dist/src/modules/ai/ideogram/ideogram.service.js +11 -0
- package/dist/src/modules/auth/auth.db.js +215 -0
- package/dist/src/modules/auth/auth.dto.js +38 -0
- package/dist/src/modules/auth/auth.lib.d.ts +4 -4
- package/dist/src/modules/auth/auth.lib.js +284 -0
- package/dist/src/modules/auth/auth.middleware.js +52 -0
- package/dist/src/modules/auth/auth.repository.js +541 -0
- package/dist/src/modules/auth/auth.service.js +201 -0
- package/dist/src/modules/auth/auth.trpc.d.ts +18 -18
- package/dist/src/modules/auth/auth.trpc.js +157 -0
- package/dist/src/modules/auth/auth.utils.js +97 -0
- package/dist/src/modules/base/base.abstract.js +53 -0
- package/dist/src/modules/base/base.dto.js +112 -0
- package/dist/src/modules/base/base.grants.js +123 -0
- package/dist/src/modules/base/base.grants.test.js +668 -0
- package/dist/src/modules/base/base.repository.js +307 -0
- package/dist/src/modules/base/base.service.js +109 -0
- package/dist/src/modules/base/base.types.js +2 -0
- package/dist/src/modules/billing/billing.db.js +29 -0
- package/dist/src/modules/billing/billing.repository.js +235 -0
- package/dist/src/modules/billing/billing.router.js +56 -0
- package/dist/src/modules/billing/billing.service.js +147 -0
- package/dist/src/modules/billing/billing.trpc.d.ts +5 -5
- package/dist/src/modules/billing/billing.trpc.js +17 -0
- package/dist/src/modules/clay/clay.repository.js +26 -0
- package/dist/src/modules/clay/clay.service.js +24 -0
- package/dist/src/modules/connect/connect.db.js +30 -0
- package/dist/src/modules/connect/connect.dto.js +36 -0
- package/dist/src/modules/connect/connect.linkedin.js +53 -0
- package/dist/src/modules/connect/connect.oauth.js +198 -0
- package/dist/src/modules/connect/connect.repository.d.ts +7 -7
- package/dist/src/modules/connect/connect.repository.js +54 -0
- package/dist/src/modules/connect/connect.router.js +54 -0
- package/dist/src/modules/connect/connect.service.d.ts +14 -14
- package/dist/src/modules/connect/connect.service.js +114 -0
- package/dist/src/modules/connect/connect.trpc.d.ts +10 -10
- package/dist/src/modules/connect/connect.trpc.js +21 -0
- package/dist/src/modules/connect/connect.types.js +2 -0
- package/dist/src/modules/crypto/crypto.db.js +17 -0
- package/dist/src/modules/crypto/crypto.repository.js +10 -0
- package/dist/src/modules/crypto/crypto.service.js +52 -0
- package/dist/src/modules/email/email.service.js +107 -0
- package/dist/src/modules/file/file.repository.js +79 -0
- package/dist/src/modules/file/file.router.js +99 -0
- package/dist/src/modules/file/file.service.js +150 -0
- package/dist/src/modules/recurrence/recurrence.db.js +66 -0
- package/dist/src/modules/recurrence/recurrence.repository.js +39 -0
- package/dist/src/modules/recurrence/recurrence.service.js +70 -0
- package/dist/src/modules/recurrence/recurrence.trpc.d.ts +15 -15
- package/dist/src/modules/recurrence/recurrence.trpc.js +65 -0
- package/dist/src/modules/social/social.dto.js +18 -0
- package/dist/src/modules/social/social.linkedin.js +427 -0
- package/dist/src/modules/social/social.linkedin.test.js +235 -0
- package/dist/src/modules/social/social.service.js +76 -0
- package/dist/src/modules/social/social.types.js +2 -0
- package/dist/src/modules/tag/tag.db.js +42 -0
- package/dist/src/modules/tag/tag.dto.js +9 -0
- package/dist/src/modules/tag/tag.repository.js +154 -0
- package/dist/src/modules/tag/tag.service.js +31 -0
- package/dist/src/modules/tag/tag.trpc.d.ts +5 -5
- package/dist/src/modules/tag/tag.trpc.js +47 -0
- package/dist/src/modules/utils/applyPagination.js +16 -0
- package/dist/src/modules/utils/applySorting.js +18 -0
- package/dist/src/modules/utils/getConditionsFromFilters.js +200 -0
- package/dist/src/modules/video/video.service.js +84 -0
- package/dist/src/modules/webhook/webhook.constants.js +10 -0
- package/dist/src/modules/webhook/webhook.db.js +17 -0
- package/dist/src/modules/webhook/webhook.dto.js +7 -0
- package/dist/src/modules/webhook/webhook.repository.js +56 -0
- package/dist/src/modules/webhook/webhook.router.js +30 -0
- package/dist/src/modules/webhook/webhook.service.js +68 -0
- package/dist/src/modules/workflow/workflow.db.js +30 -0
- package/dist/src/modules/workflow/workflow.repository.js +105 -0
- package/dist/src/modules/workflow/workflow.service.js +37 -0
- package/dist/src/modules/workflow/workflow.trpc.d.ts +5 -5
- package/dist/src/modules/workflow/workflow.trpc.js +21 -0
- package/dist/src/modules/workflow/workflow.types.js +2 -0
- package/dist/src/modules/workflow/workflow.utils.js +173 -0
- package/dist/src/test/stubs/utils.js +5 -0
- package/dist/src/trpc/context.d.ts +5 -5
- package/dist/src/trpc/context.js +17 -0
- package/dist/src/trpc/index.js +6 -0
- package/dist/src/trpc/procedures.d.ts +56 -56
- package/dist/src/trpc/procedures.js +32 -0
- package/dist/src/trpc/utils.js +20 -0
- package/dist/src/types.d.ts +33 -33
- package/dist/src/types.js +13 -0
- package/dist/src/utils/errors.js +104 -0
- package/dist/src/utils/logger.js +11 -0
- package/dist/src/utils/posthog.js +31 -0
- package/dist/src/utils/types.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/tsconfig.json +2 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createBillingRouter = createBillingRouter;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const body_parser_1 = tslib_1.__importDefault(require("body-parser"));
|
|
6
|
+
const express_1 = require("express");
|
|
7
|
+
function createBillingRouter(authMiddleware, service) {
|
|
8
|
+
const billingRouter = (0, express_1.Router)();
|
|
9
|
+
billingRouter.get("/checkout/:priceId", authMiddleware, async (req, res) => {
|
|
10
|
+
const user = req.user;
|
|
11
|
+
const session = await service.createCheckoutSession({ priceId: req.params.priceId }, { user });
|
|
12
|
+
if (session.isErr()) {
|
|
13
|
+
return res.status(500).json({ message: session.error.message });
|
|
14
|
+
}
|
|
15
|
+
if (!session.value.url) {
|
|
16
|
+
return res.status(500).json({ message: "Failed to create checkout session" });
|
|
17
|
+
}
|
|
18
|
+
return res.redirect(session.value.url);
|
|
19
|
+
});
|
|
20
|
+
billingRouter.get("/portal", authMiddleware, async (req, res) => {
|
|
21
|
+
const user = req.user;
|
|
22
|
+
const session = await service.createBillingPortalSession({ user });
|
|
23
|
+
if (session.isErr()) {
|
|
24
|
+
return res.status(500).json({ message: session.error.message });
|
|
25
|
+
}
|
|
26
|
+
return res.redirect(session.value.url);
|
|
27
|
+
});
|
|
28
|
+
billingRouter.get("/success", authMiddleware, async (req, res) => {
|
|
29
|
+
const user = req.user;
|
|
30
|
+
if (!user.stripeCustomerId) {
|
|
31
|
+
return res.redirect(`${process.env.VITE_APP_URL}/billing`);
|
|
32
|
+
}
|
|
33
|
+
const result = await service.syncStripeData(user.stripeCustomerId);
|
|
34
|
+
if (result.isErr()) {
|
|
35
|
+
return res.redirect(`${process.env.VITE_APP_URL}/billing?error=SYNC_FAILED`);
|
|
36
|
+
}
|
|
37
|
+
return res.redirect(`${process.env.VITE_APP_URL}/billing`);
|
|
38
|
+
});
|
|
39
|
+
billingRouter.post("/webhook", body_parser_1.default.raw({ type: "application/json" }), async (req, res) => {
|
|
40
|
+
const signature = req.headers["stripe-signature"];
|
|
41
|
+
if (!signature)
|
|
42
|
+
return res.status(400).json({ message: "No signature" });
|
|
43
|
+
if (typeof signature !== "string")
|
|
44
|
+
return res.status(500).json({ message: "Signature is not a string" });
|
|
45
|
+
const event = service.constructEvent(req.body, signature);
|
|
46
|
+
if (event.isErr()) {
|
|
47
|
+
return res.status(500).json({ message: event.error.message });
|
|
48
|
+
}
|
|
49
|
+
const result = await service.processEvent(event.value);
|
|
50
|
+
if (result.isErr()) {
|
|
51
|
+
return res.status(500).json({ message: result.error.message });
|
|
52
|
+
}
|
|
53
|
+
return res.status(200).json({ received: true });
|
|
54
|
+
});
|
|
55
|
+
return billingRouter;
|
|
56
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BillingService = void 0;
|
|
4
|
+
const neverthrow_1 = require("neverthrow");
|
|
5
|
+
const base_service_1 = require("#modules/base/base.service");
|
|
6
|
+
const posthog_1 = require("#utils/posthog");
|
|
7
|
+
const allowedEvents = [
|
|
8
|
+
"checkout.session.completed",
|
|
9
|
+
"customer.subscription.created",
|
|
10
|
+
"customer.subscription.updated",
|
|
11
|
+
"customer.subscription.deleted",
|
|
12
|
+
"customer.subscription.paused",
|
|
13
|
+
"customer.subscription.resumed",
|
|
14
|
+
"customer.subscription.pending_update_applied",
|
|
15
|
+
"customer.subscription.pending_update_expired",
|
|
16
|
+
"customer.subscription.trial_will_end",
|
|
17
|
+
"invoice.paid",
|
|
18
|
+
"invoice.payment_failed",
|
|
19
|
+
"invoice.payment_action_required",
|
|
20
|
+
"invoice.upcoming",
|
|
21
|
+
"invoice.marked_uncollectible",
|
|
22
|
+
"invoice.payment_succeeded",
|
|
23
|
+
"payment_intent.succeeded",
|
|
24
|
+
"payment_intent.payment_failed",
|
|
25
|
+
"payment_intent.canceled",
|
|
26
|
+
];
|
|
27
|
+
class BillingService extends base_service_1.BaseService {
|
|
28
|
+
async createUserCustomer({ user, }) {
|
|
29
|
+
let stripeCustomer = null;
|
|
30
|
+
const existingCustomer = await this.repository.billing.getCustomerByEmail(user.email);
|
|
31
|
+
if (existingCustomer.isErr())
|
|
32
|
+
return (0, neverthrow_1.err)(existingCustomer.error);
|
|
33
|
+
stripeCustomer = existingCustomer.value;
|
|
34
|
+
if (!stripeCustomer) {
|
|
35
|
+
const newCustomer = await this.repository.billing.createCustomer({
|
|
36
|
+
email: user.email,
|
|
37
|
+
name: user.name,
|
|
38
|
+
userId: user.id,
|
|
39
|
+
});
|
|
40
|
+
if (newCustomer.isErr())
|
|
41
|
+
return (0, neverthrow_1.err)(newCustomer.error);
|
|
42
|
+
stripeCustomer = newCustomer.value;
|
|
43
|
+
}
|
|
44
|
+
if (!stripeCustomer)
|
|
45
|
+
return this.error("INTERNAL_SERVER_ERROR", "Failed to create or get stripe customer");
|
|
46
|
+
const updatedUser = await this.repository.billing.updateUserCustomerId({
|
|
47
|
+
userId: user.id,
|
|
48
|
+
customerId: stripeCustomer.id,
|
|
49
|
+
});
|
|
50
|
+
if (updatedUser.isErr())
|
|
51
|
+
return (0, neverthrow_1.err)(updatedUser.error);
|
|
52
|
+
return (0, neverthrow_1.ok)(stripeCustomer);
|
|
53
|
+
}
|
|
54
|
+
async createUserHook({ user, }) {
|
|
55
|
+
const stripeCustomer = await this.createUserCustomer({ user });
|
|
56
|
+
if (stripeCustomer.isErr())
|
|
57
|
+
return (0, neverthrow_1.err)(stripeCustomer.error);
|
|
58
|
+
if (this.repository.billing.hasTrial()) {
|
|
59
|
+
const existingSubscription = await this.repository.billing.getLatestSubscription(user.id);
|
|
60
|
+
if (existingSubscription.isErr())
|
|
61
|
+
return (0, neverthrow_1.err)(existingSubscription.error);
|
|
62
|
+
if (!existingSubscription.value) {
|
|
63
|
+
const subscription = await this.repository.billing.createTrialSubscription(stripeCustomer.value.id);
|
|
64
|
+
if (subscription.isErr())
|
|
65
|
+
return (0, neverthrow_1.err)(subscription.error);
|
|
66
|
+
}
|
|
67
|
+
const syncResult = await this.syncStripeData(stripeCustomer.value.id);
|
|
68
|
+
if (syncResult.isErr())
|
|
69
|
+
return (0, neverthrow_1.err)(syncResult.error);
|
|
70
|
+
if (syncResult.value === false)
|
|
71
|
+
return this.error("INTERNAL_SERVER_ERROR", "Sync did not create new subscription");
|
|
72
|
+
}
|
|
73
|
+
return (0, neverthrow_1.ok)(true);
|
|
74
|
+
}
|
|
75
|
+
async getActiveSubscription({ user }) {
|
|
76
|
+
return this.repository.billing.getActiveSubscription(user.id);
|
|
77
|
+
}
|
|
78
|
+
async listInvoices({ user }) {
|
|
79
|
+
if (!user.stripeCustomerId)
|
|
80
|
+
return this.error("NOT_FOUND", "User has no stripe customer id");
|
|
81
|
+
return this.repository.billing.listInvoices(user.stripeCustomerId);
|
|
82
|
+
}
|
|
83
|
+
async createCheckoutSession({ priceId }, { user }) {
|
|
84
|
+
let stripeCustomerId = user.stripeCustomerId;
|
|
85
|
+
if (!stripeCustomerId) {
|
|
86
|
+
const stripeCustomer = await this.createUserCustomer({ user });
|
|
87
|
+
if (stripeCustomer.isErr())
|
|
88
|
+
return (0, neverthrow_1.err)(stripeCustomer.error);
|
|
89
|
+
stripeCustomerId = stripeCustomer.value.id;
|
|
90
|
+
}
|
|
91
|
+
return this.repository.billing.createCheckoutSession({
|
|
92
|
+
customerId: stripeCustomerId,
|
|
93
|
+
priceId,
|
|
94
|
+
userId: user.id,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async createBillingPortalSession({ user, }) {
|
|
98
|
+
let stripeCustomerId = user.stripeCustomerId;
|
|
99
|
+
if (!stripeCustomerId) {
|
|
100
|
+
const stripeCustomer = await this.createUserCustomer({ user });
|
|
101
|
+
if (stripeCustomer.isErr())
|
|
102
|
+
return (0, neverthrow_1.err)(stripeCustomer.error);
|
|
103
|
+
stripeCustomerId = stripeCustomer.value.id;
|
|
104
|
+
}
|
|
105
|
+
return this.repository.billing.createBillingPortalSession(stripeCustomerId);
|
|
106
|
+
}
|
|
107
|
+
constructEvent(body, signature) {
|
|
108
|
+
if (!process.env.STRIPE_WEBHOOK_SECRET)
|
|
109
|
+
return this.error("INTERNAL_SERVER_ERROR", "Stripe webhook secret is not set");
|
|
110
|
+
return this.repository.billing.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET);
|
|
111
|
+
}
|
|
112
|
+
async syncStripeData(customerId, eventType) {
|
|
113
|
+
const user = await this.repository.billing.getUserByCustomerId(customerId);
|
|
114
|
+
if (user.isErr())
|
|
115
|
+
return (0, neverthrow_1.err)(user.error);
|
|
116
|
+
if (!user.value)
|
|
117
|
+
return this.error("NOT_FOUND", "User not found");
|
|
118
|
+
if (eventType) {
|
|
119
|
+
(0, posthog_1.posthogCapture)({
|
|
120
|
+
distinctId: user.value.id,
|
|
121
|
+
event: `stripe.${eventType}`,
|
|
122
|
+
properties: {
|
|
123
|
+
customerId,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return this.repository.billing.syncStripeData({ customerId, userId: user.value.id });
|
|
128
|
+
}
|
|
129
|
+
async processEvent(event) {
|
|
130
|
+
return this.throwableAsync(async () => {
|
|
131
|
+
// Skip processing if the event isn't one I'm tracking (list of all events below)
|
|
132
|
+
if (!allowedEvents.includes(event.type))
|
|
133
|
+
return (0, neverthrow_1.ok)(false);
|
|
134
|
+
// All the events I track have a customerId
|
|
135
|
+
const { customer: customerId } = event?.data?.object;
|
|
136
|
+
// This helps make it typesafe and also lets me know if my assumption is wrong
|
|
137
|
+
if (typeof customerId !== "string") {
|
|
138
|
+
return this.error("INTERNAL_SERVER_ERROR", `[STRIPE HOOK] Unexpected event structure: customer ID is not a string. Event type: ${event.type}`);
|
|
139
|
+
}
|
|
140
|
+
const result = await this.syncStripeData(customerId, event.type);
|
|
141
|
+
if (result.isErr())
|
|
142
|
+
return (0, neverthrow_1.err)(result.error);
|
|
143
|
+
return (0, neverthrow_1.ok)(true);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.BillingService = BillingService;
|
|
@@ -4,9 +4,9 @@ export declare function createBillingTRPC(billingService: BillingService): impor
|
|
|
4
4
|
session: {
|
|
5
5
|
id: string;
|
|
6
6
|
userId: string;
|
|
7
|
-
expiresAt: Date;
|
|
8
|
-
createdAt: Date;
|
|
9
7
|
updatedAt: Date;
|
|
8
|
+
createdAt: Date;
|
|
9
|
+
expiresAt: Date;
|
|
10
10
|
token: string;
|
|
11
11
|
ipAddress: string | null;
|
|
12
12
|
userAgent: string | null;
|
|
@@ -18,13 +18,12 @@ export declare function createBillingTRPC(billingService: BillingService): impor
|
|
|
18
18
|
};
|
|
19
19
|
user: {
|
|
20
20
|
name: string;
|
|
21
|
-
image: string | null;
|
|
22
21
|
id: string;
|
|
23
|
-
createdAt: Date;
|
|
24
22
|
updatedAt: Date;
|
|
25
23
|
email: string;
|
|
26
|
-
metadata: Record<string, unknown>;
|
|
27
24
|
emailVerified: boolean;
|
|
25
|
+
image: string | null;
|
|
26
|
+
createdAt: Date;
|
|
28
27
|
role: string | null;
|
|
29
28
|
banned: boolean | null;
|
|
30
29
|
banReason: string | null;
|
|
@@ -34,6 +33,7 @@ export declare function createBillingTRPC(billingService: BillingService): impor
|
|
|
34
33
|
paymentPlanTier: string | null;
|
|
35
34
|
paymentPlanExpiresAt: Date | null;
|
|
36
35
|
preferences: string | null;
|
|
36
|
+
metadata: Record<string, unknown>;
|
|
37
37
|
onboarding: number | null;
|
|
38
38
|
flags: string | null;
|
|
39
39
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createBillingTRPC = createBillingTRPC;
|
|
4
|
+
const billing_schema_1 = require("@m5kdev/commons/modules/billing/billing.schema");
|
|
5
|
+
const _trpc_1 = require("#trpc");
|
|
6
|
+
function createBillingTRPC(billingService) {
|
|
7
|
+
return (0, _trpc_1.router)({
|
|
8
|
+
getActiveSubscription: _trpc_1.procedure
|
|
9
|
+
.output(billing_schema_1.billingSchema.nullable())
|
|
10
|
+
.query(async ({ ctx: { user } }) => {
|
|
11
|
+
return (0, _trpc_1.handleTRPCResult)(await billingService.getActiveSubscription({ user }));
|
|
12
|
+
}),
|
|
13
|
+
listInvoices: _trpc_1.procedure.query(async ({ ctx: { user } }) => {
|
|
14
|
+
return (0, _trpc_1.handleTRPCResult)(await billingService.listInvoices({ user }));
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClayRepository = void 0;
|
|
4
|
+
const neverthrow_1 = require("neverthrow");
|
|
5
|
+
const base_repository_1 = require("#modules/base/base.repository");
|
|
6
|
+
const { CLAY_WEBHOOK_AUTH_TOKEN } = process.env;
|
|
7
|
+
class ClayRepository extends base_repository_1.BaseExternaRepository {
|
|
8
|
+
async sendToWebhook(webhookUrl, row, callbackUrl) {
|
|
9
|
+
return this.throwableAsync(async () => {
|
|
10
|
+
const response = await fetch(webhookUrl, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: {
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
...(CLAY_WEBHOOK_AUTH_TOKEN ? { "x-clay-webhook-auth": CLAY_WEBHOOK_AUTH_TOKEN } : {}),
|
|
15
|
+
},
|
|
16
|
+
body: JSON.stringify({ ...row, callback: callbackUrl }),
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok)
|
|
19
|
+
return this.error("BAD_REQUEST", `HTTP error! status: ${response.status}`, {
|
|
20
|
+
cause: response,
|
|
21
|
+
});
|
|
22
|
+
return (0, neverthrow_1.ok)();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.ClayRepository = ClayRepository;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClayService = void 0;
|
|
4
|
+
const base_service_1 = require("#modules/base/base.service");
|
|
5
|
+
class ClayService extends base_service_1.BaseService {
|
|
6
|
+
tables;
|
|
7
|
+
constructor(repositories, services, tables) {
|
|
8
|
+
super(repositories, services);
|
|
9
|
+
this.tables = tables;
|
|
10
|
+
}
|
|
11
|
+
async waitForResponse(webhookUrl, row, timeoutInSeconds) {
|
|
12
|
+
return await this.service.webhook.waitForRequest((url) => {
|
|
13
|
+
return this.repository.clay.sendToWebhook(webhookUrl, row, url);
|
|
14
|
+
}, timeoutInSeconds);
|
|
15
|
+
}
|
|
16
|
+
async sendToTable(table, row, timeoutInSeconds) {
|
|
17
|
+
const tableData = this.tables[table];
|
|
18
|
+
if (!tableData)
|
|
19
|
+
return this.error("NOT_FOUND", `Table ${table} not found`);
|
|
20
|
+
const response = await this.waitForResponse(tableData.webhookUrl, row, tableData.timeoutInSeconds || timeoutInSeconds);
|
|
21
|
+
return response;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.ClayService = ClayService;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.connect = void 0;
|
|
4
|
+
const sqlite_core_1 = require("drizzle-orm/sqlite-core");
|
|
5
|
+
const uuid_1 = require("uuid");
|
|
6
|
+
exports.connect = (0, sqlite_core_1.sqliteTable)("connect", {
|
|
7
|
+
id: (0, sqlite_core_1.text)("id").primaryKey().$default(uuid_1.v4),
|
|
8
|
+
userId: (0, sqlite_core_1.text)("user_id").notNull(), // FK -> users.id
|
|
9
|
+
provider: (0, sqlite_core_1.text)("provider").notNull(), // e.g. "linkedin"
|
|
10
|
+
accountType: (0, sqlite_core_1.text)("account_type").notNull(), // "user" | "page" | "org" | "channel"
|
|
11
|
+
providerAccountId: (0, sqlite_core_1.text)("provider_account_id").notNull(), // e.g. LinkedIn URN, FB Page ID, IG business acct ID, X user ID
|
|
12
|
+
handle: (0, sqlite_core_1.text)("handle"), // @name or page slug
|
|
13
|
+
displayName: (0, sqlite_core_1.text)("display_name"),
|
|
14
|
+
avatarUrl: (0, sqlite_core_1.text)("avatar_url"),
|
|
15
|
+
// OAuth credentials (ENCRYPTED)
|
|
16
|
+
accessToken: (0, sqlite_core_1.text)("access_token").notNull(),
|
|
17
|
+
refreshToken: (0, sqlite_core_1.text)("refresh_token"), // may be null if provider doesn’t issue refresh tokens
|
|
18
|
+
tokenType: (0, sqlite_core_1.text)("token_type"), // e.g. "bearer"
|
|
19
|
+
scope: (0, sqlite_core_1.text)("scope"), // space- or comma-separated list, for auditing
|
|
20
|
+
expiresAt: (0, sqlite_core_1.integer)("expires_at", { mode: "timestamp" }), // epoch seconds
|
|
21
|
+
// Provider-specific glue
|
|
22
|
+
parentId: (0, sqlite_core_1.text)("parent_id"), // e.g. FB Page’s connected IG business account, or org URN
|
|
23
|
+
metadataJson: (0, sqlite_core_1.text)("metadata_json", { mode: "json" }), // JSON string for extras (region, perms, etc.)
|
|
24
|
+
revokedAt: (0, sqlite_core_1.integer)("revoked_at", { mode: "timestamp" }),
|
|
25
|
+
lastRefreshedAt: (0, sqlite_core_1.integer)("last_refreshed_at", { mode: "timestamp" }),
|
|
26
|
+
createdAt: (0, sqlite_core_1.integer)("created_at", { mode: "timestamp" })
|
|
27
|
+
.notNull()
|
|
28
|
+
.$default(() => new Date()),
|
|
29
|
+
updatedAt: (0, sqlite_core_1.integer)("updated_at", { mode: "timestamp" }),
|
|
30
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.connectDeleteOutputSchema = exports.connectDeleteInputSchema = exports.connectListOutputSchema = exports.connectListInputSchema = exports.connectSelectOutputSchema = exports.connectSelectSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
exports.connectSelectSchema = zod_1.z.object({
|
|
6
|
+
id: zod_1.z.string(),
|
|
7
|
+
userId: zod_1.z.string(),
|
|
8
|
+
provider: zod_1.z.string(),
|
|
9
|
+
accountType: zod_1.z.string(),
|
|
10
|
+
providerAccountId: zod_1.z.string(),
|
|
11
|
+
handle: zod_1.z.string().nullish(),
|
|
12
|
+
displayName: zod_1.z.string().nullish(),
|
|
13
|
+
avatarUrl: zod_1.z.string().nullish(),
|
|
14
|
+
accessToken: zod_1.z.string(),
|
|
15
|
+
refreshToken: zod_1.z.string().nullish(),
|
|
16
|
+
tokenType: zod_1.z.string().nullish(),
|
|
17
|
+
scope: zod_1.z.string().nullish(),
|
|
18
|
+
expiresAt: zod_1.z.date().nullish(),
|
|
19
|
+
parentId: zod_1.z.string().nullish(),
|
|
20
|
+
metadataJson: zod_1.z.unknown().nullish(),
|
|
21
|
+
revokedAt: zod_1.z.date().nullish(),
|
|
22
|
+
lastRefreshedAt: zod_1.z.date().nullish(),
|
|
23
|
+
createdAt: zod_1.z.date(),
|
|
24
|
+
updatedAt: zod_1.z.date().nullish(),
|
|
25
|
+
});
|
|
26
|
+
exports.connectSelectOutputSchema = exports.connectSelectSchema.omit({
|
|
27
|
+
accessToken: true,
|
|
28
|
+
refreshToken: true,
|
|
29
|
+
});
|
|
30
|
+
exports.connectListInputSchema = zod_1.z.object({
|
|
31
|
+
providers: zod_1.z.array(zod_1.z.string()).optional(),
|
|
32
|
+
inactive: zod_1.z.boolean().optional(),
|
|
33
|
+
});
|
|
34
|
+
exports.connectListOutputSchema = zod_1.z.array(exports.connectSelectOutputSchema);
|
|
35
|
+
exports.connectDeleteInputSchema = zod_1.z.object({ id: zod_1.z.string() });
|
|
36
|
+
exports.connectDeleteOutputSchema = zod_1.z.object({ id: zod_1.z.string() });
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLinkedInProvider = createLinkedInProvider;
|
|
4
|
+
function createLinkedInProvider() {
|
|
5
|
+
const clientId = process.env.LINKEDIN_CLIENT_ID;
|
|
6
|
+
const clientSecret = process.env.LINKEDIN_CLIENT_SECRET;
|
|
7
|
+
const baseUrl = process.env.VITE_SERVER_URL;
|
|
8
|
+
if (!clientId || !clientSecret || !baseUrl) {
|
|
9
|
+
throw new Error("Missing required LinkedIn OAuth environment variables: LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET, VITE_SERVER_URL");
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
id: "linkedin",
|
|
13
|
+
clientId,
|
|
14
|
+
clientSecret,
|
|
15
|
+
redirectUri: `${baseUrl}/connect/linkedin/callback`,
|
|
16
|
+
// LinkedIn OpenID Connect scopes
|
|
17
|
+
scopes: ["openid", "profile", "w_member_social"],
|
|
18
|
+
// LinkedIn doesn't support PKCE - disable it
|
|
19
|
+
supportsPKCE: false,
|
|
20
|
+
// LinkedIn OpenID Connect endpoints
|
|
21
|
+
issuerConfig: {
|
|
22
|
+
issuer: "https://www.linkedin.com",
|
|
23
|
+
authorization_endpoint: "https://www.linkedin.com/oauth/v2/authorization",
|
|
24
|
+
token_endpoint: "https://www.linkedin.com/oauth/v2/accessToken",
|
|
25
|
+
userinfo_endpoint: "https://api.linkedin.com/v2/userinfo",
|
|
26
|
+
},
|
|
27
|
+
async mapProfile(accessToken) {
|
|
28
|
+
// Use OpenID Connect userinfo endpoint
|
|
29
|
+
const response = await fetch("https://api.linkedin.com/v2/userinfo", {
|
|
30
|
+
headers: {
|
|
31
|
+
Authorization: `Bearer ${accessToken}`,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(`LinkedIn API error: ${response.status} ${response.statusText}`);
|
|
36
|
+
}
|
|
37
|
+
const data = (await response.json());
|
|
38
|
+
return {
|
|
39
|
+
providerAccountId: data.sub,
|
|
40
|
+
displayName: data.name,
|
|
41
|
+
avatarUrl: data.picture,
|
|
42
|
+
accountType: "user",
|
|
43
|
+
metadata: {
|
|
44
|
+
givenName: data.given_name,
|
|
45
|
+
familyName: data.family_name,
|
|
46
|
+
locale: data.locale,
|
|
47
|
+
email: data.email,
|
|
48
|
+
emailVerified: data.email_verified,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateOAuthState = generateOAuthState;
|
|
4
|
+
exports.getOAuthState = getOAuthState;
|
|
5
|
+
exports.createConfiguration = createConfiguration;
|
|
6
|
+
exports.buildAuthorizationUrl = buildAuthorizationUrl;
|
|
7
|
+
exports.exchangeCodeForTokens = exchangeCodeForTokens;
|
|
8
|
+
exports.refreshAccessToken = refreshAccessToken;
|
|
9
|
+
const tslib_1 = require("tslib");
|
|
10
|
+
const client = tslib_1.__importStar(require("openid-client"));
|
|
11
|
+
const logger_1 = require("#utils/logger");
|
|
12
|
+
// In-memory store for OAuth state (keyed by sessionId + provider)
|
|
13
|
+
// In production, consider using Redis with TTL
|
|
14
|
+
const oauthStateStore = new Map();
|
|
15
|
+
const STATE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
16
|
+
function getStateKey(sessionId, provider) {
|
|
17
|
+
return `${sessionId}:${provider}`;
|
|
18
|
+
}
|
|
19
|
+
async function generateOAuthState(sessionId, provider) {
|
|
20
|
+
const state = client.randomState();
|
|
21
|
+
const codeVerifier = client.randomPKCECodeVerifier();
|
|
22
|
+
const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
|
|
23
|
+
const oauthState = {
|
|
24
|
+
state,
|
|
25
|
+
codeVerifier,
|
|
26
|
+
codeChallenge,
|
|
27
|
+
sessionId,
|
|
28
|
+
provider,
|
|
29
|
+
};
|
|
30
|
+
const key = getStateKey(sessionId, provider);
|
|
31
|
+
oauthStateStore.set(key, oauthState);
|
|
32
|
+
// Clean up after TTL
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
oauthStateStore.delete(key);
|
|
35
|
+
}, STATE_TTL_MS);
|
|
36
|
+
return oauthState;
|
|
37
|
+
}
|
|
38
|
+
function getOAuthState(sessionId, provider, state) {
|
|
39
|
+
const key = getStateKey(sessionId, provider);
|
|
40
|
+
const stored = oauthStateStore.get(key);
|
|
41
|
+
if (!stored || stored.state !== state) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
// Clean up after use
|
|
45
|
+
oauthStateStore.delete(key);
|
|
46
|
+
return stored;
|
|
47
|
+
}
|
|
48
|
+
async function createConfiguration(provider) {
|
|
49
|
+
// LinkedIn uses client_secret_post (form-encoded body parameters)
|
|
50
|
+
// The library's ClientSecretPost handles this correctly
|
|
51
|
+
const clientAuth = provider.clientSecret
|
|
52
|
+
? client.ClientSecretPost(provider.clientSecret)
|
|
53
|
+
: client.None();
|
|
54
|
+
if (provider.issuerConfig) {
|
|
55
|
+
// Use manual issuer config (e.g., LinkedIn) - create Configuration directly
|
|
56
|
+
// LinkedIn doesn't support OpenID Connect discovery
|
|
57
|
+
const serverMetadata = {
|
|
58
|
+
issuer: provider.issuerConfig.issuer,
|
|
59
|
+
authorization_endpoint: provider.issuerConfig.authorization_endpoint,
|
|
60
|
+
token_endpoint: provider.issuerConfig.token_endpoint,
|
|
61
|
+
...(provider.issuerConfig.userinfo_endpoint && {
|
|
62
|
+
userinfo_endpoint: provider.issuerConfig.userinfo_endpoint,
|
|
63
|
+
}),
|
|
64
|
+
// LinkedIn JWKS URI for ID token signature verification
|
|
65
|
+
...(provider.id === "linkedin" && {
|
|
66
|
+
jwks_uri: "https://www.linkedin.com/oauth/openid/jwks",
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
const clientMetadata = {
|
|
70
|
+
client_id: provider.clientId,
|
|
71
|
+
...(provider.clientSecret && { client_secret: provider.clientSecret }),
|
|
72
|
+
redirect_uris: [provider.redirectUri],
|
|
73
|
+
};
|
|
74
|
+
return new client.Configuration(serverMetadata, provider.clientId, clientMetadata, clientAuth);
|
|
75
|
+
}
|
|
76
|
+
// Auto-discovery from well-known endpoint
|
|
77
|
+
if (!provider.issuerUrl) {
|
|
78
|
+
throw new Error("Provider must have either issuerConfig or issuerUrl");
|
|
79
|
+
}
|
|
80
|
+
const serverUrl = new URL(provider.issuerUrl);
|
|
81
|
+
return await client.discovery(serverUrl, provider.clientId, undefined, clientAuth);
|
|
82
|
+
}
|
|
83
|
+
async function buildAuthorizationUrl(provider, state) {
|
|
84
|
+
const config = await createConfiguration(provider);
|
|
85
|
+
const parameters = {
|
|
86
|
+
scope: provider.scopes.join(" "),
|
|
87
|
+
state: state.state,
|
|
88
|
+
redirect_uri: provider.redirectUri,
|
|
89
|
+
};
|
|
90
|
+
// Add PKCE parameters only if provider supports it
|
|
91
|
+
if (provider.supportsPKCE !== false) {
|
|
92
|
+
parameters.code_challenge = state.codeChallenge;
|
|
93
|
+
parameters.code_challenge_method = "S256";
|
|
94
|
+
}
|
|
95
|
+
const url = client.buildAuthorizationUrl(config, parameters);
|
|
96
|
+
return url.toString();
|
|
97
|
+
}
|
|
98
|
+
async function exchangeCodeForTokens(provider, code, codeVerifier, redirectUri, state) {
|
|
99
|
+
const logger = logger_1.logger.child({ layer: "exchangeCodeForTokens" });
|
|
100
|
+
try {
|
|
101
|
+
// LinkedIn-specific workaround: Manual token exchange to bypass ID token validation
|
|
102
|
+
// LinkedIn's OpenID Connect ID token has non-standard claim format
|
|
103
|
+
if (provider.id === "linkedin" && provider.issuerConfig) {
|
|
104
|
+
const tokenEndpoint = provider.issuerConfig.token_endpoint;
|
|
105
|
+
const body = new URLSearchParams({
|
|
106
|
+
grant_type: "authorization_code",
|
|
107
|
+
code,
|
|
108
|
+
redirect_uri: provider.redirectUri,
|
|
109
|
+
client_id: provider.clientId,
|
|
110
|
+
client_secret: provider.clientSecret,
|
|
111
|
+
});
|
|
112
|
+
const response = await fetch(tokenEndpoint, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: {
|
|
115
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
116
|
+
},
|
|
117
|
+
body: body.toString(),
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok) {
|
|
120
|
+
const errorData = await response.json().catch(() => ({}));
|
|
121
|
+
throw new Error(`LinkedIn token exchange failed: ${response.status} ${response.statusText} - ${JSON.stringify(errorData)}`);
|
|
122
|
+
}
|
|
123
|
+
const tokenData = (await response.json());
|
|
124
|
+
return {
|
|
125
|
+
accessToken: tokenData.access_token,
|
|
126
|
+
refreshToken: tokenData.refresh_token,
|
|
127
|
+
tokenType: tokenData.token_type || "bearer",
|
|
128
|
+
expiresAt: tokenData.expires_in
|
|
129
|
+
? new Date(Date.now() + tokenData.expires_in * 1000)
|
|
130
|
+
: undefined,
|
|
131
|
+
scope: tokenData.scope,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Standard flow for other providers
|
|
135
|
+
const config = await createConfiguration(provider);
|
|
136
|
+
const currentUrl = new URL(redirectUri);
|
|
137
|
+
currentUrl.searchParams.set("code", code);
|
|
138
|
+
currentUrl.searchParams.set("state", state);
|
|
139
|
+
const checks = {
|
|
140
|
+
expectedState: state,
|
|
141
|
+
};
|
|
142
|
+
// Only include PKCE verifier if provider supports it
|
|
143
|
+
if (provider.supportsPKCE !== false) {
|
|
144
|
+
checks.pkceCodeVerifier = codeVerifier;
|
|
145
|
+
}
|
|
146
|
+
const tokenSet = await client.authorizationCodeGrant(config, currentUrl, checks);
|
|
147
|
+
return {
|
|
148
|
+
accessToken: tokenSet.access_token,
|
|
149
|
+
refreshToken: tokenSet.refresh_token,
|
|
150
|
+
tokenType: tokenSet.token_type,
|
|
151
|
+
expiresAt: tokenSet.expires_in
|
|
152
|
+
? new Date(Date.now() + tokenSet.expires_in * 1000)
|
|
153
|
+
: undefined,
|
|
154
|
+
scope: tokenSet.scope,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
// Enhanced error logging for OAuth issues
|
|
159
|
+
logger.error("Token exchange error", { error, provider: provider.id });
|
|
160
|
+
if (error instanceof Error) {
|
|
161
|
+
const errorMessage = error.message || "Unknown error";
|
|
162
|
+
// Check if this is an ID token validation error for LinkedIn
|
|
163
|
+
// LinkedIn's ID token may have non-standard claim format, but we can still use the access token
|
|
164
|
+
if (provider.id === "linkedin" &&
|
|
165
|
+
errorMessage.includes("JWT claim") &&
|
|
166
|
+
error.code === "OAUTH_JWT_CLAIM_COMPARISON_FAILED") {
|
|
167
|
+
// Try to extract access token from the error response if available
|
|
168
|
+
// This is a workaround for LinkedIn's ID token validation issues
|
|
169
|
+
logger.warn("LinkedIn ID token validation failed, but token exchange may have succeeded. Check if access token is available.", { error: errorMessage });
|
|
170
|
+
// Re-throw for now - we need the access token to continue
|
|
171
|
+
// In a production scenario, you might want to manually parse the token response
|
|
172
|
+
throw new Error(`LinkedIn ID token validation failed: ${errorMessage}. This is a known issue with LinkedIn's OpenID Connect implementation.`);
|
|
173
|
+
}
|
|
174
|
+
// ResponseBodyError from oauth4webapi has a 'cause' property with the error details
|
|
175
|
+
const responseBodyError = error;
|
|
176
|
+
const errorDetails = responseBodyError.cause;
|
|
177
|
+
// Extract error and error_description from LinkedIn's response
|
|
178
|
+
const linkedInError = errorDetails?.error;
|
|
179
|
+
const linkedInErrorDescription = errorDetails?.error_description;
|
|
180
|
+
const fullErrorMessage = linkedInError
|
|
181
|
+
? `LinkedIn OAuth error: ${linkedInError}${linkedInErrorDescription ? ` - ${linkedInErrorDescription}` : ""}`
|
|
182
|
+
: `Token exchange failed: ${errorMessage}${errorDetails ? ` - Details: ${JSON.stringify(errorDetails)}` : ""}`;
|
|
183
|
+
throw new Error(fullErrorMessage);
|
|
184
|
+
}
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function refreshAccessToken(provider, refreshToken) {
|
|
189
|
+
const config = await createConfiguration(provider);
|
|
190
|
+
const tokenSet = await client.refreshTokenGrant(config, refreshToken);
|
|
191
|
+
return {
|
|
192
|
+
accessToken: tokenSet.access_token,
|
|
193
|
+
refreshToken: tokenSet.refresh_token || refreshToken,
|
|
194
|
+
tokenType: tokenSet.token_type,
|
|
195
|
+
expiresAt: tokenSet.expires_in ? new Date(Date.now() + tokenSet.expires_in * 1000) : undefined,
|
|
196
|
+
scope: tokenSet.scope,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -391,23 +391,23 @@ export declare class ConnectRepository extends BaseTableRepository<Orm, Schema,
|
|
|
391
391
|
upsert(data: ConnectInsert, tx?: Orm): Promise<import("../base/base.dto").ServerResult<{
|
|
392
392
|
id: string;
|
|
393
393
|
userId: string;
|
|
394
|
+
updatedAt: Date | null;
|
|
395
|
+
createdAt: Date;
|
|
396
|
+
expiresAt: Date | null;
|
|
397
|
+
accessToken: string;
|
|
398
|
+
refreshToken: string | null;
|
|
399
|
+
scope: string | null;
|
|
394
400
|
provider: string;
|
|
401
|
+
parentId: string | null;
|
|
395
402
|
accountType: string;
|
|
396
403
|
providerAccountId: string;
|
|
397
404
|
handle: string | null;
|
|
398
405
|
displayName: string | null;
|
|
399
406
|
avatarUrl: string | null;
|
|
400
|
-
accessToken: string;
|
|
401
|
-
refreshToken: string | null;
|
|
402
407
|
tokenType: string | null;
|
|
403
|
-
scope: string | null;
|
|
404
|
-
expiresAt: Date | null;
|
|
405
|
-
parentId: string | null;
|
|
406
408
|
metadataJson: unknown;
|
|
407
409
|
revokedAt: Date | null;
|
|
408
410
|
lastRefreshedAt: Date | null;
|
|
409
|
-
createdAt: Date;
|
|
410
|
-
updatedAt: Date | null;
|
|
411
411
|
}>>;
|
|
412
412
|
}
|
|
413
413
|
export {};
|