@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.
Files changed (113) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +18 -0
  3. package/dist/src/lib/posthog.js +7 -0
  4. package/dist/src/lib/sentry.js +9 -0
  5. package/dist/src/modules/access/access.repository.js +32 -0
  6. package/dist/src/modules/access/access.service.js +51 -0
  7. package/dist/src/modules/access/access.test.js +182 -0
  8. package/dist/src/modules/access/access.utils.js +20 -0
  9. package/dist/src/modules/ai/ai.db.js +39 -0
  10. package/dist/src/modules/ai/ai.prompt.js +30 -0
  11. package/dist/src/modules/ai/ai.repository.js +26 -0
  12. package/dist/src/modules/ai/ai.router.js +132 -0
  13. package/dist/src/modules/ai/ai.service.js +207 -0
  14. package/dist/src/modules/ai/ai.trpc.d.ts +5 -5
  15. package/dist/src/modules/ai/ai.trpc.js +20 -0
  16. package/dist/src/modules/ai/ideogram/ideogram.constants.js +167 -0
  17. package/dist/src/modules/ai/ideogram/ideogram.dto.js +49 -0
  18. package/dist/src/modules/ai/ideogram/ideogram.prompt.js +860 -0
  19. package/dist/src/modules/ai/ideogram/ideogram.repository.js +46 -0
  20. package/dist/src/modules/ai/ideogram/ideogram.service.js +11 -0
  21. package/dist/src/modules/auth/auth.db.js +215 -0
  22. package/dist/src/modules/auth/auth.dto.js +38 -0
  23. package/dist/src/modules/auth/auth.lib.d.ts +4 -4
  24. package/dist/src/modules/auth/auth.lib.js +284 -0
  25. package/dist/src/modules/auth/auth.middleware.js +52 -0
  26. package/dist/src/modules/auth/auth.repository.js +541 -0
  27. package/dist/src/modules/auth/auth.service.js +201 -0
  28. package/dist/src/modules/auth/auth.trpc.d.ts +18 -18
  29. package/dist/src/modules/auth/auth.trpc.js +157 -0
  30. package/dist/src/modules/auth/auth.utils.js +97 -0
  31. package/dist/src/modules/base/base.abstract.js +53 -0
  32. package/dist/src/modules/base/base.dto.js +112 -0
  33. package/dist/src/modules/base/base.grants.js +123 -0
  34. package/dist/src/modules/base/base.grants.test.js +668 -0
  35. package/dist/src/modules/base/base.repository.js +307 -0
  36. package/dist/src/modules/base/base.service.js +109 -0
  37. package/dist/src/modules/base/base.types.js +2 -0
  38. package/dist/src/modules/billing/billing.db.js +29 -0
  39. package/dist/src/modules/billing/billing.repository.js +235 -0
  40. package/dist/src/modules/billing/billing.router.js +56 -0
  41. package/dist/src/modules/billing/billing.service.js +147 -0
  42. package/dist/src/modules/billing/billing.trpc.d.ts +5 -5
  43. package/dist/src/modules/billing/billing.trpc.js +17 -0
  44. package/dist/src/modules/clay/clay.repository.js +26 -0
  45. package/dist/src/modules/clay/clay.service.js +24 -0
  46. package/dist/src/modules/connect/connect.db.js +30 -0
  47. package/dist/src/modules/connect/connect.dto.js +36 -0
  48. package/dist/src/modules/connect/connect.linkedin.js +53 -0
  49. package/dist/src/modules/connect/connect.oauth.js +198 -0
  50. package/dist/src/modules/connect/connect.repository.d.ts +7 -7
  51. package/dist/src/modules/connect/connect.repository.js +54 -0
  52. package/dist/src/modules/connect/connect.router.js +54 -0
  53. package/dist/src/modules/connect/connect.service.d.ts +14 -14
  54. package/dist/src/modules/connect/connect.service.js +114 -0
  55. package/dist/src/modules/connect/connect.trpc.d.ts +10 -10
  56. package/dist/src/modules/connect/connect.trpc.js +21 -0
  57. package/dist/src/modules/connect/connect.types.js +2 -0
  58. package/dist/src/modules/crypto/crypto.db.js +17 -0
  59. package/dist/src/modules/crypto/crypto.repository.js +10 -0
  60. package/dist/src/modules/crypto/crypto.service.js +52 -0
  61. package/dist/src/modules/email/email.service.js +107 -0
  62. package/dist/src/modules/file/file.repository.js +79 -0
  63. package/dist/src/modules/file/file.router.js +99 -0
  64. package/dist/src/modules/file/file.service.js +150 -0
  65. package/dist/src/modules/recurrence/recurrence.db.js +66 -0
  66. package/dist/src/modules/recurrence/recurrence.repository.js +39 -0
  67. package/dist/src/modules/recurrence/recurrence.service.js +70 -0
  68. package/dist/src/modules/recurrence/recurrence.trpc.d.ts +15 -15
  69. package/dist/src/modules/recurrence/recurrence.trpc.js +65 -0
  70. package/dist/src/modules/social/social.dto.js +18 -0
  71. package/dist/src/modules/social/social.linkedin.js +427 -0
  72. package/dist/src/modules/social/social.linkedin.test.js +235 -0
  73. package/dist/src/modules/social/social.service.js +76 -0
  74. package/dist/src/modules/social/social.types.js +2 -0
  75. package/dist/src/modules/tag/tag.db.js +42 -0
  76. package/dist/src/modules/tag/tag.dto.js +9 -0
  77. package/dist/src/modules/tag/tag.repository.js +154 -0
  78. package/dist/src/modules/tag/tag.service.js +31 -0
  79. package/dist/src/modules/tag/tag.trpc.d.ts +5 -5
  80. package/dist/src/modules/tag/tag.trpc.js +47 -0
  81. package/dist/src/modules/utils/applyPagination.js +16 -0
  82. package/dist/src/modules/utils/applySorting.js +18 -0
  83. package/dist/src/modules/utils/getConditionsFromFilters.js +200 -0
  84. package/dist/src/modules/video/video.service.js +84 -0
  85. package/dist/src/modules/webhook/webhook.constants.js +10 -0
  86. package/dist/src/modules/webhook/webhook.db.js +17 -0
  87. package/dist/src/modules/webhook/webhook.dto.js +7 -0
  88. package/dist/src/modules/webhook/webhook.repository.js +56 -0
  89. package/dist/src/modules/webhook/webhook.router.js +30 -0
  90. package/dist/src/modules/webhook/webhook.service.js +68 -0
  91. package/dist/src/modules/workflow/workflow.db.js +30 -0
  92. package/dist/src/modules/workflow/workflow.repository.js +105 -0
  93. package/dist/src/modules/workflow/workflow.service.js +37 -0
  94. package/dist/src/modules/workflow/workflow.trpc.d.ts +5 -5
  95. package/dist/src/modules/workflow/workflow.trpc.js +21 -0
  96. package/dist/src/modules/workflow/workflow.types.js +2 -0
  97. package/dist/src/modules/workflow/workflow.utils.js +173 -0
  98. package/dist/src/test/stubs/utils.js +5 -0
  99. package/dist/src/trpc/context.d.ts +5 -5
  100. package/dist/src/trpc/context.js +17 -0
  101. package/dist/src/trpc/index.js +6 -0
  102. package/dist/src/trpc/procedures.d.ts +56 -56
  103. package/dist/src/trpc/procedures.js +32 -0
  104. package/dist/src/trpc/utils.js +20 -0
  105. package/dist/src/types.d.ts +33 -33
  106. package/dist/src/types.js +13 -0
  107. package/dist/src/utils/errors.js +104 -0
  108. package/dist/src/utils/logger.js +11 -0
  109. package/dist/src/utils/posthog.js +31 -0
  110. package/dist/src/utils/types.js +2 -0
  111. package/dist/tsconfig.tsbuildinfo +1 -1
  112. package/package.json +3 -3
  113. 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 {};