@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.d.ts +42 -41
- package/dist/index.js +266 -456
- package/dist/index.js.map +1 -1
- package/package.json +24 -8
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
|
|
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/
|
|
3229
|
-
import
|
|
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
|
|
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
|
|
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
|
-
|
|
3467
|
-
|
|
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
|
|
3292
|
+
return toIntent(intent);
|
|
3484
3293
|
},
|
|
3485
3294
|
async capturePayment(intentId) {
|
|
3486
3295
|
const intent = intents.get(intentId);
|
|
3487
|
-
if (!intent) throw new
|
|
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
|
|
3528
|
-
return
|
|
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
|
|
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
|
|
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, "<")}</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
|
-
|
|
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 =
|
|
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/
|
|
3939
|
-
import {
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
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
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
function
|
|
3962
|
-
|
|
3963
|
-
const
|
|
3964
|
-
return
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
}
|
|
3986
|
-
|
|
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
|
-
|
|
3993
|
-
|
|
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
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
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
|
-
|
|
4006
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5130
|
-
EVT_PAYMENT_FAILED as
|
|
5131
|
-
EVT_PAYMENT_REFUNDED as
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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 (
|
|
5967
|
-
throw new
|
|
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
|
|
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
|
-
|
|
5981
|
-
const
|
|
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:
|
|
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,
|