@orangecheck/me-client 0.9.0 → 0.12.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.mjs CHANGED
@@ -38,22 +38,69 @@ async function api(path, opts = {}) {
38
38
  });
39
39
  if (!res.ok) {
40
40
  let message = `me.ochk.io ${res.status}`;
41
+ let errorCode;
42
+ let body;
41
43
  try {
42
44
  const data = await res.json();
43
- if (data?.error) message = data.error;
45
+ body = data;
46
+ if (data?.error) {
47
+ errorCode = data.error;
48
+ message = data.reason ?? data.error;
49
+ }
44
50
  } catch {
45
51
  }
46
- throw new MeClientError(message, res.status);
52
+ let retryAfterSeconds;
53
+ if (res.status === 429) {
54
+ const headerValue = res.headers.get("retry-after");
55
+ if (headerValue) {
56
+ const parsed = Number.parseInt(headerValue, 10);
57
+ if (Number.isFinite(parsed) && parsed >= 0) retryAfterSeconds = parsed;
58
+ }
59
+ if (retryAfterSeconds == null && typeof body?.retry_after_seconds === "number") {
60
+ retryAfterSeconds = body.retry_after_seconds;
61
+ }
62
+ }
63
+ throw new MeClientError(message, res.status, { errorCode, retryAfterSeconds });
47
64
  }
48
65
  return await res.json();
49
66
  }
50
67
  var MeClientError = class extends Error {
51
- constructor(message, status) {
68
+ constructor(message, status, opts) {
52
69
  super(message);
53
70
  this.name = "MeClientError";
54
71
  this.status = status;
72
+ this.errorCode = opts?.errorCode;
73
+ this.retryAfterSeconds = opts?.retryAfterSeconds;
74
+ }
75
+ /** True for the 429 surface · convenience boolean so callers don't
76
+ * have to compare status === 429 every time. */
77
+ get isRateLimited() {
78
+ return this.status === 429;
55
79
  }
56
80
  };
81
+ async function withRateLimitRetry(fn, opts = {}) {
82
+ const maxRetries = opts.maxRetries ?? 2;
83
+ const maxRetryAfter = opts.maxRetryAfterSeconds ?? 30;
84
+ const baseBackoff = opts.baseBackoffMs ?? 250;
85
+ let attempt = 0;
86
+ while (true) {
87
+ try {
88
+ return await fn();
89
+ } catch (err) {
90
+ if (!(err instanceof MeClientError) || !err.isRateLimited) throw err;
91
+ if (attempt >= maxRetries) throw err;
92
+ let waitMs;
93
+ if (err.retryAfterSeconds != null) {
94
+ if (err.retryAfterSeconds > maxRetryAfter) throw err;
95
+ waitMs = err.retryAfterSeconds * 1e3;
96
+ } else {
97
+ waitMs = baseBackoff * 2 ** attempt;
98
+ }
99
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
100
+ attempt++;
101
+ }
102
+ }
103
+ }
57
104
 
58
105
  // src/session.ts
59
106
  var DEFAULT_POLICY = {
@@ -109,7 +156,7 @@ async function invalidate(sessionId) {
109
156
  }
110
157
  var session = { create, refresh, invalidate };
111
158
  function OcSignInButton({
112
- scope,
159
+ scope: scope2,
113
160
  sessionPolicy,
114
161
  returnTo,
115
162
  onSignin,
@@ -130,7 +177,7 @@ function OcSignInButton({
130
177
  if (event2.defaultPrevented) return;
131
178
  setPending(true);
132
179
  try {
133
- const result = await session.create({ scope, sessionPolicy, returnTo });
180
+ const result = await session.create({ scope: scope2, sessionPolicy, returnTo });
134
181
  onSignin?.(result);
135
182
  } catch (err) {
136
183
  onSignInError?.(err instanceof Error ? err : new Error(String(err)));
@@ -272,6 +319,23 @@ async function fire(options) {
272
319
  body: options
273
320
  });
274
321
  }
322
+ async function fireBatch(options) {
323
+ if (!options.project_key) {
324
+ throw new Error("event.fireBatch requires project_key");
325
+ }
326
+ if (!Array.isArray(options.events) || options.events.length === 0) {
327
+ throw new Error("event.fireBatch requires at least one event");
328
+ }
329
+ if (options.events.length > 100) {
330
+ throw new Error(
331
+ `event.fireBatch \xB7 max 100 events per call (got ${options.events.length}). Split client-side.`
332
+ );
333
+ }
334
+ return api("/api/integrator/event/batch", {
335
+ method: "POST",
336
+ body: options
337
+ });
338
+ }
275
339
  function verify(envelope, config2, payment_amount_sats) {
276
340
  const issues = [];
277
341
  const eventCfg = config2.events[envelope.subtype];
@@ -356,7 +420,7 @@ function verify(envelope, config2, payment_amount_sats) {
356
420
  issues
357
421
  };
358
422
  }
359
- var event = { fire, verify };
423
+ var event = { fire, fireBatch, verify };
360
424
 
361
425
  // src/config.ts
362
426
  function validate(cfg) {
@@ -722,9 +786,60 @@ async function defaultLive() {
722
786
  }
723
787
  var federations = { list, live, get, defaultLive };
724
788
 
789
+ // src/scope.ts
790
+ var VALID_SCOPES = [
791
+ "bitcoin_address",
792
+ "email",
793
+ "attest_tier",
794
+ "display_name"
795
+ ];
796
+ async function granted(options) {
797
+ if (!options.project_key) {
798
+ throw new Error("oc.scope.granted: project_key is required");
799
+ }
800
+ const r = await api(
801
+ `/api/integrator/scopes/${encodeURIComponent(options.project_key)}`,
802
+ { method: "GET", signal: options.signal }
803
+ );
804
+ return {
805
+ sub: r.sub,
806
+ scopes_granted: r.scopes_granted,
807
+ scopes: r.scopes
808
+ };
809
+ }
810
+ function request(scopes, options) {
811
+ if (!options.project_key) {
812
+ throw new Error("oc.scope.request: project_key is required");
813
+ }
814
+ if (!options.return_to) {
815
+ throw new Error("oc.scope.request: return_to is required");
816
+ }
817
+ if (!Array.isArray(scopes) || scopes.length === 0) {
818
+ throw new Error("oc.scope.request: scopes must be a non-empty array");
819
+ }
820
+ for (const s of scopes) {
821
+ if (!VALID_SCOPES.includes(s)) {
822
+ throw new Error(
823
+ `oc.scope.request: unknown scope "${s}" \xB7 valid: ${VALID_SCOPES.join(", ")}`
824
+ );
825
+ }
826
+ }
827
+ if (typeof window === "undefined") {
828
+ throw new Error("oc.scope.request: must be called in a browser environment");
829
+ }
830
+ const origin2 = options.origin ?? getOrigin();
831
+ const url = new URL("/me/scope-grant", origin2);
832
+ url.searchParams.set("project_key", options.project_key);
833
+ url.searchParams.set("scopes", scopes.join(","));
834
+ url.searchParams.set("return_to", options.return_to);
835
+ window.location.assign(url.toString());
836
+ throw new Error("oc.scope.request: redirected to consent prompt");
837
+ }
838
+ var scope = { granted, request };
839
+
725
840
  // src/index.ts
726
- var oc = { session, payment, event, config, webhook, federations };
841
+ var oc = { session, payment, event, config, webhook, federations, scope };
727
842
 
728
- export { MIN_INTEGRATOR_PRICE_SATS, MeClientError, OcSignInButton, PLATFORM_FEE_POLICY, clearBearerToken, computeFees, config, event, federations, getBearerToken, getOrigin, oc, onTelemetry, payment, session, setBearerToken, setOrigin, validateIntegratorConfig, webhook };
843
+ export { MIN_INTEGRATOR_PRICE_SATS, MeClientError, OcSignInButton, PLATFORM_FEE_POLICY, clearBearerToken, computeFees, config, event, federations, getBearerToken, getOrigin, oc, onTelemetry, payment, scope, session, setBearerToken, setOrigin, validateIntegratorConfig, webhook, withRateLimitRetry };
729
844
  //# sourceMappingURL=index.mjs.map
730
845
  //# sourceMappingURL=index.mjs.map