@base44/app-plugin-commerce 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,9 @@
5
5
  * Actions: create | delete
6
6
  */
7
7
  import { createClientFromRequest } from "npm:@base44/sdk";
8
+ // The only layer that may reach the runtime: shared/ is client-bundleable,
9
+ // so the secret store is read here and handed down per call.
10
+ import { secrets } from "base44:runtime";
8
11
  import { HttpError, requireAdmin } from "../../../shared/commerce/auth.ts";
9
12
  import { refundCardOrder } from "../../../shared/commerce/payments.ts";
10
13
  import { round2 } from "../../../shared/commerce/money.ts";
@@ -64,6 +67,7 @@ async function create(sr: any, payload: any, actor: string): Promise<any> {
64
67
  let gatewayRefund: Awaited<ReturnType<typeof refundCardOrder>> = null;
65
68
  if (payload.refund_payment) {
66
69
  gatewayRefund = await refundCardOrder(sr, order, {
70
+ secrets,
67
71
  amount,
68
72
  reason: payload.reason,
69
73
  });
@@ -27,6 +27,9 @@
27
27
  * 400 `webhook_not_implemented`.
28
28
  */
29
29
  import { createClientFromRequest } from "npm:@base44/sdk";
30
+ // The only layer that may reach the runtime: shared/ is client-bundleable,
31
+ // so the secret store is read here and handed down per call.
32
+ import { secrets } from "base44:runtime";
30
33
  import { HttpError } from "../../../shared/commerce/auth.ts";
31
34
  import { getSettings } from "../../../shared/commerce/settings.ts";
32
35
  import { confirmCardPayment } from "../../../shared/commerce/payments.ts";
@@ -41,7 +44,7 @@ Deno.serve(async (req: Request) => {
41
44
  try {
42
45
  const payload = await req.text();
43
46
 
44
- const event = await parseWebhook(req, payload);
47
+ const event = await parseWebhook(req, payload, secrets);
45
48
  // Not about one of this store's orders — acknowledge so the provider
46
49
  // doesn't retry.
47
50
  if (!event || !event.order_id) {
@@ -62,6 +65,7 @@ Deno.serve(async (req: Request) => {
62
65
 
63
66
  const settings = await getSettings(sr);
64
67
  const result = await confirmCardPayment(sr, order, {
68
+ secrets,
65
69
  reference: event.reference,
66
70
  trustedPaid: event.paid,
67
71
  settings,
@@ -25,6 +25,9 @@
25
25
  * order without a key. Entity access is service-role throughout.
26
26
  */
27
27
  import { createClientFromRequest } from "npm:@base44/sdk";
28
+ // The only layer that may reach the runtime: shared/ is client-bundleable,
29
+ // so the secret store is read here and handed down per call.
30
+ import { secrets } from "base44:runtime";
28
31
  import { HttpError, getCallerUser, isAdmin } from "../../../shared/commerce/auth.ts";
29
32
  import { getSettings } from "../../../shared/commerce/settings.ts";
30
33
  import { serializeOrderForCustomer } from "../../../shared/commerce/orders.ts";
@@ -112,6 +115,7 @@ Deno.serve(async (req: Request) => {
112
115
  returnPath: settings.general?.order_received_path,
113
116
  });
114
117
  const link = await startCardPayment(sr, order, {
118
+ secrets,
115
119
  successUrl,
116
120
  cancelUrl,
117
121
  customerEmail: order.billing?.email || undefined,
@@ -134,6 +138,7 @@ Deno.serve(async (req: Request) => {
134
138
  const order = await authorizeOrder(sr, payload, admin);
135
139
  const settings = await getSettings(sr);
136
140
  const result = await confirmCardPayment(sr, order, {
141
+ secrets,
137
142
  settings,
138
143
  actor: admin ? (user?.email ?? "admin") : "customer-return",
139
144
  });
@@ -154,6 +159,7 @@ Deno.serve(async (req: Request) => {
154
159
  returnPath: settings.general?.order_received_path,
155
160
  });
156
161
  paymentLink = await startCardPayment(sr, result.order, {
162
+ secrets,
157
163
  successUrl,
158
164
  cancelUrl,
159
165
  customerEmail: result.order.billing?.email || undefined,
@@ -195,6 +201,7 @@ Deno.serve(async (req: Request) => {
195
201
  const order = await authorizeOrder(sr, payload, admin);
196
202
  const settings = await getSettings(sr);
197
203
  const result = await confirmCardPayment(sr, order, {
204
+ secrets,
198
205
  settings,
199
206
  actor: admin ? (user?.email ?? "admin") : "customer-return",
200
207
  });
@@ -12,6 +12,9 @@
12
12
  * Checkout is open to guests; order_key possession is the guest bearer credential.
13
13
  */
14
14
  import { createClientFromRequest } from "npm:@base44/sdk";
15
+ // The only layer that may reach the runtime: shared/ is client-bundleable,
16
+ // so the secret store is read here and handed down per call.
17
+ import { secrets } from "base44:runtime";
15
18
  import { HttpError, getCallerUser, ownsEmail } from "../../../shared/commerce/auth.ts";
16
19
  import { getSetting, getSettings } from "../../../shared/commerce/settings.ts";
17
20
  import { calculateTotals } from "../../../shared/commerce/totals.ts";
@@ -290,6 +293,7 @@ async function placeOrder(sr: any, req: Request, user: any, payload: any): Promi
290
293
  returnPath: settings.general?.order_received_path,
291
294
  });
292
295
  const link = await startCardPayment(sr, order, {
296
+ secrets,
293
297
  successUrl,
294
298
  cancelUrl,
295
299
  customerEmail: billing.email,
@@ -357,6 +361,7 @@ async function confirmPayment(sr: any, payload: any): Promise<any> {
357
361
 
358
362
  const settings = await getSettings(sr);
359
363
  const result = await confirmCardPayment(sr, order, {
364
+ secrets,
360
365
  settings,
361
366
  actor: "customer-return",
362
367
  });
@@ -41,6 +41,17 @@
41
41
  */
42
42
  import { HttpError } from "./auth.ts";
43
43
 
44
+ /**
45
+ * The app's secret store, handed in per call by the function layer:
46
+ * `import { secrets } from "base44:runtime"` there, never in this file —
47
+ * that specifier resolves only in the Deno function runtime, and `shared/` is
48
+ * reachable by the client bundler, so importing it here fails the storefront's
49
+ * build. `Deno.env` would leak the same runtime assumption.
50
+ */
51
+ export interface SecretStore {
52
+ get(name: string): unknown;
53
+ }
54
+
44
55
  /** Stripe's REST API, called directly — no SDK to bundle in the function. */
45
56
  const STRIPE_API = "https://api.stripe.com/v1";
46
57
  /** Pinned, so a Stripe API release can never change the shapes read below. */
@@ -61,14 +72,9 @@ export interface CardPaymentPage {
61
72
  * store that has the file but not yet the secret answers a clean 503 at
62
73
  * checkout instead of failing to boot every commerce function that imports it.
63
74
  */
64
- const secret = (name: string): string => {
65
- // Deno.env, never `secrets` from "base44:runtime": that specifier resolves
66
- // only in the Deno function runtime, and this file lives under shared/ where
67
- // a client bundler can reach it — the static import fails the Vite build of
68
- // the whole storefront. Base44 publishes app secrets into the function
69
- // environment, so Deno.env.get reads the same values.
75
+ const secret = (secrets: SecretStore, name: string): string => {
70
76
  try {
71
- return String(Deno.env.get(name) ?? "");
77
+ return String(secrets?.get(name) ?? "");
72
78
  } catch {
73
79
  return "";
74
80
  }
@@ -81,9 +87,9 @@ const secret = (name: string): string => {
81
87
  */
82
88
  const STRIPE_KEY_SECRETS = ["STRIPE_SECRET_KEY", "STRIPE_API_KEY", "STRIPE_KEY"];
83
89
 
84
- const stripeKey = (): string => {
90
+ const stripeKey = (secrets: SecretStore): string => {
85
91
  for (const name of STRIPE_KEY_SECRETS) {
86
- const value = secret(name);
92
+ const value = secret(secrets, name);
87
93
  if (value) return value;
88
94
  }
89
95
  // The client is told only that cards are unavailable — which secret is
@@ -104,7 +110,7 @@ const stripeKey = (): string => {
104
110
  * as `base44_app_id`, which is how the platform attributes a Stripe payment
105
111
  * back to this app — send it on every call that creates money movement.
106
112
  */
107
- const base44AppId = (): string => secret("BASE44_APP_ID");
113
+ const base44AppId = (secrets: SecretStore): string => secret(secrets, "BASE44_APP_ID");
108
114
 
109
115
  /**
110
116
  * One Stripe REST call. A body makes it a POST (form-encoded, with an
@@ -113,9 +119,9 @@ const base44AppId = (): string => secret("BASE44_APP_ID");
113
119
  * Stripe's own error text stays in the log: it describes backend configuration
114
120
  * (keys, account state, API parameters), so the caller gets a flat message.
115
121
  */
116
- async function stripeCall(path: string, body?: URLSearchParams): Promise<any> {
122
+ async function stripeCall(secrets: SecretStore, path: string, body?: URLSearchParams): Promise<any> {
117
123
  const headers: Record<string, string> = {
118
- "Authorization": `Bearer ${stripeKey()}`,
124
+ "Authorization": `Bearer ${stripeKey(secrets)}`,
119
125
  "Stripe-Version": STRIPE_VERSION,
120
126
  };
121
127
  if (body) {
@@ -156,7 +162,7 @@ export async function createCardPayment(
156
162
  order_id: String(order.id),
157
163
  order_key: String(order.order_key),
158
164
  };
159
- const appId = base44AppId();
165
+ const appId = base44AppId(opts.secrets);
160
166
  if (appId) metadata.base44_app_id = appId;
161
167
 
162
168
  const params = new URLSearchParams();
@@ -174,7 +180,7 @@ export async function createCardPayment(
174
180
  params.set(`payment_intent_data[metadata][${key}]`, value);
175
181
  }
176
182
 
177
- const session = await stripeCall("/checkout/sessions", params);
183
+ const session = await stripeCall(opts.secrets, "/checkout/sessions", params);
178
184
  if (!session?.url) throw new HttpError(502, "Stripe did not return a payment page URL.", "payment_session_failed");
179
185
  return { url: session.url, reference: String(session.id) };
180
186
  }
@@ -184,8 +190,8 @@ export async function createCardPayment(
184
190
  * caller. Runs on the customer-return page, the webhook's unverified path and
185
191
  * the admin's "Check payment" button.
186
192
  */
187
- export async function checkCardPaymentPaid(_sr: any, order: any, reference: string): Promise<boolean> {
188
- const session = await stripeCall(`/checkout/sessions/${encodeURIComponent(reference)}`);
193
+ export async function checkCardPaymentPaid(_sr: any, order: any, reference: string, secrets: SecretStore): Promise<boolean> {
194
+ const session = await stripeCall(secrets, `/checkout/sessions/${encodeURIComponent(reference)}`);
189
195
  // The payment must be for THIS order — stops a reference to some other
190
196
  // (genuinely paid) session being replayed against a different order.
191
197
  return session?.payment_status === "paid" && session?.metadata?.order_id === String(order.id);
@@ -200,20 +206,21 @@ export async function refundCardPayment(_sr: any, _order: any, opts: {
200
206
  amount: number;
201
207
  currency: string;
202
208
  reason?: string;
209
+ secrets: SecretStore;
203
210
  }): Promise<{ refund_id: string }> {
204
211
  // The stored reference is the Checkout Session; the refundable object is the
205
212
  // payment intent behind it, which only exists once the session was paid.
206
- const session = await stripeCall(`/checkout/sessions/${encodeURIComponent(opts.reference)}`);
213
+ const session = await stripeCall(opts.secrets, `/checkout/sessions/${encodeURIComponent(opts.reference)}`);
207
214
  if (!session?.payment_intent) {
208
215
  throw new HttpError(409, "This payment has no charge to refund at Stripe.", "no_charge_to_refund");
209
216
  }
210
217
  const params = new URLSearchParams();
211
218
  params.set("payment_intent", String(session.payment_intent));
212
219
  params.set("amount", String(minorUnits(opts.amount, opts.currency)));
213
- const appId = base44AppId();
220
+ const appId = base44AppId(opts.secrets);
214
221
  if (appId) params.set("metadata[base44_app_id]", appId);
215
222
 
216
- const refund = await stripeCall("/refunds", params);
223
+ const refund = await stripeCall(opts.secrets, "/refunds", params);
217
224
  return { refund_id: String(refund.id) };
218
225
  }
219
226
 
@@ -246,7 +253,7 @@ export interface CardWebhookEvent {
246
253
  * null for anything that isn't a payment event for one of this store's orders;
247
254
  * the premade function answers 200 so Stripe stops retrying.
248
255
  */
249
- export async function parseWebhook(_req: Request, payload: string): Promise<CardWebhookEvent | null> {
256
+ export async function parseWebhook(_req: Request, payload: string, secrets: SecretStore): Promise<CardWebhookEvent | null> {
250
257
  let event: any;
251
258
  try { event = JSON.parse(payload); } catch { return null; }
252
259
  const metadata = event?.data?.object?.metadata;
@@ -34,16 +34,25 @@
34
34
  * Until implemented, the Credit Card checkout option answers
35
35
  * 503 `no_card_payment_provider`.
36
36
  *
37
- * Credentials belong in Base44 secrets, read with `Deno.env.get("...")`
38
- * never in an entity, never in the code, never from the client. Do NOT import
39
- * `secrets` from `base44:runtime` here: that specifier resolves only in the
40
- * Deno function runtime, and a static import of it in this shared file fails
41
- * the client bundle of the whole storefront. When one is missing, log which one and answer the caller with the
37
+ * Credentials belong in Base44 secrets but this file never reaches for them
38
+ * itself. Every function below is handed a `SecretStore` by the function that
39
+ * called it, the one layer that may `import { secrets } from "base44:runtime"`.
40
+ * Neither that import nor `Deno.env` belongs here: `shared/` is reachable by
41
+ * the client bundler, so either one fails the storefront's build and pins this
42
+ * file to a single runtime. Never an entity, never the code, never the client. When one is missing, log which one and answer the caller with the
42
43
  * flat 503 below: the storefront must not learn the names of the app's
43
44
  * secrets.
44
45
  */
45
46
  import { HttpError } from "./auth.ts";
46
47
 
48
+ /**
49
+ * The app's secret store, handed in per call by the function layer:
50
+ * `import { secrets } from "base44:runtime"` there, never here.
51
+ */
52
+ export interface SecretStore {
53
+ get(name: string): unknown;
54
+ }
55
+
47
56
  /** A hosted payment page for one order. */
48
57
  export interface CardPaymentPage {
49
58
  /** Where the customer goes to pay. */
@@ -67,7 +76,7 @@ export interface CardPaymentPage {
67
76
  export async function createCardPayment(
68
77
  _sr: any,
69
78
  _order: any,
70
- _opts: { successUrl: string; cancelUrl: string; customerEmail?: string },
79
+ _opts: { successUrl: string; cancelUrl: string; customerEmail?: string; secrets: SecretStore },
71
80
  ): Promise<CardPaymentPage> {
72
81
  throw new HttpError(
73
82
  503,
@@ -82,7 +91,7 @@ export async function createCardPayment(
82
91
  * a claim from the client. Called by the customer-return page, the webhook's
83
92
  * unsigned path, and the admin's "Check payment" button.
84
93
  */
85
- export async function checkCardPaymentPaid(_sr: any, _order: any, _reference: string): Promise<boolean> {
94
+ export async function checkCardPaymentPaid(_sr: any, _order: any, _reference: string, _secrets: SecretStore): Promise<boolean> {
86
95
  return false;
87
96
  }
88
97
 
@@ -97,6 +106,7 @@ export async function refundCardPayment(_sr: any, _order: any, _opts: {
97
106
  amount: number;
98
107
  currency: string;
99
108
  reason?: string;
109
+ secrets: SecretStore;
100
110
  }): Promise<{ refund_id: string }> {
101
111
  throw new HttpError(
102
112
  501,
@@ -140,7 +150,7 @@ export interface CardWebhookEvent {
140
150
  * aren't about a payment for one of this store's orders (answered 200 so the
141
151
  * provider doesn't retry).
142
152
  */
143
- export async function parseWebhook(_req: Request, _payload: string): Promise<CardWebhookEvent | null> {
153
+ export async function parseWebhook(_req: Request, _payload: string, _secrets: SecretStore): Promise<CardWebhookEvent | null> {
144
154
  throw new HttpError(
145
155
  400,
146
156
  "This store's payment webhook is not implemented.",
@@ -19,7 +19,7 @@
19
19
  import { HttpError } from "./auth.ts";
20
20
  import { round2 } from "./money.ts";
21
21
  import { transitionOrder } from "./orders.ts";
22
- import { checkCardPaymentPaid, createCardPayment, refundCardPayment } from "./card-payment.ts";
22
+ import { checkCardPaymentPaid, createCardPayment, refundCardPayment, type SecretStore } from "./card-payment.ts";
23
23
 
24
24
  /** The one `commerce.PaymentGateway` slug that pays online; all others are manual. */
25
25
  export const CARD_GATEWAY_SLUG = "card";
@@ -182,6 +182,8 @@ export async function startCardPayment(sr: any, order: any, opts: {
182
182
  successUrl: string;
183
183
  cancelUrl: string;
184
184
  customerEmail?: string;
185
+ // Handed down from the function entry — shared/ may not read secrets itself.
186
+ secrets: SecretStore;
185
187
  }): Promise<{ url: string; reference: string }> {
186
188
  if (isOrderPaid(order)) {
187
189
  throw new HttpError(409, "This order is already paid.", "already_paid");
@@ -210,12 +212,13 @@ export async function confirmCardPayment(sr: any, order: any, opts: {
210
212
  settings?: Record<string, any>;
211
213
  actor?: string;
212
214
  trustedPaid?: boolean;
213
- } = {}): Promise<{ paid: boolean; already_confirmed: boolean; order: any }> {
215
+ secrets: SecretStore;
216
+ }): Promise<{ paid: boolean; already_confirmed: boolean; order: any }> {
214
217
  if (isOrderPaid(order)) return { paid: true, already_confirmed: true, order };
215
218
 
216
219
  const reference = opts.reference || orderMeta(order, REFERENCE_META_KEY) || String(order?.transaction_id ?? "");
217
220
  const paid = opts.trustedPaid === true ||
218
- (reference ? await checkCardPaymentPaid(sr, order, reference) : false);
221
+ (reference ? await checkCardPaymentPaid(sr, order, reference, opts.secrets) : false);
219
222
  if (!paid) return { paid: false, already_confirmed: false, order };
220
223
 
221
224
  if (reference) {
@@ -242,10 +245,12 @@ export async function confirmCardPayment(sr: any, order: any, opts: {
242
245
  export async function refundCardOrder(sr: any, order: any, opts: {
243
246
  amount: number;
244
247
  reason?: string;
248
+ secrets: SecretStore;
245
249
  }): Promise<{ refund_id: string } | null> {
246
250
  const reference = order?.transaction_id || orderMeta(order, REFERENCE_META_KEY);
247
251
  if (!isCardGateway(order?.payment_method) || !reference) return null;
248
252
  return await refundCardPayment(sr, order, {
253
+ secrets: opts.secrets,
249
254
  reference,
250
255
  amount: round2(opts.amount),
251
256
  currency: String(order.currency || "USD"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@base44/app-plugin-commerce",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Base44 Commerce plugin — entities, backend functions, shared commerce engine, admin UI and the commerce skill, shipped as copyable source",
5
5
  "keywords": [
6
6
  "base44",