@nymbalhq/platform 0.2.0 → 1.0.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
@@ -391,6 +391,9 @@ var InMemoryDocumentStore = class {
391
391
  this.#sweepTimer = null;
392
392
  }
393
393
  }
394
+ capabilities() {
395
+ return ["get", "put", "delete", "query", "batch", "ttl", "conditional-put"];
396
+ }
394
397
  async get(collection, key) {
395
398
  const entry = this.#data.get(collection)?.get(key);
396
399
  if (!entry) return null;
@@ -632,6 +635,9 @@ var InProcessEventBus = class {
632
635
  constructor(options = {}) {
633
636
  this.#logger = options.logger;
634
637
  }
638
+ capabilities() {
639
+ return ["publish", "subscribe", "wildcard", "replay"];
640
+ }
635
641
  async publish(event) {
636
642
  await this.#deliver(event, () => true);
637
643
  }
@@ -3170,7 +3176,12 @@ async function registerHandlers(deps) {
3170
3176
 
3171
3177
  // src/app.ts
3172
3178
  import { DEFAULT_JWT_SECRET } from "@nymbalhq/config";
3173
- import { ConfigError as ConfigError4 } from "@nymbalhq/types";
3179
+ import { ConfigError as ConfigError6 } from "@nymbalhq/types";
3180
+
3181
+ // src/adapters/registry.ts
3182
+ import {
3183
+ ConfigError as ConfigError5
3184
+ } from "@nymbalhq/types";
3174
3185
 
3175
3186
  // src/adapters/security/headers.ts
3176
3187
  function buildSecurityHeaders(security) {
@@ -3203,7 +3214,7 @@ function createMiddlewareSecurityAdapter(deps) {
3203
3214
  const { logger } = deps;
3204
3215
  let lastAppliedAt = "";
3205
3216
  return {
3206
- capabilities: ["headers"],
3217
+ capabilities: () => ["headers"],
3207
3218
  producesEvents: [],
3208
3219
  consumesEvents: [],
3209
3220
  initialize: noopInitialize,
@@ -3225,220 +3236,17 @@ function createMiddlewareSecurityAdapter(deps) {
3225
3236
  };
3226
3237
  }
3227
3238
 
3228
- // src/adapters/payments/stripe-adapter.ts
3229
- import Stripe from "stripe";
3239
+ // src/adapters/payments/native-stub.ts
3240
+ import { v7 as uuidv73 } from "uuid";
3230
3241
  import {
3231
3242
  EVT_PAYMENT_CAPTURED,
3232
3243
  EVT_PAYMENT_FAILED,
3233
3244
  EVT_PAYMENT_REFUNDED,
3234
3245
  PaymentError
3235
3246
  } from "@nymbalhq/types";
3236
- function toIsoCurrency(currency) {
3237
- return currency.toUpperCase();
3238
- }
3239
- function mapStatus(status) {
3240
- switch (status) {
3241
- case "requires_payment_method":
3242
- case "requires_confirmation":
3243
- case "requires_action":
3244
- case "processing":
3245
- case "requires_capture":
3246
- case "succeeded":
3247
- case "canceled":
3248
- return status === "canceled" ? "cancelled" : status;
3249
- default:
3250
- return "failed";
3251
- }
3252
- }
3253
- function refundStatus(status) {
3254
- if (status === "succeeded") return "succeeded";
3255
- if (status === "failed" || status === "canceled") return "failed";
3256
- return "pending";
3257
- }
3258
- function isDefiniteRefusal(err) {
3259
- const type = err?.type;
3260
- return type === "StripeCardError" || type === "StripeInvalidRequestError";
3261
- }
3262
- function toIntent(intent) {
3263
- return {
3264
- id: intent.id,
3265
- clientSecret: intent.client_secret ?? "",
3266
- amountMinor: intent.amount,
3267
- currency: toIsoCurrency(intent.currency),
3268
- status: mapStatus(intent.status)
3269
- };
3270
- }
3271
- function createStripePaymentsAdapter(deps) {
3272
- const { logger } = deps;
3273
- const cfg = deps.config;
3274
- if (!cfg.secretKey) throw new PaymentError("not_configured", "Stripe secretKey missing");
3275
- if (!cfg.webhookSecret) throw new PaymentError("not_configured", "Stripe webhookSecret missing");
3276
- const stripeClient = new Stripe(cfg.secretKey, {
3277
- ...cfg.apiVersion !== void 0 && {
3278
- apiVersion: cfg.apiVersion
3279
- },
3280
- ...cfg.host !== void 0 && { host: cfg.host },
3281
- ...cfg.port !== void 0 && { port: cfg.port },
3282
- ...cfg.protocol !== void 0 && { protocol: cfg.protocol }
3283
- });
3284
- return {
3285
- kind: "payments",
3286
- providerName: "stripe",
3287
- capabilities: ["intent", "capture", "refund", "webhook"],
3288
- producesEvents: [EVT_PAYMENT_CAPTURED, EVT_PAYMENT_FAILED, EVT_PAYMENT_REFUNDED],
3289
- consumesEvents: [],
3290
- async initialize() {
3291
- },
3292
- async healthCheck() {
3293
- try {
3294
- await stripeClient.balance.retrieve();
3295
- return { status: "healthy" };
3296
- } catch (err) {
3297
- return {
3298
- status: "down",
3299
- detail: err instanceof Error ? err.message : String(err)
3300
- };
3301
- }
3302
- },
3303
- async createPaymentIntent(params) {
3304
- try {
3305
- const intent = await stripeClient.paymentIntents.create({
3306
- amount: params.amountMinor,
3307
- currency: params.currency.toLowerCase(),
3308
- automatic_payment_methods: { enabled: true },
3309
- ...params.receiptEmail !== void 0 && { receipt_email: params.receiptEmail },
3310
- metadata: {
3311
- ...params.metadata ?? {},
3312
- ...params.orderRef !== void 0 ? { orderRef: params.orderRef } : {},
3313
- ...params.customerId !== void 0 ? { customerId: params.customerId } : {}
3314
- }
3315
- });
3316
- return toIntent(intent);
3317
- } catch (err) {
3318
- throw new PaymentError("intent_failed", "Stripe createPaymentIntent failed", { cause: err });
3319
- }
3320
- },
3321
- async capturePayment(intentId, amountMinor) {
3322
- try {
3323
- const captured = await stripeClient.paymentIntents.capture(intentId, {
3324
- ...amountMinor !== void 0 && { amount_to_capture: amountMinor }
3325
- });
3326
- return {
3327
- paymentId: captured.id,
3328
- capturedAmountMinor: captured.amount_received ?? captured.amount,
3329
- currency: toIsoCurrency(captured.currency),
3330
- status: mapStatus(captured.status)
3331
- };
3332
- } catch (err) {
3333
- throw new PaymentError("capture_failed", "Stripe capturePayment failed", { cause: err });
3334
- }
3335
- },
3336
- async refund(paymentId, amountMinor, reason, opts) {
3337
- const params = {
3338
- payment_intent: paymentId,
3339
- ...amountMinor !== void 0 && { amount: amountMinor },
3340
- ...reason !== void 0 && { reason }
3341
- };
3342
- try {
3343
- const refund = opts?.idempotencyKey === void 0 ? await stripeClient.refunds.create(params) : await stripeClient.refunds.create(params, { idempotencyKey: opts.idempotencyKey });
3344
- return {
3345
- refundId: refund.id,
3346
- paymentId,
3347
- amountMinor: refund.amount,
3348
- currency: toIsoCurrency(refund.currency),
3349
- reason: refund.reason ?? null,
3350
- status: refundStatus(refund.status)
3351
- };
3352
- } catch (err) {
3353
- if (isDefiniteRefusal(err)) {
3354
- throw new PaymentError("refund_rejected", "Stripe refused the refund", { cause: err });
3355
- }
3356
- throw new PaymentError("refund_unknown", "Stripe refund outcome unknown", { cause: err });
3357
- }
3358
- },
3359
- async getPaymentStatus(intentId) {
3360
- const intent = await stripeClient.paymentIntents.retrieve(intentId);
3361
- return mapStatus(intent.status);
3362
- },
3363
- async getPaymentIntent(intentId) {
3364
- try {
3365
- return toIntent(await stripeClient.paymentIntents.retrieve(intentId));
3366
- } catch (err) {
3367
- throw new PaymentError("intent_failed", "Stripe getPaymentIntent failed", { cause: err });
3368
- }
3369
- },
3370
- async cancelPaymentIntent(intentId) {
3371
- try {
3372
- await stripeClient.paymentIntents.cancel(intentId);
3373
- } catch (err) {
3374
- const status = await stripeClient.paymentIntents.retrieve(intentId).then((intent) => intent.status).catch(() => void 0);
3375
- if (status === "canceled") return;
3376
- throw new PaymentError("cancel_refused", `Stripe would not cancel ${intentId}`, {
3377
- cause: err
3378
- });
3379
- }
3380
- },
3381
- async parseWebhook(rawBody, signature) {
3382
- let event;
3383
- try {
3384
- event = stripeClient.webhooks.constructEvent(rawBody, signature, cfg.webhookSecret);
3385
- } catch (err) {
3386
- throw new PaymentError("webhook_invalid", "Stripe webhook signature invalid", {
3387
- cause: err
3388
- });
3389
- }
3390
- const baseResult = { verified: true, eventId: event.id };
3391
- switch (event.type) {
3392
- case "payment_intent.succeeded": {
3393
- const intent = event.data.object;
3394
- return {
3395
- ...baseResult,
3396
- kind: "payment.captured",
3397
- paymentIntentId: intent.id,
3398
- amountMinor: intent.amount_received ?? intent.amount,
3399
- currency: toIsoCurrency(intent.currency),
3400
- raw: { ...intent }
3401
- };
3402
- }
3403
- case "payment_intent.payment_failed": {
3404
- const intent = event.data.object;
3405
- return {
3406
- ...baseResult,
3407
- kind: "payment.failed",
3408
- paymentIntentId: intent.id,
3409
- reason: intent.last_payment_error?.message ?? "payment failed"
3410
- };
3411
- }
3412
- case "charge.refunded": {
3413
- const charge = event.data.object;
3414
- const pi = typeof charge.payment_intent === "string" ? charge.payment_intent : charge.payment_intent?.id;
3415
- return {
3416
- ...baseResult,
3417
- kind: "payment.refunded",
3418
- ...pi !== void 0 && { paymentIntentId: pi },
3419
- amountMinor: charge.amount_refunded,
3420
- currency: toIsoCurrency(charge.currency)
3421
- };
3422
- }
3423
- default:
3424
- logger.debug({ type: event.type }, "stripe webhook ignored");
3425
- return { ...baseResult, kind: "ignored" };
3426
- }
3427
- }
3428
- };
3429
- }
3430
-
3431
- // src/adapters/payments/native-stub.ts
3432
- import { v7 as uuidv73 } from "uuid";
3433
- import {
3434
- EVT_PAYMENT_CAPTURED as EVT_PAYMENT_CAPTURED2,
3435
- EVT_PAYMENT_FAILED as EVT_PAYMENT_FAILED2,
3436
- EVT_PAYMENT_REFUNDED as EVT_PAYMENT_REFUNDED2,
3437
- PaymentError as PaymentError2
3438
- } from "@nymbalhq/types";
3439
3247
  var STUB_ID_PREFIX = "pi_stub_";
3440
3248
  function rejected(message) {
3441
- return new PaymentError2("refund_rejected", message);
3249
+ return new PaymentError("refund_rejected", message);
3442
3250
  }
3443
3251
  function statusFor(receiptEmail) {
3444
3252
  const address = receiptEmail ?? "";
@@ -3448,7 +3256,7 @@ function statusFor(receiptEmail) {
3448
3256
  if (local.endsWith("+stub-decline")) return "failed";
3449
3257
  return "succeeded";
3450
3258
  }
3451
- function toIntent2(intent) {
3259
+ function toIntent(intent) {
3452
3260
  return {
3453
3261
  id: intent.id,
3454
3262
  clientSecret: `${intent.id}_secret`,
@@ -3463,8 +3271,9 @@ function createNativeStubPaymentsAdapter(deps) {
3463
3271
  return {
3464
3272
  kind: "payments",
3465
3273
  providerName: "native-stub",
3466
- capabilities: ["intent", "capture", "refund", "webhook"],
3467
- producesEvents: [EVT_PAYMENT_CAPTURED2, EVT_PAYMENT_FAILED2, EVT_PAYMENT_REFUNDED2],
3274
+ // No `webhook`: parseWebhook verifies no signature, so a template cannot rely on it.
3275
+ capabilities: () => ["intent", "capture", "refund"],
3276
+ producesEvents: [EVT_PAYMENT_CAPTURED, EVT_PAYMENT_FAILED, EVT_PAYMENT_REFUNDED],
3468
3277
  consumesEvents: [],
3469
3278
  initialize: noopInitialize,
3470
3279
  healthCheck: () => okHealth("stub \u2014 always healthy"),
@@ -3480,11 +3289,11 @@ function createNativeStubPaymentsAdapter(deps) {
3480
3289
  };
3481
3290
  intents.set(id, intent);
3482
3291
  logger.debug({ intentId: id, status: intent.status }, "stub payment intent created");
3483
- return toIntent2(intent);
3292
+ return toIntent(intent);
3484
3293
  },
3485
3294
  async capturePayment(intentId) {
3486
3295
  const intent = intents.get(intentId);
3487
- if (!intent) throw new PaymentError2("capture_failed", `unknown intent ${intentId}`);
3296
+ if (!intent) throw new PaymentError("capture_failed", `unknown intent ${intentId}`);
3488
3297
  intent.status = "succeeded";
3489
3298
  return {
3490
3299
  paymentId: intent.id,
@@ -3524,14 +3333,14 @@ function createNativeStubPaymentsAdapter(deps) {
3524
3333
  },
3525
3334
  async getPaymentIntent(intentId) {
3526
3335
  const intent = intents.get(intentId);
3527
- if (!intent) throw new PaymentError2("intent_failed", `unknown intent ${intentId}`);
3528
- return toIntent2(intent);
3336
+ if (!intent) throw new PaymentError("intent_failed", `unknown intent ${intentId}`);
3337
+ return toIntent(intent);
3529
3338
  },
3530
3339
  async cancelPaymentIntent(intentId) {
3531
3340
  const intent = intents.get(intentId);
3532
3341
  if (!intent) return;
3533
3342
  if (intent.status === "succeeded") {
3534
- throw new PaymentError2("cancel_refused", `intent ${intentId} is already captured`);
3343
+ throw new PaymentError("cancel_refused", `intent ${intentId} is already captured`);
3535
3344
  }
3536
3345
  intent.status = "cancelled";
3537
3346
  },
@@ -3550,142 +3359,33 @@ function createNativeStubPaymentsAdapter(deps) {
3550
3359
  ...event.currency !== void 0 && { currency: event.currency }
3551
3360
  };
3552
3361
  } catch (err) {
3553
- throw new PaymentError2("webhook_invalid", "stub webhook body not JSON", { cause: err });
3362
+ throw new PaymentError("webhook_invalid", "stub webhook body not JSON", { cause: err });
3554
3363
  }
3555
3364
  }
3556
3365
  };
3557
3366
  }
3558
3367
 
3559
3368
  // src/adapters/email/native-email-adapter.ts
3560
- import nodemailer from "nodemailer";
3561
3369
  import {
3562
- AdapterError as AdapterError3,
3563
3370
  EVT_CART_ABANDONED as EVT_CART_ABANDONED2,
3564
3371
  EVT_CUSTOMER_CREATED as EVT_CUSTOMER_CREATED3,
3565
3372
  EVT_ORDER_PLACED as EVT_ORDER_PLACED2,
3566
- EVT_ORDER_SHIPPED as EVT_ORDER_SHIPPED3
3373
+ EVT_ORDER_SHIPPED as EVT_ORDER_SHIPPED3,
3374
+ renderEmailTemplate
3567
3375
  } from "@nymbalhq/types";
3568
-
3569
- // src/adapters/email/templates.ts
3570
- function render(template, vars) {
3571
- return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, path) => {
3572
- const parts = path.split(".");
3573
- let cur = vars;
3574
- for (const p of parts) {
3575
- if (cur && typeof cur === "object" && p in cur) {
3576
- cur = cur[p];
3577
- } else {
3578
- return "";
3579
- }
3580
- }
3581
- return cur == null ? "" : String(cur);
3582
- });
3583
- }
3584
- var TEMPLATES = {
3585
- "order-placed": {
3586
- subject: "Order {{orderNumber}} confirmed",
3587
- body: `Hi {{customerName}},
3588
-
3589
- Thank you for your order {{orderNumber}}. Total: {{totalFormatted}}.
3590
-
3591
- We'll let you know when it ships.`
3592
- },
3593
- "order-shipped": {
3594
- subject: "Order {{orderNumber}} is on its way",
3595
- body: `Hi {{customerName}},
3596
-
3597
- Your order {{orderNumber}} has shipped via {{carrier}}.
3598
- Tracking: {{trackingNumber}}`
3599
- },
3600
- welcome: {
3601
- subject: "Welcome to {{storeName}}",
3602
- body: `Welcome {{customerName}}! Your account is ready.`
3603
- },
3604
- "cart-abandoned": {
3605
- subject: "Don't forget your cart at {{storeName}}",
3606
- body: `Hi,
3607
-
3608
- You left items in your cart. Pick up where you left off: {{resumeUrl}}`
3609
- }
3610
- };
3611
- function renderTemplate(template, vars) {
3612
- const t = TEMPLATES[template];
3613
- if (!t) {
3614
- return {
3615
- subject: `[nymbal] ${template}`,
3616
- text: JSON.stringify(vars),
3617
- html: `<pre>${JSON.stringify(vars, null, 2)}</pre>`
3618
- };
3619
- }
3620
- const subject = render(t.subject, vars);
3621
- const text = render(t.body, vars);
3622
- const html = `<pre>${text.replace(/</g, "&lt;")}</pre>`;
3623
- return { subject, text, html };
3624
- }
3625
-
3626
- // src/adapters/email/native-email-adapter.ts
3627
3376
  function createNativeEmailAdapter(deps) {
3628
3377
  const log = deps.logger.child({ adapter: "email", provider: "native" });
3629
- let cfg = deps.config ?? { transport: "console" };
3630
- let transporter = null;
3631
- function ensureTransport() {
3632
- if (cfg.transport !== "smtp") return;
3633
- if (transporter) return;
3634
- if (!cfg.smtp) {
3635
- throw new AdapterError3("email.misconfigured", "SMTP transport selected but smtp.host not set");
3636
- }
3637
- transporter = nodemailer.createTransport({
3638
- host: cfg.smtp.host,
3639
- port: cfg.smtp.port,
3640
- secure: cfg.smtp.secure ?? cfg.smtp.port === 465,
3641
- ...cfg.smtp.user !== void 0 && cfg.smtp.pass !== void 0 ? { auth: { user: cfg.smtp.user, pass: cfg.smtp.pass } } : {}
3642
- });
3643
- }
3644
3378
  return {
3645
3379
  kind: "email",
3646
3380
  providerName: "native",
3647
- capabilities: ["transactional", "list-sync"],
3381
+ capabilities: () => ["transactional", "list-sync"],
3648
3382
  producesEvents: [],
3649
- consumesEvents: [
3650
- EVT_ORDER_PLACED2,
3651
- EVT_ORDER_SHIPPED3,
3652
- EVT_CUSTOMER_CREATED3,
3653
- EVT_CART_ABANDONED2
3654
- ],
3655
- async initialize(rawConfig) {
3656
- cfg = { ...cfg, ...rawConfig };
3657
- if (cfg.transport === "smtp") ensureTransport();
3658
- },
3659
- async healthCheck() {
3660
- if (cfg.transport === "smtp" && transporter) {
3661
- try {
3662
- await transporter.verify();
3663
- return { status: "healthy", detail: "SMTP verified" };
3664
- } catch (err) {
3665
- return {
3666
- status: "degraded",
3667
- detail: `SMTP verify failed: ${err instanceof Error ? err.message : String(err)}`
3668
- };
3669
- }
3670
- }
3671
- return okHealth("console transport");
3383
+ consumesEvents: [EVT_ORDER_PLACED2, EVT_ORDER_SHIPPED3, EVT_CUSTOMER_CREATED3, EVT_CART_ABANDONED2],
3384
+ async initialize() {
3672
3385
  },
3386
+ healthCheck: () => okHealth("console transport"),
3673
3387
  async sendTransactional(params) {
3674
- const rendered = renderTemplate(params.template, params.vars);
3675
- if (cfg.transport === "smtp") {
3676
- ensureTransport();
3677
- if (!transporter) {
3678
- throw new AdapterError3("email.transport_missing", "SMTP transport not initialised");
3679
- }
3680
- const info = await transporter.sendMail({
3681
- from: cfg.from ?? "no-reply@nymbal.dev",
3682
- to: params.to,
3683
- subject: rendered.subject,
3684
- text: rendered.text,
3685
- html: rendered.html
3686
- });
3687
- return { messageId: info.messageId ?? null, accepted: (info.accepted ?? []).length > 0 };
3688
- }
3388
+ const rendered = renderEmailTemplate(params.template, params.vars);
3689
3389
  log.info(
3690
3390
  { to: params.to, subject: rendered.subject, template: params.template },
3691
3391
  "email.sent (console)"
@@ -3725,7 +3425,7 @@ function createNativeSearchAdapter(deps) {
3725
3425
  return {
3726
3426
  kind: "search",
3727
3427
  providerName: "native",
3728
- capabilities: ["index", "search", "facet"],
3428
+ capabilities: () => ["index", "search", "facet"],
3729
3429
  producesEvents: [],
3730
3430
  consumesEvents: [EVT_PRODUCT_CREATED3, EVT_PRODUCT_UPDATED3, EVT_PRODUCT_DELETED3],
3731
3431
  initialize: noopInitialize,
@@ -3810,7 +3510,7 @@ function createNativeReviewsAdapter(deps) {
3810
3510
  return {
3811
3511
  kind: "reviews",
3812
3512
  providerName: "native",
3813
- capabilities: ["submit", "moderate", "aggregate"],
3513
+ capabilities: () => ["submit", "moderate", "aggregate"],
3814
3514
  producesEvents: [],
3815
3515
  consumesEvents: [EVT_ORDER_DELIVERED3],
3816
3516
  initialize: noopInitialize,
@@ -3860,7 +3560,7 @@ function createNativeAnalyticsAdapter(logger) {
3860
3560
  return {
3861
3561
  kind: "analytics",
3862
3562
  providerName: "native",
3863
- capabilities: ["track", "identify"],
3563
+ capabilities: () => ["track", "identify"],
3864
3564
  producesEvents: [],
3865
3565
  consumesEvents: [],
3866
3566
  initialize: noopInitialize,
@@ -3880,7 +3580,7 @@ function createNativeShippingAdapter(logger) {
3880
3580
  return {
3881
3581
  kind: "shipping",
3882
3582
  providerName: "native",
3883
- capabilities: ["flat-rate"],
3583
+ capabilities: () => ["flat-rate"],
3884
3584
  producesEvents: [],
3885
3585
  consumesEvents: [],
3886
3586
  initialize: noopInitialize,
@@ -3919,7 +3619,7 @@ function createNativeTaxAdapter(logger, defaultRate = 0) {
3919
3619
  return {
3920
3620
  kind: "tax",
3921
3621
  providerName: "native",
3922
- capabilities: ["flat-rate"],
3622
+ capabilities: () => ["flat-rate"],
3923
3623
  producesEvents: [],
3924
3624
  consumesEvents: [],
3925
3625
  initialize: noopInitialize,
@@ -3935,97 +3635,170 @@ function createNativeTaxAdapter(logger, defaultRate = 0) {
3935
3635
  };
3936
3636
  }
3937
3637
 
3938
- // src/adapters/ai/native-adapter.ts
3939
- import { AdapterError as AdapterError4 } from "@nymbalhq/types";
3940
- function createNativeAiAdapter(_logger) {
3941
- return {
3942
- kind: "ai",
3943
- providerName: "native",
3944
- capabilities: [],
3945
- producesEvents: [],
3946
- consumesEvents: [],
3947
- initialize: noopInitialize,
3948
- healthCheck: () => okHealth("AI adapter is stubbed \u2014 configure a real provider for v1.0"),
3949
- async embed() {
3950
- throw new AdapterError4("not_configured", "AI embedding is not configured in v0.1");
3951
- },
3952
- async complete() {
3953
- throw new AdapterError4("not_configured", "AI completion is not configured in v0.1");
3954
- }
3955
- };
3638
+ // src/adapters/loader.ts
3639
+ import { existsSync } from "fs";
3640
+ import { join as join2 } from "path";
3641
+ import { ConfigError as ConfigError4 } from "@nymbalhq/types";
3642
+ var PROVIDER_PACKAGES = {
3643
+ payments: { stripe: { package: "@nymbalhq/adapter-stripe", sdk: "stripe" } },
3644
+ ai: { anthropic: { package: "@nymbalhq/adapter-anthropic", sdk: "@anthropic-ai/sdk" } },
3645
+ email: { smtp: { package: "@nymbalhq/adapter-smtp", sdk: "nodemailer" } }
3646
+ };
3647
+ var INSTALL_VERBS = {
3648
+ npm: "npm install",
3649
+ pnpm: "pnpm add",
3650
+ yarn: "yarn add",
3651
+ bun: "bun add"
3652
+ };
3653
+ var LOCKFILES = [
3654
+ ["pnpm-lock.yaml", "pnpm"],
3655
+ ["yarn.lock", "yarn"],
3656
+ ["bun.lock", "bun"],
3657
+ ["bun.lockb", "bun"],
3658
+ ["package-lock.json", "npm"]
3659
+ ];
3660
+ function packageManager(env, cwd) {
3661
+ const agent = env["npm_config_user_agent"]?.split("/")[0];
3662
+ if (agent !== void 0 && agent in INSTALL_VERBS) return agent;
3663
+ for (const [file, pm] of LOCKFILES) {
3664
+ if (existsSync(join2(cwd, file))) return pm;
3665
+ }
3666
+ return "npm";
3956
3667
  }
3957
-
3958
- // src/adapters/ai/anthropic-adapter.ts
3959
- import Anthropic from "@anthropic-ai/sdk";
3960
- import { AdapterError as AdapterError5 } from "@nymbalhq/types";
3961
- function createAnthropicAiAdapter(config) {
3962
- const model = config.model ?? "claude-sonnet-4-6";
3963
- const client = new Anthropic({ apiKey: config.apiKey });
3964
- return {
3965
- kind: "ai",
3966
- providerName: "anthropic",
3967
- capabilities: ["complete"],
3968
- producesEvents: [],
3969
- consumesEvents: [],
3970
- initialize: noopInitialize,
3971
- healthCheck: () => okHealth(`Anthropic AI adapter using model ${model}`),
3972
- async embed() {
3973
- throw new AdapterError5("not_configured", "Embedding not yet supported by the Anthropic adapter");
3974
- },
3975
- async complete(prompt, opts) {
3976
- const maxTokens = opts?.maxTokens ?? 1024;
3977
- const message = await client.messages.create({
3978
- model,
3979
- max_tokens: maxTokens,
3980
- messages: [{ role: "user", content: prompt }]
3981
- });
3982
- const block = message.content[0];
3983
- if (!block || block.type !== "text") {
3984
- throw new AdapterError5("unexpected_response", "Anthropic returned no text content");
3985
- }
3986
- return block.text;
3987
- }
3988
- };
3668
+ function packageOf(specifier) {
3669
+ const parts = specifier.split("/");
3670
+ return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
3671
+ }
3672
+ function missingPackage(error) {
3673
+ if (error?.code !== "ERR_MODULE_NOT_FOUND") return null;
3674
+ const match = /Cannot find (?:package|module) '([^']+)'/.exec(String(error.message));
3675
+ return match?.[1] === void 0 ? null : packageOf(match[1]);
3676
+ }
3677
+ async function loadProviderAdapter(slot, provider, ctx) {
3678
+ const entry = PROVIDER_PACKAGES[slot]?.[provider];
3679
+ if (entry === void 0) return void 0;
3680
+ let mod;
3681
+ try {
3682
+ mod = await import(entry.package);
3683
+ } catch (error) {
3684
+ const missing = missingPackage(error);
3685
+ if (missing !== entry.package && missing !== entry.sdk) throw error;
3686
+ const install = `${INSTALL_VERBS[packageManager(ctx.env, process.cwd())]} ${entry.package} ${entry.sdk}`;
3687
+ throw new ConfigError4(`${slot}.provider '${provider}' needs ${entry.package}: ${install}`, {
3688
+ code: "adapter.package_missing",
3689
+ cause: error,
3690
+ context: { slot, provider, missing, install }
3691
+ });
3692
+ }
3693
+ if (typeof mod.createAdapter !== "function") {
3694
+ throw new ConfigError4(`${entry.package} exports no createAdapter(ctx)`, {
3695
+ code: "adapter.package_invalid",
3696
+ context: { slot, provider, package: entry.package }
3697
+ });
3698
+ }
3699
+ return mod.createAdapter({ ...ctx, slot });
3989
3700
  }
3990
3701
 
3991
3702
  // src/adapters/registry.ts
3992
- function paymentsAdapterFor(provider) {
3993
- return provider === "stripe" ? "stripe" : "native-stub";
3703
+ var COMMERCE_SLOTS = [
3704
+ "payments",
3705
+ "email",
3706
+ "reviews",
3707
+ "search",
3708
+ "analytics",
3709
+ "shipping",
3710
+ "tax",
3711
+ "ai"
3712
+ ];
3713
+ function checkInjectedAdapters(config, injected = {}) {
3714
+ for (const slot of COMMERCE_SLOTS) {
3715
+ const provider = config.commerce[slot].provider;
3716
+ const given = injected[slot] !== void 0;
3717
+ if (provider === "custom" && !given) {
3718
+ throw new ConfigError5(
3719
+ `commerce.${slot}.provider is 'custom' but createApp was given no ${slot} adapter (createApp(config, { adapters: { ${slot} } })). nymbal dev and nymbal start cannot inject one yet: run the app from an entry of your own that calls createApp.`,
3720
+ { code: "adapter.custom_mismatch", context: { slot } }
3721
+ );
3722
+ }
3723
+ if (provider !== "custom" && given) {
3724
+ throw new ConfigError5(
3725
+ `createApp was given a ${slot} adapter, but commerce.${slot}.provider is '${provider}'; set it to 'custom' to use the injected adapter.`,
3726
+ { code: "adapter.custom_mismatch", context: { slot, provider } }
3727
+ );
3728
+ }
3729
+ }
3730
+ }
3731
+ async function resolveSlot(slot, config, logger, injected, builtIns) {
3732
+ const block = config.commerce[slot];
3733
+ const blockConfig = block.config ?? {};
3734
+ let adapter;
3735
+ if (block.provider === "custom") {
3736
+ adapter = injected[slot];
3737
+ } else {
3738
+ adapter = await loadProviderAdapter(slot, block.provider, {
3739
+ config: blockConfig,
3740
+ logger: logger.child({ adapter: slot }),
3741
+ env: process.env
3742
+ });
3743
+ if (adapter === void 0 && Object.hasOwn(builtIns, block.provider)) {
3744
+ adapter = builtIns[block.provider]();
3745
+ }
3746
+ }
3747
+ if (adapter === void 0) {
3748
+ throw new ConfigError5(`commerce.${slot}.provider '${block.provider}' has no adapter`, {
3749
+ context: { slot, provider: block.provider }
3750
+ });
3751
+ }
3752
+ await adapter?.initialize(blockConfig);
3753
+ return adapter;
3754
+ }
3755
+ function resolvePaymentsAdapter(config, logger, injected = {}) {
3756
+ return resolveSlot("payments", config, logger, injected, {
3757
+ native: () => createNativeStubPaymentsAdapter({ logger: logger.child({ adapter: "payments" }) })
3758
+ });
3994
3759
  }
3995
3760
  async function buildAdapters(deps) {
3996
- const { config, logger, documentStore, repos } = deps;
3997
- const payments = paymentsAdapterFor(config.commerce.payments.provider) === "stripe" ? createStripePaymentsAdapter({
3998
- logger: logger.child({ adapter: "payments" }),
3999
- config: config.commerce.payments.config ?? {}
4000
- }) : createNativeStubPaymentsAdapter({ logger: logger.child({ adapter: "payments" }) });
4001
- const email = createNativeEmailAdapter({
4002
- logger: logger.child({ adapter: "email" }),
4003
- config: config.commerce.email.config ?? {}
3761
+ const { config, logger, documentStore, repos, payments } = deps;
3762
+ const injected = deps.injected ?? {};
3763
+ const slot = (name, builtIns) => resolveSlot(name, config, logger, injected, builtIns);
3764
+ const email = await slot("email", {
3765
+ native: () => createNativeEmailAdapter({ logger: logger.child({ adapter: "email" }) })
4004
3766
  });
4005
- await email.initialize(email ? config.commerce.email.config ?? {} : {});
4006
- const search = createNativeSearchAdapter({
4007
- documentStore,
4008
- logger: logger.child({ adapter: "search" })
3767
+ const search = await slot("search", {
3768
+ native: () => createNativeSearchAdapter({ documentStore, logger: logger.child({ adapter: "search" }) })
4009
3769
  });
4010
- const reviews3 = createNativeReviewsAdapter({
4011
- repos,
4012
- logger: logger.child({ adapter: "reviews" })
3770
+ const reviews3 = await slot("reviews", {
3771
+ native: () => createNativeReviewsAdapter({ repos, logger: logger.child({ adapter: "reviews" }) })
4013
3772
  });
4014
- const analytics = createNativeAnalyticsAdapter(logger);
4015
- const shipping = createNativeShippingAdapter(logger);
4016
- const tax = createNativeTaxAdapter(logger);
4017
- const aiProvider = config.commerce.ai?.provider;
4018
- const ai = aiProvider === "anthropic" ? createAnthropicAiAdapter({
4019
- apiKey: process.env["ANTHROPIC_API_KEY"] ?? "",
4020
- model: config.commerce.ai?.config?.["model"] ?? "claude-sonnet-4-6",
4021
- logger: logger.child({ adapter: "ai" })
4022
- }) : createNativeAiAdapter(logger);
3773
+ const analytics = await slot("analytics", { native: () => createNativeAnalyticsAdapter(logger) });
3774
+ const shipping = await slot("shipping", { native: () => createNativeShippingAdapter(logger) });
3775
+ const tax = await slot("tax", { native: () => createNativeTaxAdapter(logger) });
3776
+ const ai = await slot("ai", { none: () => null });
4023
3777
  const security = createMiddlewareSecurityAdapter({
4024
3778
  logger: logger.child({ adapter: "security" })
4025
3779
  });
4026
3780
  return { security, payments, email, search, reviews: reviews3, analytics, shipping, tax, ai };
4027
3781
  }
4028
3782
 
3783
+ // src/media/local-fs-storage.ts
3784
+ import { mkdir, writeFile } from "fs/promises";
3785
+ import { join as join3 } from "path";
3786
+ var LOCAL_FS_MEDIA_CAPABILITIES = ["put"];
3787
+ function createLocalFsMediaStorage(options) {
3788
+ const { root } = options;
3789
+ const urlPrefix = options.urlPrefix ?? "/imported-media";
3790
+ return {
3791
+ capabilities: () => LOCAL_FS_MEDIA_CAPABILITIES,
3792
+ async put(key, body, _contentType) {
3793
+ const dest = join3(root, key);
3794
+ const dir = dest.substring(0, dest.lastIndexOf("/"));
3795
+ await mkdir(dir, { recursive: true });
3796
+ await writeFile(dest, body);
3797
+ return { url: `${urlPrefix}/${key}` };
3798
+ }
3799
+ };
3800
+ }
3801
+
4029
3802
  // src/services/product-service.ts
4030
3803
  import { v7 as uuidv75 } from "uuid";
4031
3804
  import {
@@ -4926,7 +4699,7 @@ function createOrderService(deps) {
4926
4699
  // src/services/inventory-service.ts
4927
4700
  import { v7 as uuidv711 } from "uuid";
4928
4701
  import {
4929
- AdapterError as AdapterError6,
4702
+ AdapterError as AdapterError3,
4930
4703
  NotFoundError as NotFoundError6,
4931
4704
  ValidationError as ValidationError9,
4932
4705
  EVT_INVENTORY_CHANGED as EVT_INVENTORY_CHANGED2,
@@ -5025,7 +4798,7 @@ function createInventoryService(deps) {
5025
4798
  async reserveStock(variantId, qty, orderRef = null, ttlSeconds) {
5026
4799
  const taken = await take(variantId, qty, orderRef, ttlSeconds);
5027
4800
  if ("available" in taken) {
5028
- throw new AdapterError6(
4801
+ throw new AdapterError3(
5029
4802
  "inventory.insufficient",
5030
4803
  `Insufficient stock for variant ${variantId}`,
5031
4804
  {
@@ -5126,12 +4899,12 @@ import {
5126
4899
  EVT_CART_CONVERTED,
5127
4900
  EVT_CART_UPDATED as EVT_CART_UPDATED2,
5128
4901
  EVT_ORDER_PAID as EVT_ORDER_PAID4,
5129
- EVT_PAYMENT_CAPTURED as EVT_PAYMENT_CAPTURED3,
5130
- EVT_PAYMENT_FAILED as EVT_PAYMENT_FAILED3,
5131
- EVT_PAYMENT_REFUNDED as EVT_PAYMENT_REFUNDED3,
4902
+ EVT_PAYMENT_CAPTURED as EVT_PAYMENT_CAPTURED2,
4903
+ EVT_PAYMENT_FAILED as EVT_PAYMENT_FAILED2,
4904
+ EVT_PAYMENT_REFUNDED as EVT_PAYMENT_REFUNDED2,
5132
4905
  NotFoundError as NotFoundError7,
5133
4906
  OrderConflictError as OrderConflictError2,
5134
- PaymentError as PaymentError3,
4907
+ PaymentError as PaymentError2,
5135
4908
  StatusTransitionError as StatusTransitionError2
5136
4909
  } from "@nymbalhq/types";
5137
4910
  var StockUnavailable = class extends Error {
@@ -5203,7 +4976,7 @@ function createCheckoutService(deps) {
5203
4976
  return recordOutcome(tx, paymentIntentId, order, `order_not_pending: ${order.status}`);
5204
4977
  }
5205
4978
  async function recordOutcome(tx, paymentIntentId, order, reason, detail) {
5206
- await publisher.publish(EVT_PAYMENT_FAILED3, {
4979
+ await publisher.publish(EVT_PAYMENT_FAILED2, {
5207
4980
  paymentId: paymentIntentId,
5208
4981
  orderId: order?.id ?? "",
5209
4982
  reason
@@ -5259,7 +5032,7 @@ function createCheckoutService(deps) {
5259
5032
  }
5260
5033
  return settlement;
5261
5034
  } catch (err) {
5262
- if (err instanceof PaymentError3 && err.code === "payment.refund_rejected") {
5035
+ if (err instanceof PaymentError2 && err.code === "payment.refund_rejected") {
5263
5036
  logger.error({ ...context2, err }, "provider refused the refund of a capture");
5264
5037
  return { status: "failed", providerRefundId: null, mismatch: false };
5265
5038
  }
@@ -5308,7 +5081,7 @@ function createCheckoutService(deps) {
5308
5081
  });
5309
5082
  await refreshRefunded(repos, tx, order.id, /* @__PURE__ */ new Date());
5310
5083
  if (settlement.status === "succeeded") {
5311
- await publisher.publish(EVT_PAYMENT_REFUNDED3, {
5084
+ await publisher.publish(EVT_PAYMENT_REFUNDED2, {
5312
5085
  paymentId: paymentIntentId,
5313
5086
  orderId: order.id,
5314
5087
  refundId,
@@ -5383,7 +5156,7 @@ function createCheckoutService(deps) {
5383
5156
  if (order.customerId) {
5384
5157
  await repos.customer.incrementStats(tx, order.customerId, 1, paid.amountMinor, now);
5385
5158
  }
5386
- await publisher.publish(EVT_PAYMENT_CAPTURED3, {
5159
+ await publisher.publish(EVT_PAYMENT_CAPTURED2, {
5387
5160
  paymentId: paymentIntentId,
5388
5161
  orderId: order.id,
5389
5162
  amountMinor: paid.amountMinor,
@@ -5525,7 +5298,7 @@ function createCheckoutService(deps) {
5525
5298
  try {
5526
5299
  await payments.cancelPaymentIntent(order.paymentIntentId);
5527
5300
  } catch (err) {
5528
- if (!(err instanceof PaymentError3 && err.code === "payment.cancel_refused")) throw err;
5301
+ if (!(err instanceof PaymentError2 && err.code === "payment.cancel_refused")) throw err;
5529
5302
  throw new CheckoutError(
5530
5303
  "payment_in_progress",
5531
5304
  `The payment for order ${order.orderNumber} is already under way`,
@@ -5565,7 +5338,7 @@ function createCheckoutService(deps) {
5565
5338
  } else if (outcome === "failed") {
5566
5339
  const reason = `payment_${intent.status}`;
5567
5340
  await cancelDraft(order.id, reason);
5568
- await publisher.publish(EVT_PAYMENT_FAILED3, {
5341
+ await publisher.publish(EVT_PAYMENT_FAILED2, {
5569
5342
  paymentId: intent.id,
5570
5343
  orderId: order.id,
5571
5344
  reason
@@ -5662,7 +5435,7 @@ function createCheckoutService(deps) {
5662
5435
  try {
5663
5436
  await payments.cancelPaymentIntent(found.paymentIntentId);
5664
5437
  } catch (err) {
5665
- if (!(err instanceof PaymentError3 && err.code === "payment.cancel_refused")) throw err;
5438
+ if (!(err instanceof PaymentError2 && err.code === "payment.cancel_refused")) throw err;
5666
5439
  throw new OrderConflictError2(
5667
5440
  "order.payment_in_progress",
5668
5441
  `The payment for order ${found.orderNumber} is under way with the provider`,
@@ -5960,33 +5733,38 @@ function startEventRetentionSweeper(deps) {
5960
5733
  }
5961
5734
 
5962
5735
  // src/app.ts
5963
- function refuseUnsafeProduction(config) {
5736
+ function refuseUnsafeProduction(config, payments) {
5964
5737
  if (process.env.NODE_ENV !== "production") return;
5965
5738
  const provider = config.commerce.payments.provider;
5966
- if (paymentsAdapterFor(provider) === "native-stub") {
5967
- throw new ConfigError4(
5739
+ if (payments.providerName === "native-stub") {
5740
+ throw new ConfigError6(
5968
5741
  `Payments provider "${provider}" resolves to the stub adapter, which verifies no webhook signature; configure a real payments provider for production.`,
5969
5742
  { code: "config.production_stub_payments", context: { provider } }
5970
5743
  );
5971
5744
  }
5972
5745
  if (config.security.auth.jwtSecret === DEFAULT_JWT_SECRET) {
5973
- throw new ConfigError4(
5746
+ throw new ConfigError6(
5974
5747
  "security.auth.jwtSecret is the schema default; set a secret of your own for production.",
5975
5748
  { code: "config.production_default_jwt_secret" }
5976
5749
  );
5977
5750
  }
5978
5751
  }
5979
5752
  async function createApp(config, options = {}) {
5980
- refuseUnsafeProduction(config);
5981
- const platform = createPlatform(config, options);
5753
+ const logger = options.logger ?? createLogger({ base: { store: config.store.name } });
5754
+ const adapterLogger = logger.child({ layer: "adapter" });
5755
+ checkInjectedAdapters(config, options.adapters);
5756
+ const payments = await resolvePaymentsAdapter(config, adapterLogger, options.adapters);
5757
+ refuseUnsafeProduction(config, payments);
5758
+ const platform = createPlatform(config, { ...options, logger });
5982
5759
  const version = options.version ?? "0.1.0";
5983
- const logger = platform.logger;
5984
5760
  const repos = buildRepositories(platform.commandStore, platform.documentStore);
5985
5761
  const adapters = await buildAdapters({
5986
5762
  config,
5987
- logger: logger.child({ layer: "adapter" }),
5763
+ logger: adapterLogger,
5988
5764
  documentStore: platform.documentStore,
5989
- repos
5765
+ repos,
5766
+ payments,
5767
+ ...options.adapters !== void 0 && { injected: options.adapters }
5990
5768
  });
5991
5769
  const relay = createRelay({
5992
5770
  store: platform.commandStore,
@@ -6073,6 +5851,37 @@ async function createApp(config, options = {}) {
6073
5851
  adminRoot: options2.adminRoot !== void 0 ? options2.adminRoot : process.env["NYMBAL_ADMIN_DIST"]
6074
5852
  });
6075
5853
  },
5854
+ capabilities() {
5855
+ const commerce = Object.fromEntries(
5856
+ COMMERCE_SLOTS.map((slot) => {
5857
+ const adapter = adapters[slot];
5858
+ return [
5859
+ slot,
5860
+ {
5861
+ provider: config.commerce[slot].provider,
5862
+ capabilities: adapter === null ? [] : adapter.capabilities()
5863
+ }
5864
+ ];
5865
+ })
5866
+ );
5867
+ return {
5868
+ ...commerce,
5869
+ security: {
5870
+ provider: config.security.adapter,
5871
+ capabilities: adapters.security.capabilities()
5872
+ },
5873
+ documentStore: {
5874
+ provider: config.infrastructure.documentStore,
5875
+ capabilities: platform.documentStore.capabilities()
5876
+ },
5877
+ eventBus: {
5878
+ provider: config.infrastructure.eventBus,
5879
+ capabilities: platform.eventBus.capabilities()
5880
+ },
5881
+ // The kernel ships one media storage, on the local filesystem; the importer creates it per run.
5882
+ media: { provider: "local-fs", capabilities: LOCAL_FS_MEDIA_CAPABILITIES }
5883
+ };
5884
+ },
6076
5885
  async stop() {
6077
5886
  await relay.stop();
6078
5887
  for (const s of sweepers) s.stop();
@@ -6141,6 +5950,26 @@ function compilePathPattern(pattern) {
6141
5950
  return new RegExp(`^${escaped}$`);
6142
5951
  }
6143
5952
 
5953
+ // src/adapters/ai/native-adapter.ts
5954
+ import { AdapterError as AdapterError4 } from "@nymbalhq/types";
5955
+ function createNativeAiAdapter(_logger) {
5956
+ return {
5957
+ kind: "ai",
5958
+ providerName: "native",
5959
+ capabilities: () => [],
5960
+ producesEvents: [],
5961
+ consumesEvents: [],
5962
+ initialize: noopInitialize,
5963
+ healthCheck: () => okHealth("AI adapter is stubbed \u2014 configure a real provider for v1.0"),
5964
+ async embed() {
5965
+ throw new AdapterError4("not_configured", "AI embedding is not configured in v0.1");
5966
+ },
5967
+ async complete() {
5968
+ throw new AdapterError4("not_configured", "AI completion is not configured in v0.1");
5969
+ }
5970
+ };
5971
+ }
5972
+
6144
5973
  // src/seed/run.ts
6145
5974
  import { inArray as inArray3 } from "drizzle-orm";
6146
5975
  import { v7 as uuidv713 } from "uuid";
@@ -6553,35 +6382,18 @@ async function runSeed(deps) {
6553
6382
  alreadySeeded: false
6554
6383
  };
6555
6384
  }
6556
-
6557
- // src/media/local-fs-storage.ts
6558
- import { mkdir, writeFile } from "fs/promises";
6559
- import { join as join2 } from "path";
6560
- function createLocalFsMediaStorage(options) {
6561
- const { root } = options;
6562
- const urlPrefix = options.urlPrefix ?? "/imported-media";
6563
- return {
6564
- async put(key, body, _contentType) {
6565
- const dest = join2(root, key);
6566
- const dir = dest.substring(0, dest.lastIndexOf("/"));
6567
- await mkdir(dir, { recursive: true });
6568
- await writeFile(dest, body);
6569
- return { url: `${urlPrefix}/${key}` };
6570
- }
6571
- };
6572
- }
6573
6385
  export {
6574
6386
  CSRF_COOKIE,
6575
6387
  CSRF_HEADER,
6576
6388
  InMemoryDocumentStore,
6577
6389
  InProcessEventBus,
6390
+ PROVIDER_PACKAGES,
6578
6391
  PROVIDER_REFUND_REASON,
6579
6392
  RateLimiter,
6580
6393
  buildAdapters,
6581
6394
  buildRepositories,
6582
6395
  buildSecurityHeaders,
6583
6396
  buildServices,
6584
- createAnthropicAiAdapter,
6585
6397
  createApp,
6586
6398
  createAuthService,
6587
6399
  createCartRepository,
@@ -6628,10 +6440,8 @@ export {
6628
6440
  createReviewRepository,
6629
6441
  createSearchService,
6630
6442
  createStockAdjustmentRepository,
6631
- createStripePaymentsAdapter,
6632
6443
  createVariantRepository,
6633
6444
  orderPayment,
6634
- paymentsAdapterFor,
6635
6445
  rawClients,
6636
6446
  refreshRefunded,
6637
6447
  registerAdminStaticRoute,