@almadar/integrations 2.6.3 → 2.8.0

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/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createLogger } from '@almadar/logger';
1
2
  import { integratorsRegistry } from '@almadar/patterns';
2
3
  import Stripe from 'stripe';
3
4
  import { google } from 'googleapis';
@@ -13,32 +14,20 @@ import { tmpdir } from 'os';
13
14
 
14
15
  // src/core/logger.ts
15
16
  var ConsoleLogger = class {
16
- constructor(level = "info") {
17
- this.level = level;
17
+ constructor(_level = "info") {
18
+ this.log = createLogger("almadar:integrations");
18
19
  }
19
20
  debug(message, meta) {
20
- if (this.shouldLog("debug")) {
21
- console.debug(`[DEBUG] ${message}`, meta || "");
22
- }
21
+ this.log.debug(message, meta);
23
22
  }
24
23
  info(message, meta) {
25
- if (this.shouldLog("info")) {
26
- console.log(`[INFO] ${message}`, meta || "");
27
- }
24
+ this.log.info(message, meta);
28
25
  }
29
26
  warn(message, meta) {
30
- if (this.shouldLog("warn")) {
31
- console.warn(`[WARN] ${message}`, meta || "");
32
- }
27
+ this.log.warn(message, meta);
33
28
  }
34
29
  error(message, meta) {
35
- if (this.shouldLog("error")) {
36
- console.error(`[ERROR] ${message}`, meta || "");
37
- }
38
- }
39
- shouldLog(level) {
40
- const levels = ["debug", "info", "warn", "error"];
41
- return levels.indexOf(level) >= levels.indexOf(this.level);
30
+ this.log.error(message, meta);
42
31
  }
43
32
  };
44
33
  function validateParams(integration, action, params) {
@@ -289,6 +278,179 @@ function resetIntegrationFactory() {
289
278
  _factory?.reset();
290
279
  _factory = null;
291
280
  }
281
+ var STRIPE_API_VERSION = "2025-02-24.acacia";
282
+ function priceToTier(priceId, prices) {
283
+ if (priceId === prices.solo) return "solo";
284
+ if (priceId === prices.teams) return "teams";
285
+ return null;
286
+ }
287
+ function mapStatus(status) {
288
+ switch (status) {
289
+ case "active":
290
+ case "past_due":
291
+ case "canceled":
292
+ case "incomplete":
293
+ case "incomplete_expired":
294
+ case "trialing":
295
+ case "unpaid":
296
+ return status;
297
+ case "paused":
298
+ return "unpaid";
299
+ default: {
300
+ return "incomplete";
301
+ }
302
+ }
303
+ }
304
+ function isoFromUnix(seconds) {
305
+ return new Date(seconds * 1e3).toISOString();
306
+ }
307
+ function shapeSubscription(sub, prices) {
308
+ const firstItem = sub.items.data[0];
309
+ const priceId = firstItem?.price.id ?? "";
310
+ const quantity = firstItem?.quantity ?? 1;
311
+ const customerId = typeof sub.customer === "string" ? sub.customer : sub.customer.id;
312
+ return {
313
+ subscriptionId: sub.id,
314
+ customerId,
315
+ status: mapStatus(sub.status),
316
+ priceId,
317
+ quantity,
318
+ currentPeriodStart: isoFromUnix(sub.current_period_start),
319
+ currentPeriodEnd: isoFromUnix(sub.current_period_end),
320
+ cancelAtPeriodEnd: sub.cancel_at_period_end,
321
+ tier: priceToTier(priceId, prices)
322
+ };
323
+ }
324
+ function customerIdOf(customer) {
325
+ if (customer === null) return "";
326
+ return typeof customer === "string" ? customer : customer.id;
327
+ }
328
+ function verifyAndParseStripeEvent(input) {
329
+ const stripe = new Stripe("placeholder-only-for-webhook-utility", {
330
+ apiVersion: STRIPE_API_VERSION
331
+ });
332
+ let event;
333
+ try {
334
+ event = stripe.webhooks.constructEvent(
335
+ input.rawBody,
336
+ input.signature,
337
+ input.secret
338
+ );
339
+ } catch {
340
+ return { error: "bad-signature" };
341
+ }
342
+ switch (event.type) {
343
+ case "customer.subscription.created": {
344
+ const sub = event.data.object;
345
+ return {
346
+ type: "subscription.created",
347
+ eventId: event.id,
348
+ data: shapeSubscription(sub, input.prices)
349
+ };
350
+ }
351
+ case "customer.subscription.updated": {
352
+ const sub = event.data.object;
353
+ return {
354
+ type: "subscription.updated",
355
+ eventId: event.id,
356
+ data: shapeSubscription(sub, input.prices)
357
+ };
358
+ }
359
+ case "customer.subscription.deleted": {
360
+ const sub = event.data.object;
361
+ return {
362
+ type: "subscription.deleted",
363
+ eventId: event.id,
364
+ data: {
365
+ subscriptionId: sub.id,
366
+ customerId: customerIdOf(sub.customer)
367
+ }
368
+ };
369
+ }
370
+ case "invoice.payment_succeeded": {
371
+ const invoice = event.data.object;
372
+ const subRef = invoice.subscription;
373
+ return {
374
+ type: "invoice.payment_succeeded",
375
+ eventId: event.id,
376
+ data: {
377
+ customerId: customerIdOf(invoice.customer),
378
+ subscriptionId: typeof subRef === "string" ? subRef : subRef?.id ?? null,
379
+ paidAt: isoFromUnix(invoice.status_transitions.paid_at ?? invoice.created),
380
+ amountUsd: invoice.amount_paid / 100
381
+ }
382
+ };
383
+ }
384
+ case "invoice.payment_failed": {
385
+ const invoice = event.data.object;
386
+ const subRef = invoice.subscription;
387
+ return {
388
+ type: "invoice.payment_failed",
389
+ eventId: event.id,
390
+ data: {
391
+ customerId: customerIdOf(invoice.customer),
392
+ subscriptionId: typeof subRef === "string" ? subRef : subRef?.id ?? null,
393
+ failedAt: isoFromUnix(invoice.created),
394
+ reason: invoice.last_finalization_error?.message ?? "Payment failed; reason not provided by Stripe."
395
+ }
396
+ };
397
+ }
398
+ default:
399
+ return { error: "unknown-event-type", stripeType: event.type };
400
+ }
401
+ }
402
+
403
+ // src/integrations/stripe/index.ts
404
+ var STRIPE_API_VERSION2 = "2025-02-24.acacia";
405
+ function isoFromUnix2(seconds) {
406
+ return new Date(seconds * 1e3).toISOString();
407
+ }
408
+ function priceToTier2(priceId, prices) {
409
+ if (priceId === prices.solo) return "solo";
410
+ if (priceId === prices.teams) return "teams";
411
+ return null;
412
+ }
413
+ function mapStatus2(status) {
414
+ switch (status) {
415
+ case "active":
416
+ case "past_due":
417
+ case "canceled":
418
+ case "incomplete":
419
+ case "incomplete_expired":
420
+ case "trialing":
421
+ case "unpaid":
422
+ return status;
423
+ case "paused":
424
+ return "unpaid";
425
+ default: {
426
+ return "incomplete";
427
+ }
428
+ }
429
+ }
430
+ function shapeSubscription2(sub, prices) {
431
+ const firstItem = sub.items.data[0];
432
+ const priceId = firstItem?.price.id ?? "";
433
+ const quantity = firstItem?.quantity ?? 1;
434
+ const customerId = typeof sub.customer === "string" ? sub.customer : sub.customer.id;
435
+ return {
436
+ subscriptionId: sub.id,
437
+ customerId,
438
+ status: mapStatus2(sub.status),
439
+ priceId,
440
+ quantity,
441
+ currentPeriodStart: isoFromUnix2(sub.current_period_start),
442
+ currentPeriodEnd: isoFromUnix2(sub.current_period_end),
443
+ cancelAtPeriodEnd: sub.cancel_at_period_end,
444
+ tier: priceToTier2(priceId, prices)
445
+ };
446
+ }
447
+ function shapeCustomer(customer) {
448
+ return {
449
+ customerId: customer.id,
450
+ email: customer.email ?? null,
451
+ almadarUid: customer.metadata?.almadarUid ?? null
452
+ };
453
+ }
292
454
  var StripeIntegration = class extends BaseIntegration {
293
455
  constructor(config) {
294
456
  super(config);
@@ -297,10 +459,18 @@ var StripeIntegration = class extends BaseIntegration {
297
459
  throw new Error("STRIPE_SECRET_KEY not configured");
298
460
  }
299
461
  this.client = new Stripe(apiKey, {
300
- apiVersion: "2025-02-24.acacia"
462
+ apiVersion: STRIPE_API_VERSION2
301
463
  });
464
+ this.prices = {
465
+ solo: config.env.STRIPE_PRICE_SOLO ?? "",
466
+ teams: config.env.STRIPE_PRICE_TEAMS ?? ""
467
+ };
302
468
  this.logger.info("Stripe integration initialized");
303
469
  }
470
+ /** Provisioned Price IDs the integration was constructed with. */
471
+ getPrices() {
472
+ return this.prices;
473
+ }
304
474
  async execute(action, params) {
305
475
  const validation = this.validateParams(action, params);
306
476
  if (!validation.valid) {
@@ -316,7 +486,7 @@ var StripeIntegration = class extends BaseIntegration {
316
486
  };
317
487
  }
318
488
  const startTime = Date.now();
319
- let retries = 0;
489
+ const retries = 0;
320
490
  try {
321
491
  let data;
322
492
  switch (action) {
@@ -345,7 +515,10 @@ var StripeIntegration = class extends BaseIntegration {
345
515
  }
346
516
  async createPaymentIntent(params) {
347
517
  const { amount, currency, metadata } = params;
348
- this.logger.debug("Creating payment intent", { amount: Number(amount), currency: String(currency ?? "") });
518
+ this.logger.debug("Creating payment intent", {
519
+ amount: Number(amount),
520
+ currency: String(currency ?? "")
521
+ });
349
522
  return await this.client.paymentIntents.create({
350
523
  amount,
351
524
  currency,
@@ -354,17 +527,164 @@ var StripeIntegration = class extends BaseIntegration {
354
527
  }
355
528
  async confirmPayment(params) {
356
529
  const { paymentIntentId } = params;
357
- this.logger.debug("Confirming payment", { paymentIntentId: String(paymentIntentId ?? "") });
530
+ this.logger.debug("Confirming payment", {
531
+ paymentIntentId: String(paymentIntentId ?? "")
532
+ });
358
533
  return await this.client.paymentIntents.confirm(paymentIntentId);
359
534
  }
360
535
  async refund(params) {
361
536
  const { paymentIntentId, amount } = params;
362
- this.logger.debug("Creating refund", { paymentIntentId: String(paymentIntentId ?? ""), amount: Number(amount) });
537
+ this.logger.debug("Creating refund", {
538
+ paymentIntentId: String(paymentIntentId ?? ""),
539
+ amount: Number(amount)
540
+ });
363
541
  return await this.client.refunds.create({
364
542
  payment_intent: paymentIntentId,
365
543
  amount
366
544
  });
367
545
  }
546
+ // ───────────────────────────────────────────────────────────────────
547
+ // Typed action surface — canonical Almadar shapes only.
548
+ // ───────────────────────────────────────────────────────────────────
549
+ /** Look up an existing customer by Stripe ID. */
550
+ async getCustomer(customerId) {
551
+ this.logger.debug("Fetching customer", { customerId });
552
+ const customer = await this.client.customers.retrieve(customerId);
553
+ if (customer.deleted === true) return null;
554
+ return shapeCustomer(customer);
555
+ }
556
+ /**
557
+ * Create a Stripe Customer for the given Almadar user. `almadarUid` is
558
+ * stored as Stripe metadata so webhook handlers can resolve back to
559
+ * the right `users/{uid}` document.
560
+ */
561
+ async createCustomer(input) {
562
+ this.logger.debug("Creating customer", { almadarUid: input.almadarUid });
563
+ const customer = await this.client.customers.create({
564
+ email: input.email,
565
+ name: input.displayName ?? void 0,
566
+ metadata: { almadarUid: input.almadarUid }
567
+ });
568
+ return shapeCustomer(customer);
569
+ }
570
+ /**
571
+ * Create a Stripe-hosted Checkout Session for the given tier. Client
572
+ * redirects the user to the returned `url`; on success Stripe fires
573
+ * `customer.subscription.created`, which the apps/builder webhook
574
+ * handler turns into a `users/{uid}.tier` write.
575
+ */
576
+ async createCheckoutSession(input) {
577
+ const price = input.tier === "solo" ? this.prices.solo : this.prices.teams;
578
+ if (!price) {
579
+ throw new Error(`STRIPE_PRICE_${input.tier.toUpperCase()} not configured`);
580
+ }
581
+ this.logger.debug("Creating Checkout session", {
582
+ tier: input.tier,
583
+ quantity: input.quantity,
584
+ hasCustomer: input.customerId !== null
585
+ });
586
+ const session = await this.client.checkout.sessions.create({
587
+ mode: "subscription",
588
+ customer: input.customerId ?? void 0,
589
+ line_items: [{ price, quantity: input.quantity }],
590
+ success_url: input.successUrl,
591
+ cancel_url: input.cancelUrl,
592
+ subscription_data: {
593
+ metadata: input.metadata
594
+ },
595
+ automatic_tax: { enabled: true },
596
+ allow_promotion_codes: true
597
+ });
598
+ return {
599
+ url: session.url ?? "",
600
+ sessionId: session.id,
601
+ customerId: typeof session.customer === "string" ? session.customer : session.customer?.id ?? null
602
+ };
603
+ }
604
+ /** Create a Billing Portal session for self-service plan management. */
605
+ async createBillingPortalSession(input) {
606
+ this.logger.debug("Creating Portal session", { customerId: input.customerId });
607
+ const session = await this.client.billingPortal.sessions.create({
608
+ customer: input.customerId,
609
+ return_url: input.returnUrl
610
+ });
611
+ return {
612
+ url: session.url,
613
+ customerId: input.customerId
614
+ };
615
+ }
616
+ /** Fetch a subscription and shape it into the canonical form. */
617
+ async getSubscription(subscriptionId) {
618
+ this.logger.debug("Fetching subscription", { subscriptionId });
619
+ const sub = await this.client.subscriptions.retrieve(subscriptionId);
620
+ return shapeSubscription2(sub, this.prices);
621
+ }
622
+ /**
623
+ * Create a subscription directly (server-side, no Checkout). Used by
624
+ * P13.3 Solo → Teams upgrade flow.
625
+ */
626
+ async createSubscription(input) {
627
+ const price = input.tier === "solo" ? this.prices.solo : this.prices.teams;
628
+ if (!price) {
629
+ throw new Error(`STRIPE_PRICE_${input.tier.toUpperCase()} not configured`);
630
+ }
631
+ this.logger.debug("Creating subscription", {
632
+ customerId: input.customerId,
633
+ tier: input.tier,
634
+ quantity: input.quantity
635
+ });
636
+ const sub = await this.client.subscriptions.create({
637
+ customer: input.customerId,
638
+ items: [{ price, quantity: input.quantity }],
639
+ metadata: input.metadata,
640
+ proration_behavior: "create_prorations",
641
+ automatic_tax: { enabled: true }
642
+ });
643
+ return shapeSubscription2(sub, this.prices);
644
+ }
645
+ /**
646
+ * Update quantity or cancel-at-period-end. Used for Teams seat resize
647
+ * and Solo → Teams transition.
648
+ */
649
+ async updateSubscription(input) {
650
+ this.logger.debug("Updating subscription", { subscriptionId: input.subscriptionId });
651
+ const update = {
652
+ proration_behavior: "create_prorations"
653
+ };
654
+ if (typeof input.quantity === "number") {
655
+ const existing = await this.client.subscriptions.retrieve(input.subscriptionId);
656
+ const itemId = existing.items.data[0]?.id;
657
+ if (itemId !== void 0) {
658
+ update.items = [{ id: itemId, quantity: input.quantity }];
659
+ }
660
+ }
661
+ if (typeof input.cancelAtPeriodEnd === "boolean") {
662
+ update.cancel_at_period_end = input.cancelAtPeriodEnd;
663
+ }
664
+ const sub = await this.client.subscriptions.update(
665
+ input.subscriptionId,
666
+ update
667
+ );
668
+ return shapeSubscription2(sub, this.prices);
669
+ }
670
+ /**
671
+ * Cancel a subscription. Defaults to `atPeriodEnd: true` so the user
672
+ * keeps access until the current period ends.
673
+ */
674
+ async cancelSubscription(input) {
675
+ this.logger.debug("Canceling subscription", {
676
+ subscriptionId: input.subscriptionId,
677
+ atPeriodEnd: input.atPeriodEnd
678
+ });
679
+ if (input.atPeriodEnd) {
680
+ const sub2 = await this.client.subscriptions.update(input.subscriptionId, {
681
+ cancel_at_period_end: true
682
+ });
683
+ return shapeSubscription2(sub2, this.prices);
684
+ }
685
+ const sub = await this.client.subscriptions.cancel(input.subscriptionId);
686
+ return shapeSubscription2(sub, this.prices);
687
+ }
368
688
  };
369
689
  registerIntegration("stripe", StripeIntegration);
370
690
  var YouTubeIntegration = class extends BaseIntegration {
@@ -2701,6 +3021,6 @@ var DockerIntegration = class extends BaseIntegration {
2701
3021
  };
2702
3022
  registerIntegration("docker", DockerIntegration);
2703
3023
 
2704
- export { BaseIntegration, CLIIntegration, ConsoleLogger, DeepAgentIntegration, DockerIntegration, EmailIntegration, GitHubIntegration, IntegrationFactory, LLMIntegration, OAuthIntegration, OtelIntegration, QueueIntegration, RedisIntegration, StorageIntegration, StripeIntegration, TwilioIntegration, YouTubeIntegration, getIntegration, getIntegrationFactory, getRegisteredIntegrations, isKnownIntegration, registerIntegration, resetIntegrationFactory, validateParams, withRetry };
3024
+ export { BaseIntegration, CLIIntegration, ConsoleLogger, DeepAgentIntegration, DockerIntegration, EmailIntegration, GitHubIntegration, IntegrationFactory, LLMIntegration, OAuthIntegration, OtelIntegration, QueueIntegration, RedisIntegration, StorageIntegration, StripeIntegration, TwilioIntegration, YouTubeIntegration, getIntegration, getIntegrationFactory, getRegisteredIntegrations, isKnownIntegration, registerIntegration, resetIntegrationFactory, validateParams, verifyAndParseStripeEvent, withRetry };
2705
3025
  //# sourceMappingURL=index.js.map
2706
3026
  //# sourceMappingURL=index.js.map