@invergent/website-widget 2.10.5 → 2.11.1

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/ui.cjs CHANGED
@@ -81,6 +81,14 @@ var SurogatesNetworkError = class extends SurogatesError {
81
81
  this.name = "SurogatesNetworkError";
82
82
  }
83
83
  };
84
+ var SurogatesPaywallError = class extends SurogatesError {
85
+ constructor(message, opts) {
86
+ super(message, { status: 402, ...opts.detail !== void 0 && { detail: opts.detail } });
87
+ this.name = "SurogatesPaywallError";
88
+ this.code = opts.code;
89
+ if (opts.buyUrl !== void 0) this.buyUrl = opts.buyUrl;
90
+ }
91
+ };
84
92
 
85
93
  // src/protocol.ts
86
94
  async function resolvePairing(pairingBase, publishableKey, doFetchImpl = fetch) {
@@ -132,16 +140,27 @@ async function doFetch(url, init) {
132
140
  }
133
141
  return response;
134
142
  }
135
- async function extractDetail(response) {
143
+ async function extractDetailValue(response) {
136
144
  try {
137
145
  const body = await response.json();
138
- return typeof body.detail === "string" ? body.detail : void 0;
146
+ return body?.detail;
139
147
  } catch {
140
148
  return void 0;
141
149
  }
142
150
  }
143
151
  async function raiseForStatus(response, action) {
144
- const detail = await extractDetail(response);
152
+ const detailValue = await extractDetailValue(response);
153
+ const detail = typeof detailValue === "string" ? detailValue : void 0;
154
+ if (response.status === 402) {
155
+ const structured = detailValue && typeof detailValue === "object" ? detailValue : {};
156
+ const code = typeof structured.code === "string" ? structured.code : detail || "payment_required";
157
+ const buyUrl = typeof structured.buy_url === "string" ? structured.buy_url : void 0;
158
+ throw new SurogatesPaywallError(`${action} requires access (402)`, {
159
+ code,
160
+ ...buyUrl !== void 0 && { buyUrl },
161
+ ...detail !== void 0 && { detail }
162
+ });
163
+ }
145
164
  if (response.status === 401 || response.status === 403) {
146
165
  throw new SurogatesAuthError(`${action} rejected (${response.status})`, {
147
166
  status: response.status,
@@ -167,13 +186,18 @@ async function raiseForStatus(response, action) {
167
186
  ...detail !== void 0 && { detail }
168
187
  });
169
188
  }
170
- async function bootstrap(apiUrl, publishableKey, agentId) {
189
+ async function bootstrap(apiUrl, publishableKey, agentId, firebaseIdToken) {
171
190
  const response = await doFetch(apiUrl + withAgentId(PATH_BOOTSTRAP, agentId), {
172
191
  method: "POST",
173
192
  headers: {
174
193
  Authorization: `Bearer ${publishableKey}`,
175
194
  "Content-Type": "application/json"
176
- }
195
+ },
196
+ // Binds a signed-in end user to the visitor session — required
197
+ // before a monetized agent accepts messages. Anonymous
198
+ // bootstraps send no body at all, preserving the historical wire
199
+ // shape byte-for-byte.
200
+ ...firebaseIdToken ? { body: JSON.stringify({ firebase_id_token: firebaseIdToken }) } : {}
177
201
  });
178
202
  if (!response.ok) {
179
203
  await raiseForStatus(response, "Bootstrap");
@@ -477,6 +501,7 @@ var WebsiteAgent = class extends client.AbstractAgent {
477
501
  }
478
502
  this.apiUrl = config.apiUrl.replace(/\/+$/, "");
479
503
  this.publishableKey = config.publishableKey;
504
+ this.getFirebaseIdToken = config.getFirebaseIdToken;
480
505
  }
481
506
  /**
482
507
  * Ensure we have a live session cookie + CSRF token.
@@ -489,10 +514,12 @@ var WebsiteAgent = class extends client.AbstractAgent {
489
514
  */
490
515
  async ensureBootstrapped() {
491
516
  if (this.bootstrapResult) return this.bootstrapResult;
517
+ const firebaseIdToken = this.getFirebaseIdToken ? await this.getFirebaseIdToken() : void 0;
492
518
  this.bootstrapResult = await bootstrap(
493
519
  this.apiUrl,
494
520
  this.publishableKey,
495
- this.agentId
521
+ this.agentId,
522
+ firebaseIdToken ?? void 0
496
523
  );
497
524
  return this.bootstrapResult;
498
525
  }
@@ -586,7 +613,20 @@ var WebsiteAgent = class extends client.AbstractAgent {
586
613
  if (ctx.terminated) return;
587
614
  ctx.terminated = true;
588
615
  const message = err instanceof Error ? err.message : String(err);
589
- const code = err instanceof SurogatesAuthError ? "auth" : "error";
616
+ let code = "error";
617
+ if (err instanceof SurogatesAuthError) {
618
+ code = "auth";
619
+ } else if (err instanceof SurogatesPaywallError) {
620
+ code = "paywall";
621
+ this.lastPaywall = {
622
+ code: err.code,
623
+ ...err.buyUrl !== void 0 && { buyUrl: err.buyUrl }
624
+ };
625
+ if (err.code === "sign_in_required") {
626
+ this.bootstrapResult = void 0;
627
+ this.cursor = 0;
628
+ }
629
+ }
590
630
  subscriber.next({
591
631
  type: client.EventType.RUN_ERROR,
592
632
  message,
@@ -901,6 +941,31 @@ var WIDGET_STYLES = `
901
941
  flex-shrink: 0;
902
942
  }
903
943
 
944
+ .surg-paywall {
945
+ display: flex;
946
+ flex-direction: column;
947
+ gap: 8px;
948
+ margin: 0 16px;
949
+ padding: 12px;
950
+ background: #fffbeb;
951
+ border: 1px solid #fde68a;
952
+ color: #92400e;
953
+ border-radius: 10px;
954
+ font-size: 13px;
955
+ flex-shrink: 0;
956
+ }
957
+ .surg-paywall-cta {
958
+ align-self: flex-start;
959
+ padding: 6px 14px;
960
+ background: var(--surg-accent, #0f172a);
961
+ color: #ffffff;
962
+ border-radius: 8px;
963
+ font-size: 13px;
964
+ font-weight: 600;
965
+ text-decoration: none;
966
+ }
967
+ .surg-paywall-cta:hover { opacity: 0.9; }
968
+
904
969
  .surg-composer {
905
970
  display: flex;
906
971
  gap: 8px;
@@ -1029,6 +1094,21 @@ function snapshot(messages) {
1029
1094
  }
1030
1095
  return out;
1031
1096
  }
1097
+ function safeBuyUrl(url) {
1098
+ return url && /^https?:\/\//i.test(url) ? url : void 0;
1099
+ }
1100
+ function paywallCopy(code) {
1101
+ if (code === "sign_in_required") {
1102
+ return "This assistant requires an account. Get access, sign in on this site, then send your message again.";
1103
+ }
1104
+ if (code === "subscription_required") {
1105
+ return "This assistant requires an active subscription.";
1106
+ }
1107
+ if (code === "insufficient_tokens") {
1108
+ return "You\u2019re out of tokens for this assistant.";
1109
+ }
1110
+ return "This assistant requires paid access.";
1111
+ }
1032
1112
  function friendlyError(err) {
1033
1113
  if (err instanceof SurogatesAuthError) {
1034
1114
  return "This chat isn\u2019t accepting messages from this site yet. The site owner needs to allow this origin.";
@@ -1062,6 +1142,7 @@ function Widget({ agent, config, registerOpenControl, onOpenChange }) {
1062
1142
  const [messages, setMessages] = hooks.useState(() => snapshot(agent.messages));
1063
1143
  const [running, setRunning] = hooks.useState(false);
1064
1144
  const [error, setError] = hooks.useState(null);
1145
+ const [paywall, setPaywall] = hooks.useState(null);
1065
1146
  const [input, setInput] = hooks.useState("");
1066
1147
  const [agentName, setAgentName] = hooks.useState("");
1067
1148
  const title = config.title || agentName || "Assistant";
@@ -1075,10 +1156,16 @@ function Widget({ agent, config, registerOpenControl, onOpenChange }) {
1075
1156
  onRunStartedEvent: () => {
1076
1157
  setRunning(true);
1077
1158
  setError(null);
1159
+ setPaywall(null);
1078
1160
  },
1079
1161
  onRunFinishedEvent: () => setRunning(false),
1080
1162
  onRunErrorEvent: ({ event }) => {
1081
1163
  setRunning(false);
1164
+ const code = event.code;
1165
+ if (code === "paywall") {
1166
+ setPaywall(agent.lastPaywall ?? { code: "payment_required" });
1167
+ return;
1168
+ }
1082
1169
  setError(friendlyError(event));
1083
1170
  }
1084
1171
  });
@@ -1167,6 +1254,19 @@ function Widget({ agent, config, registerOpenControl, onOpenChange }) {
1167
1254
  ] })
1168
1255
  ] }),
1169
1256
  error && /* @__PURE__ */ jsxRuntime.jsx("div", { class: "surg-error", children: error }),
1257
+ paywall && /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "surg-paywall", children: [
1258
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: paywallCopy(paywall.code) }),
1259
+ safeBuyUrl(paywall.buyUrl) && /* @__PURE__ */ jsxRuntime.jsx(
1260
+ "a",
1261
+ {
1262
+ class: "surg-paywall-cta",
1263
+ href: safeBuyUrl(paywall.buyUrl),
1264
+ target: "_blank",
1265
+ rel: "noopener noreferrer",
1266
+ children: "Get access"
1267
+ }
1268
+ )
1269
+ ] }),
1170
1270
  /* @__PURE__ */ jsxRuntime.jsxs("div", { class: "surg-composer", children: [
1171
1271
  /* @__PURE__ */ jsxRuntime.jsx(
1172
1272
  "textarea",