@amos.com/amos-js 0.10.1 → 0.10.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.
package/dist/jwt.d.ts CHANGED
@@ -12,5 +12,9 @@ export declare function decodeJwt(token: string | undefined): {
12
12
  /**
13
13
  * Resolve the Amos embed origin (production vs. sandbox) from a render
14
14
  * token's decoded payload.
15
+ *
16
+ * When the parent page is on localhost / *.localhost, always target the
17
+ * local embed app (https://embed.localhost) so dashboard + embed can be
18
+ * developed together without pointing at sandbox/production iframes.
15
19
  */
16
20
  export declare function getEmbedOrigin(renderToken: string): string;
package/dist/log.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Forward an info log to the embed iframe so Rollbar records it with
3
+ * Amos credentials. Payload shape matches dashboard/embed API traces:
4
+ * `endpoint`, `headers`, `body`.
5
+ */
6
+ export declare function reportParentInfoLog({ iframe, message, endpoint, headers, body, }: {
7
+ iframe: HTMLIFrameElement;
8
+ message: string;
9
+ endpoint: string;
10
+ headers?: Record<string, string>;
11
+ body?: unknown;
12
+ }): void;
package/dist/mount.d.ts CHANGED
@@ -85,9 +85,22 @@ export type AmosBankAccountPaymentMethodFormOptions = PaymentMethodFormListenerO
85
85
  * `"0"`, which is typically under the threshold so Connect / Plaid
86
86
  * stays hidden until the host passes a real charge.
87
87
  *
88
+ * Ignored when {@link AmosBankAccountPaymentMethodFormOptions.intent}
89
+ * is `"setup"` — setup intents always show Connect (unless the render
90
+ * token disables Plaid verification).
91
+ *
88
92
  * @default "0"
89
93
  */
90
94
  amount?: string;
95
+ /**
96
+ * `"setup"` saves a bank account for later charges and always shows
97
+ * Connect / Plaid (no merchant threshold lookup). `"payment"`
98
+ * compares {@link AmosBankAccountPaymentMethodFormOptions.amount} to
99
+ * the merchant ACH threshold.
100
+ *
101
+ * @default "payment"
102
+ */
103
+ intent?: "payment" | "setup";
91
104
  };
92
105
  /**
93
106
  * Mount the secure bank-account payment method form into a container
@@ -98,7 +111,8 @@ export type AmosBankAccountPaymentMethodFormOptions = PaymentMethodFormListenerO
98
111
  * Connect bank button is rendered in the parent document and Plaid Link
99
112
  * is opened on click. `amount` defaults to `"0"`, which is typically
100
113
  * under the threshold so Connect stays hidden on open-amount forms
101
- * until the host passes a real charge. The button uses the same
114
+ * until the host passes a real charge. Pass `intent: "setup"` to always
115
+ * show Connect without a merchant lookup. The button uses the same
102
116
  * `appearance.themeVariables` as the iframe (and inherits host-page
103
117
  * tokens when those variables are unset). Otherwise a field-shaped
104
118
  * skeleton is shown and replaced by the iframe once appearance is applied.
@@ -22,8 +22,11 @@ export type BillingAddressRequirement = "country" | "full";
22
22
  export declare function getCreditCardFormSrc(renderToken: string, additionalFields?: CreditCardAdditionalFields, billingAddressRequirement?: BillingAddressRequirement): string;
23
23
  /**
24
24
  * Build the iframe `src` URL for the embedded bank-account form.
25
+ *
26
+ * Pass `intent: "setup"` so the iframe always requires Plaid (when the
27
+ * render token allows it) and does not `GET /merchants` for a threshold.
25
28
  */
26
- export declare function getBankAccountFormSrc(renderToken: string, billingAddressRequirement?: BillingAddressRequirement): string;
29
+ export declare function getBankAccountFormSrc(renderToken: string, billingAddressRequirement?: BillingAddressRequirement, intent?: "payment" | "setup"): string;
27
30
  /**
28
31
  * Default iframe pixel height for the credit-card form, taking the
29
32
  * configured `additionalFields` and `billingAddressRequirement` into
package/dist/types.d.ts CHANGED
@@ -248,10 +248,11 @@ export type Message = {
248
248
  brand: CardBrand | null;
249
249
  } | {
250
250
  /**
251
- * Embed → parent: merchant ACH verification threshold for this
252
- * render token. `achThreshold` is cents, or `null` when the
253
- * merchant has no threshold (manual ACH). `requireVerification`
254
- * is true when the fetch failed in production (fail closed).
251
+ * Embed → parent: ACH verification policy for this bank iframe.
252
+ * `achThreshold` is cents, or `null` when the merchant has no
253
+ * threshold (manual ACH). `requireVerification` is true for setup
254
+ * intents (always Plaid, no merchant lookup), and when a payment
255
+ * threshold fetch failed in production (fail closed).
255
256
  */
256
257
  type: "ACH_THRESHOLD";
257
258
  achThreshold?: number | null;
@@ -266,6 +267,16 @@ export type Message = {
266
267
  requestId: string;
267
268
  link_token?: string;
268
269
  error?: string;
270
+ } | {
271
+ /**
272
+ * Parent → embed: info telemetry for Rollbar. The iframe reports
273
+ * `message` with `endpoint`, `headers`, and `body`.
274
+ */
275
+ type: "PARENT_INFO_LOG";
276
+ message: string;
277
+ endpoint?: string;
278
+ headers?: Record<string, string>;
279
+ body?: unknown;
269
280
  };
270
281
  /**
271
282
  * Identity helper that brands an object as a typed `Message`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amos.com/amos-js",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "main": "dist/index.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,11 +35,8 @@
35
35
  "author": "Amos",
36
36
  "license": "MIT",
37
37
  "description": "Amos JavaScript SDK for embedding payment methods via iframes.",
38
- "peerDependencies": {
39
- "@amos.com/node": ">=0.1.39"
40
- },
41
38
  "devDependencies": {
42
- "@amos.com/node": "0.1.39",
39
+ "@amos.com/node": "0.1.50",
43
40
  "@biomejs/biome": "2.5.7",
44
41
  "@changesets/cli": "2.31.1",
45
42
  "@types/node": "26.1.2",
@@ -50,5 +47,8 @@
50
47
  },
51
48
  "dependencies": {
52
49
  "@types/googlepay": "0.7.11"
50
+ },
51
+ "peerDependencies": {
52
+ "@amos.com/node": ">=0.1.50"
53
53
  }
54
54
  }
package/src/jwt.ts CHANGED
@@ -25,11 +25,35 @@ export function decodeJwt(token: string | undefined): {
25
25
  };
26
26
  }
27
27
 
28
+ // --- BEGIN amos-ui sdk:link localhost embed origin ---
29
+ /**
30
+ * True when the parent page is running on localhost / *.localhost
31
+ * (e.g. https://dashboard.localhost via portless).
32
+ *
33
+ * Injected by amos-ui `pnpm sdk:link`; removed by `pnpm sdk:unlink`.
34
+ * Do not commit this block to amos-js.
35
+ */
36
+ function isLocalhostParent(): boolean {
37
+ if (typeof window === "undefined") {
38
+ return false;
39
+ }
40
+ const { hostname } = window.location;
41
+ return hostname === "localhost" || hostname.endsWith(".localhost");
42
+ }
43
+
28
44
  /**
29
45
  * Resolve the Amos embed origin (production vs. sandbox) from a render
30
46
  * token's decoded payload.
47
+ *
48
+ * When the parent page is on localhost / *.localhost, always target the
49
+ * local embed app (https://embed.localhost) so dashboard + embed can be
50
+ * developed together without pointing at sandbox/production iframes.
31
51
  */
32
52
  export function getEmbedOrigin(renderToken: string): string {
53
+ if (isLocalhostParent()) {
54
+ return "https://embed.localhost";
55
+ }
56
+
33
57
  const { env = "sandbox" }: components["schemas"]["RenderTokenJwt"] =
34
58
  decodeJwt(renderToken).payload;
35
59
 
@@ -42,3 +66,4 @@ export function getEmbedOrigin(renderToken: string): string {
42
66
  return "https://embed-sandbox.amos.com";
43
67
  }
44
68
  }
69
+ // --- END amos-ui sdk:link localhost embed origin ---
package/src/log.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { createMessage } from "./types";
2
+
3
+ const SENSITIVE_HEADER_NAMES = new Set([
4
+ "authorization",
5
+ "cookie",
6
+ "set-cookie",
7
+ "x-api-key",
8
+ ]);
9
+
10
+ const SENSITIVE_BODY_KEYS = new Set([
11
+ "authorization",
12
+ "encrypted_account_number",
13
+ "link_token",
14
+ "public_token",
15
+ "token",
16
+ ]);
17
+
18
+ function redactHeaders(
19
+ headers: Record<string, string>,
20
+ ): Record<string, string> {
21
+ const redacted: Record<string, string> = {};
22
+ for (const [key, value] of Object.entries(headers)) {
23
+ redacted[key] = SENSITIVE_HEADER_NAMES.has(key.toLowerCase())
24
+ ? "[REDACTED]"
25
+ : value;
26
+ }
27
+ return redacted;
28
+ }
29
+
30
+ function redactBody(body: unknown): unknown {
31
+ if (Array.isArray(body)) {
32
+ return body.map(redactBody);
33
+ }
34
+ if (body !== null && typeof body === "object") {
35
+ const redacted: Record<string, unknown> = {};
36
+ for (const [key, value] of Object.entries(body)) {
37
+ redacted[key] = SENSITIVE_BODY_KEYS.has(key)
38
+ ? "[REDACTED]"
39
+ : redactBody(value);
40
+ }
41
+ return redacted;
42
+ }
43
+ return body;
44
+ }
45
+
46
+ /**
47
+ * Forward an info log to the embed iframe so Rollbar records it with
48
+ * Amos credentials. Payload shape matches dashboard/embed API traces:
49
+ * `endpoint`, `headers`, `body`.
50
+ */
51
+ export function reportParentInfoLog({
52
+ iframe,
53
+ message,
54
+ endpoint,
55
+ headers = {},
56
+ body,
57
+ }: {
58
+ iframe: HTMLIFrameElement;
59
+ message: string;
60
+ endpoint: string;
61
+ headers?: Record<string, string>;
62
+ body?: unknown;
63
+ }): void {
64
+ iframe.contentWindow?.postMessage(
65
+ createMessage({
66
+ type: "PARENT_INFO_LOG",
67
+ message,
68
+ endpoint,
69
+ headers: redactHeaders(headers),
70
+ body: redactBody(body),
71
+ }),
72
+ new URL(iframe.src).origin,
73
+ );
74
+ }
package/src/messaging.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { components } from "@amos.com/node";
2
2
  import { decodeJwt } from "./jwt";
3
+ import { reportParentInfoLog } from "./log";
3
4
  import { getBankPlaidSession } from "./plaid-session";
4
5
  import {
5
6
  type Appearance,
@@ -292,6 +293,24 @@ function postConfirmIntent({
292
293
  resetIframeFields(iframe);
293
294
  }
294
295
 
296
+ const origin = getIframeTargetOrigin(iframe);
297
+ reportParentInfoLog({
298
+ iframe,
299
+ message:
300
+ type === "CONFIRM_PAYMENT_INTENT"
301
+ ? "confirmPaymentIntent"
302
+ : "confirmSetupIntent",
303
+ endpoint:
304
+ type === "CONFIRM_PAYMENT_INTENT"
305
+ ? "POST /embed/payment_intents/{id}/confirm_with_payment_method"
306
+ : "POST /embed/setup_intents/{id}/confirm_with_payment_method",
307
+ headers: {
308
+ origin: window.location.origin,
309
+ "iframe-origin": origin,
310
+ },
311
+ body: { id, token, ...(plaid ? { plaid } : {}) },
312
+ });
313
+
295
314
  iframe.contentWindow?.postMessage(
296
315
  createMessage({
297
316
  type,
package/src/mount.ts CHANGED
@@ -514,9 +514,22 @@ export type AmosBankAccountPaymentMethodFormOptions =
514
514
  * `"0"`, which is typically under the threshold so Connect / Plaid
515
515
  * stays hidden until the host passes a real charge.
516
516
  *
517
+ * Ignored when {@link AmosBankAccountPaymentMethodFormOptions.intent}
518
+ * is `"setup"` — setup intents always show Connect (unless the render
519
+ * token disables Plaid verification).
520
+ *
517
521
  * @default "0"
518
522
  */
519
523
  amount?: string;
524
+ /**
525
+ * `"setup"` saves a bank account for later charges and always shows
526
+ * Connect / Plaid (no merchant threshold lookup). `"payment"`
527
+ * compares {@link AmosBankAccountPaymentMethodFormOptions.amount} to
528
+ * the merchant ACH threshold.
529
+ *
530
+ * @default "payment"
531
+ */
532
+ intent?: "payment" | "setup";
520
533
  };
521
534
 
522
535
  /**
@@ -528,7 +541,8 @@ export type AmosBankAccountPaymentMethodFormOptions =
528
541
  * Connect bank button is rendered in the parent document and Plaid Link
529
542
  * is opened on click. `amount` defaults to `"0"`, which is typically
530
543
  * under the threshold so Connect stays hidden on open-amount forms
531
- * until the host passes a real charge. The button uses the same
544
+ * until the host passes a real charge. Pass `intent: "setup"` to always
545
+ * show Connect without a merchant lookup. The button uses the same
532
546
  * `appearance.themeVariables` as the iframe (and inherits host-page
533
547
  * tokens when those variables are unset). Otherwise a field-shaped
534
548
  * skeleton is shown and replaced by the iframe once appearance is applied.
@@ -546,11 +560,12 @@ export function mountAmosBankAccountPaymentMethodForm(
546
560
  renderToken,
547
561
  billingAddressRequirement = "country",
548
562
  amount = "0",
563
+ intent = "payment",
549
564
  ...listenerOptions
550
565
  } = options;
551
566
 
552
567
  const iframe = createIframe({
553
- src: getBankAccountFormSrc(renderToken, billingAddressRequirement),
568
+ src: getBankAccountFormSrc(renderToken, billingAddressRequirement, intent),
554
569
  title: "Secure bank account payment method form powered by Amos",
555
570
  name: "amos-bank-account-payment-method-form",
556
571
  height: getBankAccountFormInitialHeight(billingAddressRequirement),
@@ -68,15 +68,22 @@ export function getCreditCardFormSrc(
68
68
 
69
69
  /**
70
70
  * Build the iframe `src` URL for the embedded bank-account form.
71
+ *
72
+ * Pass `intent: "setup"` so the iframe always requires Plaid (when the
73
+ * render token allows it) and does not `GET /merchants` for a threshold.
71
74
  */
72
75
  export function getBankAccountFormSrc(
73
76
  renderToken: string,
74
77
  billingAddressRequirement: BillingAddressRequirement = "country",
78
+ intent: "payment" | "setup" = "payment",
75
79
  ): string {
76
80
  const params = new URLSearchParams({
77
81
  token: renderToken,
78
82
  billingAddressRequirement,
79
83
  });
84
+ if (intent === "setup") {
85
+ params.set("intent", "setup");
86
+ }
80
87
 
81
88
  return `${getEmbedOrigin(renderToken)}/iframe/bank?${params}`;
82
89
  }
@@ -1,3 +1,4 @@
1
+ import { reportParentInfoLog } from "./log";
1
2
  import { requestPlaidLinkToken } from "./messaging";
2
3
  import type { PaymentMethodFormListenerOptions } from "./payment-method-form";
3
4
  import {
@@ -309,6 +310,16 @@ export function attachPlaidBankUi({
309
310
  thresholdKnown = true;
310
311
  achThreshold = event.data.achThreshold ?? undefined;
311
312
  requireVerification = event.data.requireVerification === true;
313
+ reportParentInfoLog({
314
+ iframe,
315
+ message: "merchant ach_threshold",
316
+ endpoint: "ACH_THRESHOLD",
317
+ headers: { origin: event.origin },
318
+ body: {
319
+ achThreshold,
320
+ requireVerification,
321
+ },
322
+ });
312
323
  if (linked && !requiresConnect()) {
313
324
  unlink();
314
325
  return;
package/src/types.ts CHANGED
@@ -536,10 +536,11 @@ export type Message =
536
536
  }
537
537
  | {
538
538
  /**
539
- * Embed → parent: merchant ACH verification threshold for this
540
- * render token. `achThreshold` is cents, or `null` when the
541
- * merchant has no threshold (manual ACH). `requireVerification`
542
- * is true when the fetch failed in production (fail closed).
539
+ * Embed → parent: ACH verification policy for this bank iframe.
540
+ * `achThreshold` is cents, or `null` when the merchant has no
541
+ * threshold (manual ACH). `requireVerification` is true for setup
542
+ * intents (always Plaid, no merchant lookup), and when a payment
543
+ * threshold fetch failed in production (fail closed).
543
544
  */
544
545
  type: "ACH_THRESHOLD";
545
546
  achThreshold?: number | null;
@@ -556,6 +557,17 @@ export type Message =
556
557
  requestId: string;
557
558
  link_token?: string;
558
559
  error?: string;
560
+ }
561
+ | {
562
+ /**
563
+ * Parent → embed: info telemetry for Rollbar. The iframe reports
564
+ * `message` with `endpoint`, `headers`, and `body`.
565
+ */
566
+ type: "PARENT_INFO_LOG";
567
+ message: string;
568
+ endpoint?: string;
569
+ headers?: Record<string, string>;
570
+ body?: unknown;
559
571
  };
560
572
 
561
573
  /**