tempest-react-sdk 0.76.0 → 0.77.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.
Files changed (142) hide show
  1. package/README.md +2 -2
  2. package/bin/create-tempest-app.e2e.test.mjs +112 -0
  3. package/bin/create-tempest-app.mjs +69 -28
  4. package/bin/lib/component/generate.mjs +191 -0
  5. package/bin/lib/component/generate.test.mjs +336 -0
  6. package/bin/lib/openapi/escape.mjs +210 -0
  7. package/bin/lib/openapi/generate.hostile.test.mjs +592 -0
  8. package/bin/lib/openapi/generate.mjs +93 -17
  9. package/bin/lib/openapi/schema-to-zod.mjs +41 -4
  10. package/bin/tempest.mjs +184 -3
  11. package/dist/auth/create-tempest-auth.cjs +1 -1
  12. package/dist/auth/create-tempest-auth.cjs.map +1 -1
  13. package/dist/auth/create-tempest-auth.js +127 -55
  14. package/dist/auth/create-tempest-auth.js.map +1 -1
  15. package/dist/auth/refresh-queue.cjs +1 -1
  16. package/dist/auth/refresh-queue.cjs.map +1 -1
  17. package/dist/auth/refresh-queue.js +13 -6
  18. package/dist/auth/refresh-queue.js.map +1 -1
  19. package/dist/br/pix.cjs +1 -1
  20. package/dist/br/pix.cjs.map +1 -1
  21. package/dist/br/pix.js +72 -62
  22. package/dist/br/pix.js.map +1 -1
  23. package/dist/br.d.ts +12 -4
  24. package/dist/components/Chat/ChatAttachments.cjs +1 -1
  25. package/dist/components/Chat/ChatAttachments.cjs.map +1 -1
  26. package/dist/components/Chat/ChatAttachments.js +46 -38
  27. package/dist/components/Chat/ChatAttachments.js.map +1 -1
  28. package/dist/components/Markdown/markdown-parse.cjs +7 -5
  29. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  30. package/dist/components/Markdown/markdown-parse.js +151 -87
  31. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  32. package/dist/components/Toast/Toast.module.cjs +1 -1
  33. package/dist/components/Toast/Toast.module.cjs.map +1 -1
  34. package/dist/components/Toast/Toast.module.js +7 -3
  35. package/dist/components/Toast/Toast.module.js.map +1 -1
  36. package/dist/components/Toast/ToastProvider.cjs +1 -1
  37. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  38. package/dist/components/Toast/ToastProvider.js +77 -51
  39. package/dist/components/Toast/ToastProvider.js.map +1 -1
  40. package/dist/data/create-data-provider.cjs +1 -1
  41. package/dist/data/create-data-provider.cjs.map +1 -1
  42. package/dist/data/create-data-provider.js +32 -21
  43. package/dist/data/create-data-provider.js.map +1 -1
  44. package/dist/http/api-client.cjs +1 -1
  45. package/dist/http/api-client.cjs.map +1 -1
  46. package/dist/http/api-client.js +81 -73
  47. package/dist/http/api-client.js.map +1 -1
  48. package/dist/http/errors.cjs +1 -1
  49. package/dist/http/errors.cjs.map +1 -1
  50. package/dist/http/errors.js +39 -35
  51. package/dist/http/errors.js.map +1 -1
  52. package/dist/oauth/callback.cjs +2 -0
  53. package/dist/oauth/callback.cjs.map +1 -0
  54. package/dist/oauth/callback.js +42 -0
  55. package/dist/oauth/callback.js.map +1 -0
  56. package/dist/oauth/use-oauth-callback.cjs +1 -1
  57. package/dist/oauth/use-oauth-callback.cjs.map +1 -1
  58. package/dist/oauth/use-oauth-callback.js +13 -20
  59. package/dist/oauth/use-oauth-callback.js.map +1 -1
  60. package/dist/push/use-push-subscription.cjs +1 -1
  61. package/dist/push/use-push-subscription.cjs.map +1 -1
  62. package/dist/push/use-push-subscription.js +42 -42
  63. package/dist/push/use-push-subscription.js.map +1 -1
  64. package/dist/push/web-push-client.cjs +1 -1
  65. package/dist/push/web-push-client.cjs.map +1 -1
  66. package/dist/push/web-push-client.js +23 -3
  67. package/dist/push/web-push-client.js.map +1 -1
  68. package/dist/query/offline-persistence.cjs +1 -1
  69. package/dist/query/offline-persistence.cjs.map +1 -1
  70. package/dist/query/offline-persistence.js +68 -23
  71. package/dist/query/offline-persistence.js.map +1 -1
  72. package/dist/styles/component/Toast.css +6 -1
  73. package/dist/styles/feedback.css +6 -1
  74. package/dist/styles.css +1 -1
  75. package/dist/sw/background-sync.cjs +1 -1
  76. package/dist/sw/background-sync.cjs.map +1 -1
  77. package/dist/sw/background-sync.js +271 -60
  78. package/dist/sw/background-sync.js.map +1 -1
  79. package/dist/sw/cache-inspect.cjs.map +1 -1
  80. package/dist/sw/cache-inspect.js.map +1 -1
  81. package/dist/sw/cache.cjs +1 -1
  82. package/dist/sw/cache.cjs.map +1 -1
  83. package/dist/sw/cache.js +91 -64
  84. package/dist/sw/cache.js.map +1 -1
  85. package/dist/sw/credential-headers.cjs +2 -0
  86. package/dist/sw/credential-headers.cjs.map +1 -0
  87. package/dist/sw/credential-headers.js +17 -0
  88. package/dist/sw/credential-headers.js.map +1 -0
  89. package/dist/sw/session-data.cjs +2 -0
  90. package/dist/sw/session-data.cjs.map +1 -0
  91. package/dist/sw/session-data.js +17 -0
  92. package/dist/sw/session-data.js.map +1 -0
  93. package/dist/sw.cjs +1 -1
  94. package/dist/sw.d.ts +271 -23
  95. package/dist/sw.js +7 -5
  96. package/dist/telemetry/app-error-owner.cjs +2 -0
  97. package/dist/telemetry/app-error-owner.cjs.map +1 -0
  98. package/dist/telemetry/app-error-owner.js +18 -0
  99. package/dist/telemetry/app-error-owner.js.map +1 -0
  100. package/dist/telemetry/app-error-queue.cjs +1 -1
  101. package/dist/telemetry/app-error-queue.cjs.map +1 -1
  102. package/dist/telemetry/app-error-queue.js +19 -14
  103. package/dist/telemetry/app-error-queue.js.map +1 -1
  104. package/dist/telemetry/app-error-reporter.cjs +1 -1
  105. package/dist/telemetry/app-error-reporter.cjs.map +1 -1
  106. package/dist/telemetry/app-error-reporter.js +75 -34
  107. package/dist/telemetry/app-error-reporter.js.map +1 -1
  108. package/dist/telemetry/app-error-transport.cjs +1 -1
  109. package/dist/telemetry/app-error-transport.cjs.map +1 -1
  110. package/dist/telemetry/app-error-transport.js +16 -3
  111. package/dist/telemetry/app-error-transport.js.map +1 -1
  112. package/dist/tempest-react-sdk.cjs +1 -1
  113. package/dist/tempest-react-sdk.d.ts +979 -62
  114. package/dist/tempest-react-sdk.js +160 -156
  115. package/dist/utils/csv.cjs +3 -3
  116. package/dist/utils/csv.cjs.map +1 -1
  117. package/dist/utils/csv.js +16 -13
  118. package/dist/utils/csv.js.map +1 -1
  119. package/dist/utils/safe-url.cjs +2 -0
  120. package/dist/utils/safe-url.cjs.map +1 -0
  121. package/dist/utils/safe-url.js +39 -0
  122. package/dist/utils/safe-url.js.map +1 -0
  123. package/dist/vite/tempest-csp.cjs +2 -2
  124. package/dist/vite/tempest-csp.cjs.map +1 -1
  125. package/dist/vite/tempest-csp.js +118 -63
  126. package/dist/vite/tempest-csp.js.map +1 -1
  127. package/dist/vite.d.ts +21 -5
  128. package/dist/webrtc/peer-link.cjs +1 -1
  129. package/dist/webrtc/peer-link.cjs.map +1 -1
  130. package/dist/webrtc/peer-link.js +2 -10
  131. package/dist/webrtc/peer-link.js.map +1 -1
  132. package/dist/webrtc/peer-mesh.cjs +1 -1
  133. package/dist/webrtc/peer-mesh.cjs.map +1 -1
  134. package/dist/webrtc/peer-mesh.js +110 -72
  135. package/dist/webrtc/peer-mesh.js.map +1 -1
  136. package/package.json +1 -1
  137. package/template-pwa/src/pwa.tsx +22 -0
  138. package/template-pwa/src/sw.ts +27 -14
  139. package/dist/components/Markdown/markdown-url.cjs +0 -2
  140. package/dist/components/Markdown/markdown-url.cjs.map +0 -1
  141. package/dist/components/Markdown/markdown-url.js +0 -28
  142. package/dist/components/Markdown/markdown-url.js.map +0 -1
@@ -1,77 +1,149 @@
1
- import { createApiClient as e } from "../http/api-client.js";
2
- import { createAuthStore as t } from "./create-auth-store.js";
3
- import { createRefreshQueue as n } from "./refresh-queue.js";
1
+ import { SessionSupersededError as e } from "../http/errors.js";
2
+ import { createApiClient as t } from "../http/api-client.js";
3
+ import { createAuthStore as n } from "./create-auth-store.js";
4
+ import { createRefreshQueue as r } from "./refresh-queue.js";
4
5
  //#region src/auth/create-tempest-auth.ts
5
- function r(e) {
6
+ function i(e) {
6
7
  let t = e ?? {};
7
8
  return {
8
9
  token: t.access_token,
9
10
  refreshToken: t.refresh_token
10
11
  };
11
12
  }
12
- function i(i) {
13
- let { baseURL: a, prefix: o, loginPath: s = "/api/auth/login", refreshPath: c = "/api/auth/refresh", mePath: l, storeName: u = "tempest-auth", storage: d = "local", withCredentials: f = !1, csrf: p, fetcher: m, parseTokens: h = r, parseUser: g, refreshBody: _ = (e) => e ? { refresh_token: e } : void 0, retry: v, redirectTo: y } = i, b = t({
14
- name: u,
15
- storage: d
16
- }), x = `${u}-refresh`;
17
- function S() {
18
- return typeof window > "u" ? null : d === "session" ? window.sessionStorage : window.localStorage;
13
+ function a(a) {
14
+ let { baseURL: o, prefix: s, loginPath: c = "/api/auth/login", refreshPath: l = "/api/auth/refresh", mePath: u, storeName: d = "tempest-auth", storage: f = "local", withCredentials: p = !1, csrf: m, fetcher: h, parseTokens: g = i, parseUser: _, refreshBody: v = (e) => e ? { refresh_token: e } : void 0, retry: y, redirectTo: b, onSessionEnd: x } = a, S = n({
15
+ name: d,
16
+ storage: f
17
+ }), C = `${d}-refresh`;
18
+ function w() {
19
+ return typeof window > "u" ? null : f === "session" ? window.sessionStorage : window.localStorage;
19
20
  }
20
- function C() {
21
- return S()?.getItem(x) ?? null;
21
+ function T() {
22
+ return w()?.getItem(C) ?? null;
22
23
  }
23
- function w(e) {
24
- let t = S();
25
- t && (e ? t.setItem(x, e) : t.removeItem(x));
24
+ function E(e) {
25
+ let t = w();
26
+ t && (e ? t.setItem(C, e) : t.removeItem(C));
26
27
  }
27
- let T = () => b.getState(), E = () => T().token;
28
- async function D() {
29
- if (!l) return T().user;
30
- let e = await M.get(l, { skipAuthRetry: !0 });
31
- return T().setUser(e), e;
28
+ let D = () => S.getState(), O = () => D().token;
29
+ async function k() {
30
+ if (!u) return D().user;
31
+ let e = await U.get(u, { skipAuthRetry: !0 });
32
+ return D().setUser(e), e;
32
33
  }
33
- async function O(e) {
34
- let t = await M.post(s, {
35
- body: e,
36
- skipAuth: !0
37
- }), { token: n, refreshToken: r } = h(t);
38
- T().setToken(n), w(r ?? null);
39
- let i = g?.(t);
40
- return i == null ? D() : (T().setUser(i), i);
34
+ let A = 0, j = Promise.resolve(), M = 0;
35
+ function N() {
36
+ A += 1, F = r(P);
37
+ }
38
+ async function P() {
39
+ let t = A, n;
40
+ try {
41
+ n = await U.post(l, {
42
+ body: v(T()),
43
+ skipAuth: !0
44
+ });
45
+ } catch (n) {
46
+ throw A === t ? n : new e();
47
+ }
48
+ if (A !== t) throw new e();
49
+ let { token: r, refreshToken: i } = g(n);
50
+ D().setToken(r), i && E(i);
41
51
  }
42
- function k() {
43
- T().logout(), w(null);
52
+ let F = r(P);
53
+ function I(e) {
54
+ let t = O();
55
+ return t === null ? !1 : e.token === null || e.token !== t;
44
56
  }
45
- function A() {
46
- k(), y && typeof window < "u" && window.location.assign(y);
57
+ async function L(t) {
58
+ if (t.epoch !== null && t.epoch !== A) throw new e();
59
+ if (I(t)) {
60
+ if (t.token === null) throw Error("anonymous 401 under a live session");
61
+ return;
62
+ }
63
+ return F();
47
64
  }
48
- let j = n(async () => {
49
- let e = await M.post(c, {
50
- body: _(C()),
65
+ async function R(t, n) {
66
+ if (n.epoch !== null && n.epoch !== A) throw new e();
67
+ I(n) || await H();
68
+ }
69
+ async function z(t) {
70
+ let n = A, r = await U.post(c, {
71
+ body: t,
51
72
  skipAuth: !0
52
- }), { token: t, refreshToken: n } = h(e);
53
- T().setToken(t), n && w(n);
54
- }), M = e({
55
- baseURL: a,
56
- prefix: o,
57
- withCredentials: f,
58
- csrf: p,
59
- fetcher: m,
60
- getToken: E,
61
- refresh: j,
62
- retry: v,
63
- onUnauthorized: () => A()
73
+ });
74
+ if (A !== n) throw new e();
75
+ let { token: i, refreshToken: a } = g(r), { done: o } = B("switch"), s = A;
76
+ if (await o.catch(() => void 0), A !== s) throw new e();
77
+ D().setToken(i), E(a ?? null);
78
+ let l = _?.(r);
79
+ return l == null ? k() : (D().setUser(l), l);
80
+ }
81
+ function B(e) {
82
+ N();
83
+ let { user: n, token: r } = D(), i = r !== null || n !== null || T() !== null;
84
+ if (D().logout(), E(null), !i) return {
85
+ live: i,
86
+ done: e === "switch" || M === 0 ? j : Promise.resolve()
87
+ };
88
+ if (!x) return {
89
+ live: i,
90
+ done: Promise.resolve()
91
+ };
92
+ let a = t({
93
+ baseURL: o,
94
+ prefix: s,
95
+ withCredentials: p,
96
+ csrf: m,
97
+ fetcher: h,
98
+ getToken: () => r
99
+ });
100
+ M += 1;
101
+ let c = (async () => {
102
+ try {
103
+ await x({
104
+ reason: e,
105
+ user: n,
106
+ token: r,
107
+ api: a
108
+ });
109
+ } finally {
110
+ --M;
111
+ }
112
+ })();
113
+ return j = c.then(() => void 0, () => void 0), {
114
+ live: i,
115
+ done: c.then(() => void 0)
116
+ };
117
+ }
118
+ function V() {
119
+ return B("logout").done;
120
+ }
121
+ async function H() {
122
+ let { live: e, done: t } = B("expired");
123
+ await t.catch(() => void 0), e && b && typeof window < "u" && window.location.assign(b);
124
+ }
125
+ let U = t({
126
+ baseURL: o,
127
+ prefix: s,
128
+ withCredentials: p,
129
+ csrf: m,
130
+ fetcher: h,
131
+ getToken: O,
132
+ getSessionEpoch: () => A,
133
+ refresh: L,
134
+ retry: y,
135
+ onUnauthorized: R
64
136
  });
65
137
  return {
66
- useAuthStore: b,
67
- api: M,
68
- login: O,
69
- logout: k,
70
- refresh: j,
71
- getToken: E
138
+ useAuthStore: S,
139
+ api: U,
140
+ login: z,
141
+ logout: V,
142
+ refresh: () => F(),
143
+ getToken: O
72
144
  };
73
145
  }
74
146
  //#endregion
75
- export { i as createTempestAuth };
147
+ export { a as createTempestAuth };
76
148
 
77
149
  //# sourceMappingURL=create-tempest-auth.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-tempest-auth.js","names":[],"sources":["../../src/auth/create-tempest-auth.ts"],"sourcesContent":["import { createApiClient } from \"../http\";\nimport type { ApiClient, CsrfOptions, RetryOptions } from \"../http\";\nimport { createAuthStore } from \"./create-auth-store\";\nimport type { AuthState } from \"./create-auth-store\";\nimport { createRefreshQueue } from \"./refresh-queue\";\n\n/** The token envelope returned by a Tempest FastAPI SDK login/refresh route. */\nexport interface TempestTokenResponse {\n /** The bearer access token. */\n access_token: string;\n /** Token type — always `\"bearer\"` for the SDK. */\n token_type?: string;\n /** Optional refresh token (when the API returns it in the body, not a cookie). */\n refresh_token?: string;\n}\n\nexport interface CreateTempestAuthOptions<TUser> {\n /** Base URL of the API. Required. */\n baseURL: string;\n /**\n * Path segment the API is mounted under, such as `\"/api\"`, forwarded to\n * every client this preset builds — see the `prefix` option of\n * `createApiClient`.\n *\n * Safe to combine with the default paths below: the prefix is applied at\n * most once, so `\"/api/auth/login\"` under a `\"/api\"` prefix stays\n * `\"/api/auth/login\"`.\n */\n prefix?: string;\n /** Login route (`POST`). Default: `\"/api/auth/login\"`. */\n loginPath?: string;\n /** Refresh route (`POST`). Default: `\"/api/auth/refresh\"`. */\n refreshPath?: string;\n /** Optional current-user route (`GET`) called after login/refresh. */\n mePath?: string;\n /** Persist key for the store. Default: `\"tempest-auth\"`. */\n storeName?: string;\n /** Storage backend. Default: `\"local\"`. */\n storage?: \"local\" | \"session\";\n /** Send cookies (needed when the refresh token lives in an httpOnly cookie). */\n withCredentials?: boolean;\n /**\n * CSRF token for every write, forwarded to `createApiClient`'s `csrf`. Off\n * by default; `true` echoes the `csrf_token` cookie as `X-CSRF-Token`.\n *\n * The pair to `withCredentials`: once the refresh token lives in a cookie,\n * the browser attaches it to a `POST /auth/refresh` that another site\n * triggers. Login and refresh carry the header too — they go out with\n * `skipAuth`, and the CSRF header does not follow auth.\n */\n csrf?: boolean | CsrfOptions;\n /** Custom fetch implementation (testing / SSR). Defaults to `globalThis.fetch`. */\n fetcher?: typeof fetch;\n /**\n * Extract tokens from a login/refresh response. Default reads\n * `access_token` + `refresh_token`.\n */\n parseTokens?: (data: unknown) => { token: string; refreshToken?: string };\n /** Pull the user out of the login response, when the API embeds it. */\n parseUser?: (data: unknown) => TUser | null;\n /**\n * Build the refresh request body. Default sends `{ refresh_token }` when a\n * refresh token is stored, else `undefined` (cookie-based refresh).\n */\n refreshBody?: (refreshToken: string | null) => unknown;\n /**\n * Retry policy for `api`, forwarded to {@link createApiClient}. Off by\n * default; `true` enables the conservative built-in policy, which never\n * replays a write.\n */\n retry?: boolean | RetryOptions;\n /**\n * Where to send the browser when the session ends — a hard navigation via\n * `window.location.assign`, after the store is cleared.\n *\n * **Prefer leaving this unset.** `logout()` already clears the store, so a\n * `<RouteGuard when={isAuthenticated} redirectTo=\"/login\">` wrapped around\n * the protected area navigates on its own, which keeps the SPA alive and the\n * router history intact. Reach for this only when the expiry can happen\n * outside any guarded subtree and a full reload is acceptable.\n */\n redirectTo?: string;\n}\n\nexport interface TempestAuth<TUser, TCredentials> {\n /** The persisted Zustand auth store hook (compatible with `<AuthGuard>`). */\n useAuthStore: ReturnType<typeof createAuthStore<TUser>>;\n /** A `createApiClient` wired with bearer auth + 401 → refresh → retry. */\n api: ApiClient;\n /** Authenticate, store the session, and resolve the user (or null). */\n login: (credentials: TCredentials) => Promise<TUser | null>;\n /** Clear the session (and the stored refresh token). */\n logout: () => void;\n /** Refresh the access token (deduplicated across concurrent callers). */\n refresh: () => Promise<void>;\n /** The current access token, or null. */\n getToken: () => string | null;\n}\n\nfunction defaultParseTokens(data: unknown): { token: string; refreshToken?: string } {\n const d = (data ?? {}) as TempestTokenResponse;\n return { token: d.access_token, refreshToken: d.refresh_token };\n}\n\n/**\n * Turn-key auth preset wiring `createAuthStore` + `createRefreshQueue` +\n * `createApiClient` to the Tempest FastAPI SDK auth contract: login returns\n * `{ access_token, token_type }`, requests carry `Authorization: Bearer`, and a\n * `401` triggers a single deduplicated refresh + replay.\n *\n * The session is cleared whenever that path ends unauthorized anyway — the\n * refresh threw, or the replay came back `401` — so a refresh token that the\n * backend has revoked cannot leave the app holding a dead session. With\n * `redirectTo` set, the browser also leaves the page; without it, clearing the\n * store is enough for a `<RouteGuard>` to navigate on its own.\n *\n * @example\n * const auth = createTempestAuth<User, { email: string; password: string }>({\n * baseURL: import.meta.env.VITE_API_URL,\n * mePath: \"/api/auth/me\",\n * });\n *\n * await auth.login({ email, password }); // stores session, returns the user\n * const orders = await auth.api.get(\"/api/orders\"); // sends the bearer token\n * auth.logout();\n *\n * @param options - The auth configuration.\n * @returns The store hook, a wired API client, and login/logout/refresh helpers.\n */\nexport function createTempestAuth<TUser, TCredentials = { email: string; password: string }>(\n options: CreateTempestAuthOptions<TUser>,\n): TempestAuth<TUser, TCredentials> {\n const {\n baseURL,\n prefix,\n loginPath = \"/api/auth/login\",\n refreshPath = \"/api/auth/refresh\",\n mePath,\n storeName = \"tempest-auth\",\n storage = \"local\",\n withCredentials = false,\n csrf,\n fetcher,\n parseTokens = defaultParseTokens,\n parseUser,\n refreshBody = (rt) => (rt ? { refresh_token: rt } : undefined),\n retry,\n redirectTo,\n } = options;\n\n const useAuthStore = createAuthStore<TUser>({ name: storeName, storage });\n const refreshKey = `${storeName}-refresh`;\n\n function storageImpl(): Storage | null {\n if (typeof window === \"undefined\") return null;\n return storage === \"session\" ? window.sessionStorage : window.localStorage;\n }\n function readRefreshToken(): string | null {\n return storageImpl()?.getItem(refreshKey) ?? null;\n }\n function writeRefreshToken(token: string | null): void {\n const s = storageImpl();\n if (!s) return;\n if (token) s.setItem(refreshKey, token);\n else s.removeItem(refreshKey);\n }\n\n const state = (): AuthState<TUser> => useAuthStore.getState();\n const getToken = (): string | null => state().token;\n\n /**\n * Load the current user, without letting a `401` end the session.\n *\n * `skipAuthRetry` is what keeps this one client honest: the call is\n * authenticated, but a `401` here surfaces instead of triggering a refresh\n * and `onUnauthorized`. That was the behaviour of the throwaway client this\n * used to build per invocation, and it is the reason the preset needed\n * three clients before 0.66.0.\n */\n async function fetchUser(): Promise<TUser | null> {\n if (!mePath) return state().user;\n const user = await api.get<TUser>(mePath, { skipAuthRetry: true });\n state().setUser(user);\n return user;\n }\n\n async function login(credentials: TCredentials): Promise<TUser | null> {\n const data = await api.post<unknown>(loginPath, {\n body: credentials,\n skipAuth: true,\n });\n const { token, refreshToken } = parseTokens(data);\n state().setToken(token);\n writeRefreshToken(refreshToken ?? null);\n const embedded = parseUser?.(data);\n if (embedded != null) {\n state().setUser(embedded);\n return embedded;\n }\n return fetchUser();\n }\n\n function logout(): void {\n state().logout();\n writeRefreshToken(null);\n }\n\n /**\n * Clear the session and, when `redirectTo` is set, leave the page.\n *\n * Separate from `logout` so an explicit sign-out stays a pure state change:\n * a caller that already navigates itself would otherwise get a second,\n * competing navigation.\n */\n function endSession(): void {\n logout();\n if (redirectTo && typeof window !== \"undefined\") {\n window.location.assign(redirectTo);\n }\n }\n\n const refresh = createRefreshQueue(async () => {\n const data = await api.post<unknown>(refreshPath, {\n body: refreshBody(readRefreshToken()),\n skipAuth: true,\n });\n const { token, refreshToken } = parseTokens(data);\n state().setToken(token);\n if (refreshToken) writeRefreshToken(refreshToken);\n });\n\n const api = createApiClient({\n baseURL,\n prefix,\n withCredentials,\n csrf,\n fetcher,\n getToken,\n refresh,\n retry,\n onUnauthorized: () => endSession(),\n });\n\n return { useAuthStore, api, login, logout, refresh, getToken };\n}\n"],"mappings":";;;;AAmGA,SAAS,EAAmB,GAAyD;CACjF,IAAM,IAAK,KAAQ,CAAC;CACpB,OAAO;EAAE,OAAO,EAAE;EAAc,cAAc,EAAE;CAAc;AAClE;AA2BA,SAAgB,EACZ,GACgC;CAChC,IAAM,EACF,YACA,WACA,eAAY,mBACZ,iBAAc,qBACd,WACA,eAAY,gBACZ,aAAU,SACV,qBAAkB,IAClB,SACA,YACA,iBAAc,GACd,cACA,kBAAe,MAAQ,IAAK,EAAE,eAAe,EAAG,IAAI,KAAA,GACpD,UACA,kBACA,GAEE,IAAe,EAAuB;EAAE,MAAM;EAAW;CAAQ,CAAC,GAClE,IAAa,GAAG,EAAU;CAEhC,SAAS,IAA8B;EAEnC,OADI,OAAO,SAAW,MAAoB,OACnC,MAAY,YAAY,OAAO,iBAAiB,OAAO;CAClE;CACA,SAAS,IAAkC;EACvC,OAAO,EAAY,CAAC,EAAE,QAAQ,CAAU,KAAK;CACjD;CACA,SAAS,EAAkB,GAA4B;EACnD,IAAM,IAAI,EAAY;EACjB,MACD,IAAO,EAAE,QAAQ,GAAY,CAAK,IACjC,EAAE,WAAW,CAAU;CAChC;CAEA,IAAM,UAAgC,EAAa,SAAS,GACtD,UAAgC,EAAM,CAAC,CAAC;CAW9C,eAAe,IAAmC;EAC9C,IAAI,CAAC,GAAQ,OAAO,EAAM,CAAC,CAAC;EAC5B,IAAM,IAAO,MAAM,EAAI,IAAW,GAAQ,EAAE,eAAe,GAAK,CAAC;EAEjE,OADA,EAAM,CAAC,CAAC,QAAQ,CAAI,GACb;CACX;CAEA,eAAe,EAAM,GAAkD;EACnE,IAAM,IAAO,MAAM,EAAI,KAAc,GAAW;GAC5C,MAAM;GACN,UAAU;EACd,CAAC,GACK,EAAE,UAAO,oBAAiB,EAAY,CAAI;EAEhD,AADA,EAAM,CAAC,CAAC,SAAS,CAAK,GACtB,EAAkB,KAAgB,IAAI;EACtC,IAAM,IAAW,IAAY,CAAI;EAKjC,OAJI,KAAY,OAIT,EAAU,KAHb,EAAM,CAAC,CAAC,QAAQ,CAAQ,GACjB;CAGf;CAEA,SAAS,IAAe;EAEpB,AADA,EAAM,CAAC,CAAC,OAAO,GACf,EAAkB,IAAI;CAC1B;CASA,SAAS,IAAmB;EAExB,AADA,EAAO,GACH,KAAc,OAAO,SAAW,OAChC,OAAO,SAAS,OAAO,CAAU;CAEzC;CAEA,IAAM,IAAU,EAAmB,YAAY;EAC3C,IAAM,IAAO,MAAM,EAAI,KAAc,GAAa;GAC9C,MAAM,EAAY,EAAiB,CAAC;GACpC,UAAU;EACd,CAAC,GACK,EAAE,UAAO,oBAAiB,EAAY,CAAI;EAEhD,AADA,EAAM,CAAC,CAAC,SAAS,CAAK,GAClB,KAAc,EAAkB,CAAY;CACpD,CAAC,GAEK,IAAM,EAAgB;EACxB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,sBAAsB,EAAW;CACrC,CAAC;CAED,OAAO;EAAE;EAAc;EAAK;EAAO;EAAQ;EAAS;CAAS;AACjE"}
1
+ {"version":3,"file":"create-tempest-auth.js","names":[],"sources":["../../src/auth/create-tempest-auth.ts"],"sourcesContent":["import { createApiClient, SessionSupersededError } from \"../http\";\nimport type { ApiClient, CsrfOptions, RequestAuthContext, RetryOptions } from \"../http\";\nimport { createAuthStore } from \"./create-auth-store\";\nimport type { AuthState } from \"./create-auth-store\";\nimport { createRefreshQueue } from \"./refresh-queue\";\n\n/** The token envelope returned by a Tempest FastAPI SDK login/refresh route. */\nexport interface TempestTokenResponse {\n /** The bearer access token. */\n access_token: string;\n /** Token type — always `\"bearer\"` for the SDK. */\n token_type?: string;\n /** Optional refresh token (when the API returns it in the body, not a cookie). */\n refresh_token?: string;\n}\n\n/**\n * What {@link CreateTempestAuthOptions.onSessionEnd} receives about the session\n * that just ended.\n */\nexport interface TempestSessionEnd<TUser> {\n /**\n * Why it ended: `\"logout\"` for an explicit `logout()`, `\"expired\"` when the\n * client ended it on its own — the refresh failed, or the request replayed\n * after a refresh still came back `401` — and `\"switch\"` when `login()`\n * succeeded for another set of credentials while this session was live.\n */\n reason: \"logout\" | \"expired\" | \"switch\";\n /** The user of the ended session, as the store held it. */\n user: TUser | null;\n /** The access token of the ended session, or `null` when it had none. */\n token: string | null;\n /**\n * A client that sends the ended session's access token — the store has\n * already been cleared, so `auth.api` no longer does.\n *\n * Use it for the server-side half of the teardown, such as deleting this\n * browser's push subscription. It never refreshes and never ends a session:\n * a `401` here just rejects. On `\"logout\"` the token is usually still\n * valid; on `\"expired\"` the server has already refused it, so expect the\n * call to fail and keep the local half of the teardown independent of it.\n */\n api: ApiClient;\n}\n\nexport interface CreateTempestAuthOptions<TUser> {\n /** Base URL of the API. Required. */\n baseURL: string;\n /**\n * Path segment the API is mounted under, such as `\"/api\"`, forwarded to\n * every client this preset builds — see the `prefix` option of\n * `createApiClient`.\n *\n * Safe to combine with the default paths below: the prefix is applied at\n * most once, so `\"/api/auth/login\"` under a `\"/api\"` prefix stays\n * `\"/api/auth/login\"`.\n */\n prefix?: string;\n /** Login route (`POST`). Default: `\"/api/auth/login\"`. */\n loginPath?: string;\n /** Refresh route (`POST`). Default: `\"/api/auth/refresh\"`. */\n refreshPath?: string;\n /** Optional current-user route (`GET`) called after login/refresh. */\n mePath?: string;\n /** Persist key for the store. Default: `\"tempest-auth\"`. */\n storeName?: string;\n /** Storage backend. Default: `\"local\"`. */\n storage?: \"local\" | \"session\";\n /** Send cookies (needed when the refresh token lives in an httpOnly cookie). */\n withCredentials?: boolean;\n /**\n * CSRF token for every write, forwarded to `createApiClient`'s `csrf`. Off\n * by default; `true` echoes the `csrf_token` cookie as `X-CSRF-Token`.\n *\n * The pair to `withCredentials`: once the refresh token lives in a cookie,\n * the browser attaches it to a `POST /auth/refresh` that another site\n * triggers. Login and refresh carry the header too — they go out with\n * `skipAuth`, and the CSRF header does not follow auth.\n */\n csrf?: boolean | CsrfOptions;\n /** Custom fetch implementation (testing / SSR). Defaults to `globalThis.fetch`. */\n fetcher?: typeof fetch;\n /**\n * Extract tokens from a login/refresh response. Default reads\n * `access_token` + `refresh_token`.\n */\n parseTokens?: (data: unknown) => { token: string; refreshToken?: string };\n /** Pull the user out of the login response, when the API embeds it. */\n parseUser?: (data: unknown) => TUser | null;\n /**\n * Build the refresh request body. Default sends `{ refresh_token }` when a\n * refresh token is stored, else `undefined` (cookie-based refresh).\n */\n refreshBody?: (refreshToken: string | null) => unknown;\n /**\n * Retry policy for `api`, forwarded to {@link createApiClient}. Off by\n * default; `true` enables the conservative built-in policy, which never\n * replays a write.\n */\n retry?: boolean | RetryOptions;\n /**\n * Where to send the browser when the session ends — a hard navigation via\n * `window.location.assign`, after the store is cleared.\n *\n * **Prefer leaving this unset.** `logout()` already clears the store, so a\n * `<RouteGuard when={isAuthenticated} redirectTo=\"/login\">` wrapped around\n * the protected area navigates on its own, which keeps the SPA alive and the\n * router history intact. Reach for this only when the expiry can happen\n * outside any guarded subtree and a full reload is acceptable.\n */\n redirectTo?: string;\n /**\n * Tear down per-user state when a session ends — on an explicit `logout()`,\n * when the client ends the session on its own after a failed refresh, and\n * when a successful `login()` replaces a live session. Without it, the automatic path had no place to hook into, and\n * state derived from the user (a push subscription, a persisted query\n * cache, a background-sync queue) survived into the next account.\n *\n * Order, on every path:\n *\n * 1. any refresh still in flight is superseded, so it cannot reinstall the\n * ending session's tokens;\n * 2. the store and the stored refresh token are cleared — synchronously, so\n * `isAuthenticated` is already `false` when `logout()` returns;\n * 3. this hook runs with a {@link TempestSessionEnd} whose `api` still sends\n * the ended session's access token;\n * 4. on `\"expired\"` with `redirectTo` set, the browser navigates — after the\n * hook settled, even if it threw; on `\"switch\"`, the new session is\n * installed — after the hook settled, even if it threw.\n *\n * It may be async. `logout()` resolves when it settles and rejects with what\n * it threw; on the automatic path the error is swallowed, because the\n * request that ended the session already rejects with its own `401`. It\n * runs once per session: a second `logout()`, or the other requests of a\n * burst of `401`s, find nothing left to end and skip it.\n *\n * On `\"switch\"` the hook runs only **after** the new credentials were\n * accepted, so a failed `login()` leaves the live session untouched. It runs\n * **before** the new session is installed: teardown of the previous\n * account's state (clearing a persisted cache, a background-sync queue) has\n * to finish before the new account's requests start filling them, or it\n * would wipe — or race with — the new account's data. The cost is that\n * `isAuthenticated` is `false` while the hook runs, which a route guard may\n * act on; keep the hook short. Its error is swallowed there, since the\n * login itself succeeded.\n *\n * Inside the hook, reach the backend only through `session.api`. `auth.api`\n * goes out with no token; `auth.login()` waits on the teardown in progress —\n * this hook — and never settles. Telling the hook apart from an outside\n * caller would need an async context the browser does not have, so this is\n * a documented rule, not a runtime check.\n */\n onSessionEnd?: (session: TempestSessionEnd<TUser>) => void | Promise<void>;\n}\n\nexport interface TempestAuth<TUser, TCredentials> {\n /** The persisted Zustand auth store hook (compatible with `<AuthGuard>`). */\n useAuthStore: ReturnType<typeof createAuthStore<TUser>>;\n /** A `createApiClient` wired with bearer auth + 401 → refresh → retry. */\n api: ApiClient;\n /** Authenticate, store the session, and resolve the user (or null). */\n login: (credentials: TCredentials) => Promise<TUser | null>;\n /**\n * End the session: supersede any refresh in flight, clear the store and the\n * stored refresh token, then run `onSessionEnd`.\n *\n * The store is cleared synchronously, before the first `await`, so callers\n * that ignore the promise see the signed-out state at once. The promise\n * resolves when `onSessionEnd` settled and rejects with its error.\n */\n logout: () => Promise<void>;\n /**\n * Refresh the access token (deduplicated across concurrent callers).\n *\n * @throws {SessionSupersededError} When the session ended or changed while\n * the refresh was in flight; its tokens were discarded.\n */\n refresh: () => Promise<void>;\n /** The current access token, or null. */\n getToken: () => string | null;\n}\n\nfunction defaultParseTokens(data: unknown): { token: string; refreshToken?: string } {\n const d = (data ?? {}) as TempestTokenResponse;\n return { token: d.access_token, refreshToken: d.refresh_token };\n}\n\n/**\n * Turn-key auth preset wiring `createAuthStore` + `createRefreshQueue` +\n * `createApiClient` to the Tempest FastAPI SDK auth contract: login returns\n * `{ access_token, token_type }`, requests carry `Authorization: Bearer`, and a\n * `401` triggers a single deduplicated refresh + replay.\n *\n * The session is cleared whenever that path ends unauthorized anyway — the\n * refresh threw, or the replay came back `401` — so a refresh token that the\n * backend has revoked cannot leave the app holding a dead session. With\n * `redirectTo` set, the browser also leaves the page; without it, clearing the\n * store is enough for a `<RouteGuard>` to navigate on its own.\n *\n * Every refresh is bound to a **session epoch**, bumped by `logout()` and by a\n * `login()` whose credentials were accepted. A refresh that resolves after its epoch ended discards\n * what it brought back and rejects with {@link SessionSupersededError}, which\n * the client passes through without ending the new session. Before 0.77.0 a\n * refresh that was in flight during `logout()` reinstalled the old tokens —\n * and after `logout()` + `login(B)`, replaced B's token with A's while the UI\n * showed B (#435). A `401` from a request sent under an ended session is\n * refused the same way, instead of being replayed as the account signed in now.\n *\n * Refreshes are collapsed per rotation, not just per overlap: a `401` whose\n * request left with a token that has since been replaced in the same session\n * replays with the new token and refreshes nothing. Three expired requests take\n * one refresh whether their `401`s arrive together or one after another.\n *\n * @example\n * const auth = createTempestAuth<User, { email: string; password: string }>({\n * baseURL: import.meta.env.VITE_API_URL,\n * mePath: \"/api/auth/me\",\n * });\n *\n * await auth.login({ email, password }); // stores session, returns the user\n * const orders = await auth.api.get(\"/api/orders\"); // sends the bearer token\n * await auth.logout(); // clears, then runs onSessionEnd\n *\n * @param options - The auth configuration.\n * @returns The store hook, a wired API client, and login/logout/refresh helpers.\n */\nexport function createTempestAuth<TUser, TCredentials = { email: string; password: string }>(\n options: CreateTempestAuthOptions<TUser>,\n): TempestAuth<TUser, TCredentials> {\n const {\n baseURL,\n prefix,\n loginPath = \"/api/auth/login\",\n refreshPath = \"/api/auth/refresh\",\n mePath,\n storeName = \"tempest-auth\",\n storage = \"local\",\n withCredentials = false,\n csrf,\n fetcher,\n parseTokens = defaultParseTokens,\n parseUser,\n refreshBody = (rt) => (rt ? { refresh_token: rt } : undefined),\n retry,\n redirectTo,\n onSessionEnd,\n } = options;\n\n const useAuthStore = createAuthStore<TUser>({ name: storeName, storage });\n const refreshKey = `${storeName}-refresh`;\n\n function storageImpl(): Storage | null {\n if (typeof window === \"undefined\") return null;\n return storage === \"session\" ? window.sessionStorage : window.localStorage;\n }\n function readRefreshToken(): string | null {\n return storageImpl()?.getItem(refreshKey) ?? null;\n }\n function writeRefreshToken(token: string | null): void {\n const s = storageImpl();\n if (!s) return;\n if (token) s.setItem(refreshKey, token);\n else s.removeItem(refreshKey);\n }\n\n const state = (): AuthState<TUser> => useAuthStore.getState();\n const getToken = (): string | null => state().token;\n\n /**\n * Load the current user, without letting a `401` end the session.\n *\n * `skipAuthRetry` is what keeps this one client honest: the call is\n * authenticated, but a `401` here surfaces instead of triggering a refresh\n * and `onUnauthorized`. That was the behaviour of the throwaway client this\n * used to build per invocation, and it is the reason the preset needed\n * three clients before 0.66.0.\n */\n async function fetchUser(): Promise<TUser | null> {\n if (!mePath) return state().user;\n const user = await api.get<TUser>(mePath, { skipAuthRetry: true });\n state().setUser(user);\n return user;\n }\n\n let epoch = 0;\n let ending: Promise<void> = Promise.resolve();\n let hooksRunning = 0;\n\n /**\n * Start a new session epoch: supersede the refresh in flight and give the\n * new epoch a refresh queue of its own, so a `401` under the new session\n * never joins the old session's refresh.\n *\n * The client reads the epoch when each attempt leaves (`getSessionEpoch`),\n * which is what ties a `401` to the session that sent it — no set of\n * tokens to keep, and nothing that grows with the session's length.\n */\n function nextEpoch(): void {\n epoch += 1;\n queue = createRefreshQueue(rotate);\n }\n\n /**\n * Perform one refresh for the epoch that is current when it starts.\n *\n * Checked again after the round trip, on success and on failure alike: a\n * refresh that fails after `logout()` + `login(B)` must not reach\n * `onUnauthorized` either, or it would end B's session.\n *\n * @throws {SessionSupersededError} When the epoch changed during the call.\n */\n async function rotate(): Promise<void> {\n const started = epoch;\n let data: unknown;\n try {\n data = await api.post<unknown>(refreshPath, {\n body: refreshBody(readRefreshToken()),\n skipAuth: true,\n });\n } catch (error) {\n if (epoch !== started) throw new SessionSupersededError();\n throw error;\n }\n if (epoch !== started) throw new SessionSupersededError();\n const { token, refreshToken } = parseTokens(data);\n state().setToken(token);\n if (refreshToken) writeRefreshToken(refreshToken);\n }\n\n let queue = createRefreshQueue(rotate);\n\n /**\n * Whether a `401` must be left alone: no refresh, no ending the session.\n *\n * Two cases, both of the current epoch: an attempt that carried no token\n * while a session is now installed (it left before the sign-in, and\n * refreshing or ending the new session for it would punish the wrong\n * session); and an attempt whose token the session has since rotated past\n * (the rotation it would trigger already happened).\n *\n * @param context - The attempt's credential.\n * @returns Whether the attempt is stale within the current session.\n */\n function staleWithinSession(context: RequestAuthContext): boolean {\n const current = getToken();\n if (current === null) return false;\n return context.token === null || context.token !== current;\n }\n\n /**\n * The `refresh` the client calls on a `401`.\n *\n * - An attempt from an ended epoch is refused rather than replayed as\n * whoever is signed in now.\n * - A token this session already rotated past: the replay just picks up the\n * new one — no second refresh.\n * - No token, while a session is now installed: the `401` surfaces as is.\n * - Otherwise: join or start this epoch's refresh.\n *\n * @param context - The credential and epoch the rejected attempt left with.\n * @throws {SessionSupersededError} When the attempt belongs to an ended\n * session, or the session changed during the refresh.\n */\n async function refreshFor(context: RequestAuthContext): Promise<void> {\n if (context.epoch !== null && context.epoch !== epoch) throw new SessionSupersededError();\n if (staleWithinSession(context)) {\n if (context.token === null) throw new Error(\"anonymous 401 under a live session\");\n return;\n }\n return queue();\n }\n\n /**\n * The client's `onUnauthorized`: end the session only when the `401` is the\n * current session's own.\n *\n * The replay after a refresh is checked here too — it can come back `401`\n * after the user signed out and another account signed in, and ending the\n * session then would sign the new account out.\n *\n * @param context - The credential and epoch of the attempt that failed.\n * @throws {SessionSupersededError} When that attempt belongs to an ended\n * session.\n */\n async function onUnauthorized(_response: Response, context: RequestAuthContext): Promise<void> {\n if (context.epoch !== null && context.epoch !== epoch) throw new SessionSupersededError();\n if (staleWithinSession(context)) return;\n await endSession();\n }\n\n /**\n * Authenticate and install the new session.\n *\n * The previous session ends only once the new credentials are accepted: a\n * login that fails leaves it, and the refresh it may have in flight,\n * untouched. When one was live, `onSessionEnd` runs with `\"switch\"` before\n * the new tokens are installed — see the option for why that order. A\n * `logout()` still tearing down is awaited the same way, so the new\n * account's state never starts before the previous one's teardown ended.\n * Never call it from inside `onSessionEnd`: it would wait on that hook.\n *\n * @param credentials - Sent as the login body.\n * @returns The user of the new session, or null.\n * @throws {SessionSupersededError} When a `logout()` or another `login()`\n * moved the session while this one was in flight — during its request or\n * during the teardown; the new tokens are discarded.\n */\n async function login(credentials: TCredentials): Promise<TUser | null> {\n const started = epoch;\n const data = await api.post<unknown>(loginPath, {\n body: credentials,\n skipAuth: true,\n });\n if (epoch !== started) throw new SessionSupersededError();\n const { token, refreshToken } = parseTokens(data);\n const { done } = finish(\"switch\");\n const mine = epoch;\n await done.catch(() => undefined);\n if (epoch !== mine) throw new SessionSupersededError();\n state().setToken(token);\n writeRefreshToken(refreshToken ?? null);\n const embedded = parseUser?.(data);\n if (embedded != null) {\n state().setUser(embedded);\n return embedded;\n }\n return fetchUser();\n }\n\n /**\n * End the current session, synchronously up to the hook.\n *\n * The epoch moves first, so a refresh resolving between here and the hook\n * is already superseded. The hook only runs when there was a session to\n * end — a token, a stored refresh token or a user — which is what keeps a\n * burst of `401`s from running it once per request.\n *\n * With nothing left to end, the caller waits on the teardown already under\n * way (`ending`, which never rejects, so a later no-op does not inherit an\n * old error). Two callers skip that wait while a hook is running, because\n * they may be the hook itself and would wait on their own teardown forever:\n * `endSession` — a hook awaiting `auth.api` goes out tokenless, gets a\n * `401`, and lands here — and a `logout()` with no session left. `login()`\n * (`\"switch\"`) always waits: the new account's state must not start while\n * the previous one's teardown is still clearing it. A hook awaiting\n * `auth.login()` therefore never settles — misuse the option documents.\n *\n * The returned `done` is a fresh promise derived from the hook, not the one\n * `ending` holds: a `logout()` nobody awaits still surfaces the hook's error\n * as an unhandled rejection instead of losing it.\n *\n * @param reason - Whether the caller signed out, the client gave up, or a\n * login replaced the session.\n * @returns Whether a live session ended here, and a promise settling with\n * the hook.\n */\n function finish(reason: TempestSessionEnd<TUser>[\"reason\"]): {\n live: boolean;\n done: Promise<void>;\n } {\n nextEpoch();\n const { user, token } = state();\n const live = token !== null || user !== null || readRefreshToken() !== null;\n state().logout();\n writeRefreshToken(null);\n if (!live) {\n const wait = reason === \"switch\" || hooksRunning === 0;\n return { live, done: wait ? ending : Promise.resolve() };\n }\n if (!onSessionEnd) return { live, done: Promise.resolve() };\n const sessionApi = createApiClient({\n baseURL,\n prefix,\n withCredentials,\n csrf,\n fetcher,\n getToken: () => token,\n });\n hooksRunning += 1;\n const run = (async () => {\n try {\n await onSessionEnd({ reason, user, token, api: sessionApi });\n } finally {\n hooksRunning -= 1;\n }\n })();\n ending = run.then(\n () => undefined,\n () => undefined,\n );\n return { live, done: run.then(() => undefined) };\n }\n\n function logout(): Promise<void> {\n return finish(\"logout\").done;\n }\n\n /**\n * End the session after the client gave up on it and, when `redirectTo`\n * is set, leave the page once `onSessionEnd` settled.\n *\n * Separate from `logout` so an explicit sign-out stays a pure state change:\n * a caller that already navigates itself would otherwise get a second,\n * competing navigation. The hook is awaited before navigating because a\n * hard navigation aborts whatever request it still had in flight. Only the\n * call that ended a live session navigates; the rest of a burst of `401`s\n * waits on the same teardown, or returns at once inside a hook.\n */\n async function endSession(): Promise<void> {\n const { live, done } = finish(\"expired\");\n await done.catch(() => undefined);\n if (live && redirectTo && typeof window !== \"undefined\") {\n window.location.assign(redirectTo);\n }\n }\n\n const api = createApiClient({\n baseURL,\n prefix,\n withCredentials,\n csrf,\n fetcher,\n getToken,\n getSessionEpoch: () => epoch,\n refresh: refreshFor,\n retry,\n onUnauthorized,\n });\n\n const refresh = (): Promise<void> => queue();\n\n return { useAuthStore, api, login, logout, refresh, getToken };\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- function e(e,t={}){let{getToken:n}=t,r=null,i=null;return()=>r||(n&&i!==null&&n()===i?Promise.resolve():(r=(async()=>{try{await e(),n&&(i=n()??null)}finally{r=null}})(),r))}exports.createRefreshQueue=e;
1
+ function e(e,t={}){let{getToken:n}=t,r=null,i=null;return t=>{if(r)return r;if(n){let e=n()??null;if(t?t.token!==null&&e!==null&&t.token!==e:i!==null&&e===i)return Promise.resolve()}return r=(async()=>{try{await e(),n&&(i=n()??null)}finally{r=null}})(),r}}exports.createRefreshQueue=e;
2
2
  //# sourceMappingURL=refresh-queue.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"refresh-queue.cjs","names":[],"sources":["../../src/auth/refresh-queue.ts"],"sourcesContent":["export interface CreateRefreshQueueOptions {\n /**\n * Reads the credential the refresh installs — typically the access token.\n *\n * Supplying it makes the queue skip a refresh whose work another caller has\n * already done. Without it, only calls that literally overlap in time are\n * collapsed.\n */\n getToken?: () => string | null | undefined;\n}\n\n/**\n * Deduplicate concurrent refresh calls. When multiple 401 responses arrive\n * at once, all of them share the same in-flight `refresh()` promise instead\n * of triggering N parallel refreshes.\n *\n * **In-flight sharing alone does not collapse a burst.** A page that fires\n * several requests at once gets several 401s back, but they do not land inside\n * one window: the stragglers arrive after the first refresh already resolved,\n * find no promise to join, and each starts another one — rotating a token that\n * is already fresh. Measured against a mock backend, five concurrent expired\n * requests took two refreshes, not one.\n *\n * Pass `getToken` to close that gap. The queue remembers the token its last\n * refresh produced, and a call that finds that same token still in place returns\n * immediately, because the refresh it was about to perform has already happened.\n * The same five requests then take exactly one refresh, and so do twenty.\n *\n * @param refresh - Performs the refresh and installs the new credentials.\n * @param options - Optional token reader that enables already-refreshed\n * detection.\n * @returns A function that refreshes at most once per rotation.\n *\n * @example\n * const refresh = createRefreshQueue(() => AuthService.refresh(), {\n * getToken: () => useAuthStore.getState().token,\n * });\n *\n * // In every request that hits 401:\n * await refresh();\n * // ...retry the original request\n */\nexport function createRefreshQueue(\n refresh: () => Promise<void>,\n options: CreateRefreshQueueOptions = {},\n): () => Promise<void> {\n const { getToken } = options;\n let current: Promise<void> | null = null;\n let lastIssued: string | null = null;\n\n return () => {\n if (current) return current;\n\n if (getToken && lastIssued !== null && getToken() === lastIssued) {\n return Promise.resolve();\n }\n\n current = (async () => {\n try {\n await refresh();\n if (getToken) lastIssued = getToken() ?? null;\n } finally {\n current = null;\n }\n })();\n return current;\n };\n}\n"],"mappings":"AA0CA,SAAgB,EACZ,EACA,EAAqC,CAAC,EACnB,CACnB,GAAM,CAAE,YAAa,EACjB,EAAgC,KAChC,EAA4B,KAEhC,UACQ,IAEA,GAAY,IAAe,MAAQ,EAAS,IAAM,EAC3C,QAAQ,QAAQ,GAG3B,GAAW,SAAY,CACnB,GAAI,CACA,MAAM,EAAQ,EACV,IAAU,EAAa,EAAS,GAAK,KAC7C,QAAU,CACN,EAAU,IACd,CACJ,EAAA,CAAG,EACI,GAEf"}
1
+ {"version":3,"file":"refresh-queue.cjs","names":[],"sources":["../../src/auth/refresh-queue.ts"],"sourcesContent":["import type { RequestAuthContext } from \"../http\";\n\nexport interface CreateRefreshQueueOptions {\n /**\n * Reads the credential the refresh installs — typically the access token.\n *\n * Supplying it makes the queue skip a refresh whose work another caller has\n * already done. Without it, only calls that literally overlap in time are\n * collapsed.\n */\n getToken?: () => string | null | undefined;\n}\n\n/**\n * Deduplicate concurrent refresh calls. When multiple 401 responses arrive\n * at once, all of them share the same in-flight `refresh()` promise instead\n * of triggering N parallel refreshes.\n *\n * **In-flight sharing alone does not collapse a burst.** A page that fires\n * several requests at once gets several 401s back, but they do not land inside\n * one window: the stragglers arrive after the first refresh already resolved,\n * find no promise to join, and each starts another one — rotating a token that\n * is already fresh. Measured against a mock backend, five concurrent expired\n * requests took two refreshes, not one.\n *\n * Pass `getToken` to close that gap. Wired as `createApiClient`'s `refresh`, the\n * queue is told which token each rejected request carried: when that token is\n * no longer the current one, the rotation it was about to trigger already\n * happened, so the call returns at once and the replay picks up the new token.\n * The same five requests then take exactly one refresh, and so do twenty.\n *\n * Called by hand, with no context, the queue falls back to remembering the\n * token its last refresh produced and skipping while that token is in place.\n * That fallback cannot tell \"already refreshed\" from \"the refreshed token has\n * since expired\" — both leave the same string in the store — which is why the\n * client passes the context. Before 0.77.0 the client did not, and every\n * access token a refresh issued became unrefreshable once it expired: its `401`\n * skipped the refresh, the replay failed again, and `onUnauthorized` ended the\n * session.\n *\n * @param refresh - Performs the refresh and installs the new credentials.\n * @param options - Optional token reader that enables already-refreshed\n * detection.\n * @returns A function that refreshes at most once per rotation, accepting the\n * {@link RequestAuthContext} `createApiClient` passes on a `401`.\n *\n * @example\n * const refresh = createRefreshQueue(() => AuthService.refresh(), {\n * getToken: () => useAuthStore.getState().token,\n * });\n *\n * // In every request that hits 401:\n * await refresh();\n * // ...retry the original request\n */\nexport function createRefreshQueue(\n refresh: () => Promise<void>,\n options: CreateRefreshQueueOptions = {},\n): (context?: Pick<RequestAuthContext, \"token\">) => Promise<void> {\n const { getToken } = options;\n let current: Promise<void> | null = null;\n let lastIssued: string | null = null;\n\n return (context?: Pick<RequestAuthContext, \"token\">) => {\n if (current) return current;\n\n if (getToken) {\n const now = getToken() ?? null;\n const rotated = context\n ? context.token !== null && now !== null && context.token !== now\n : lastIssued !== null && now === lastIssued;\n if (rotated) return Promise.resolve();\n }\n\n current = (async () => {\n try {\n await refresh();\n if (getToken) lastIssued = getToken() ?? null;\n } finally {\n current = null;\n }\n })();\n return current;\n };\n}\n"],"mappings":"AAuDA,SAAgB,EACZ,EACA,EAAqC,CAAC,EACwB,CAC9D,GAAM,CAAE,YAAa,EACjB,EAAgC,KAChC,EAA4B,KAEhC,MAAQ,IAAgD,CACpD,GAAI,EAAS,OAAO,EAEpB,GAAI,EAAU,CACV,IAAM,EAAM,EAAS,GAAK,KAI1B,GAHgB,EACV,EAAQ,QAAU,MAAQ,IAAQ,MAAQ,EAAQ,QAAU,EAC5D,IAAe,MAAQ,IAAQ,EACxB,OAAO,QAAQ,QAAQ,CACxC,CAUA,MARA,IAAW,SAAY,CACnB,GAAI,CACA,MAAM,EAAQ,EACV,IAAU,EAAa,EAAS,GAAK,KAC7C,QAAU,CACN,EAAU,IACd,CACJ,EAAA,CAAG,EACI,CACX,CACJ"}
@@ -1,13 +1,20 @@
1
1
  //#region src/auth/refresh-queue.ts
2
2
  function e(e, t = {}) {
3
3
  let { getToken: n } = t, r = null, i = null;
4
- return () => r || (n && i !== null && n() === i ? Promise.resolve() : (r = (async () => {
5
- try {
6
- await e(), n && (i = n() ?? null);
7
- } finally {
8
- r = null;
4
+ return (t) => {
5
+ if (r) return r;
6
+ if (n) {
7
+ let e = n() ?? null;
8
+ if (t ? t.token !== null && e !== null && t.token !== e : i !== null && e === i) return Promise.resolve();
9
9
  }
10
- })(), r));
10
+ return r = (async () => {
11
+ try {
12
+ await e(), n && (i = n() ?? null);
13
+ } finally {
14
+ r = null;
15
+ }
16
+ })(), r;
17
+ };
11
18
  }
12
19
  //#endregion
13
20
  export { e as createRefreshQueue };
@@ -1 +1 @@
1
- {"version":3,"file":"refresh-queue.js","names":[],"sources":["../../src/auth/refresh-queue.ts"],"sourcesContent":["export interface CreateRefreshQueueOptions {\n /**\n * Reads the credential the refresh installs — typically the access token.\n *\n * Supplying it makes the queue skip a refresh whose work another caller has\n * already done. Without it, only calls that literally overlap in time are\n * collapsed.\n */\n getToken?: () => string | null | undefined;\n}\n\n/**\n * Deduplicate concurrent refresh calls. When multiple 401 responses arrive\n * at once, all of them share the same in-flight `refresh()` promise instead\n * of triggering N parallel refreshes.\n *\n * **In-flight sharing alone does not collapse a burst.** A page that fires\n * several requests at once gets several 401s back, but they do not land inside\n * one window: the stragglers arrive after the first refresh already resolved,\n * find no promise to join, and each starts another one — rotating a token that\n * is already fresh. Measured against a mock backend, five concurrent expired\n * requests took two refreshes, not one.\n *\n * Pass `getToken` to close that gap. The queue remembers the token its last\n * refresh produced, and a call that finds that same token still in place returns\n * immediately, because the refresh it was about to perform has already happened.\n * The same five requests then take exactly one refresh, and so do twenty.\n *\n * @param refresh - Performs the refresh and installs the new credentials.\n * @param options - Optional token reader that enables already-refreshed\n * detection.\n * @returns A function that refreshes at most once per rotation.\n *\n * @example\n * const refresh = createRefreshQueue(() => AuthService.refresh(), {\n * getToken: () => useAuthStore.getState().token,\n * });\n *\n * // In every request that hits 401:\n * await refresh();\n * // ...retry the original request\n */\nexport function createRefreshQueue(\n refresh: () => Promise<void>,\n options: CreateRefreshQueueOptions = {},\n): () => Promise<void> {\n const { getToken } = options;\n let current: Promise<void> | null = null;\n let lastIssued: string | null = null;\n\n return () => {\n if (current) return current;\n\n if (getToken && lastIssued !== null && getToken() === lastIssued) {\n return Promise.resolve();\n }\n\n current = (async () => {\n try {\n await refresh();\n if (getToken) lastIssued = getToken() ?? null;\n } finally {\n current = null;\n }\n })();\n return current;\n };\n}\n"],"mappings":";AA0CA,SAAgB,EACZ,GACA,IAAqC,CAAC,GACnB;CACnB,IAAM,EAAE,gBAAa,GACjB,IAAgC,MAChC,IAA4B;CAEhC,aACQ,MAEA,KAAY,MAAe,QAAQ,EAAS,MAAM,IAC3C,QAAQ,QAAQ,KAG3B,KAAW,YAAY;EACnB,IAAI;GAEA,AADA,MAAM,EAAQ,GACV,MAAU,IAAa,EAAS,KAAK;EAC7C,UAAU;GACN,IAAU;EACd;CACJ,EAAA,CAAG,GACI;AAEf"}
1
+ {"version":3,"file":"refresh-queue.js","names":[],"sources":["../../src/auth/refresh-queue.ts"],"sourcesContent":["import type { RequestAuthContext } from \"../http\";\n\nexport interface CreateRefreshQueueOptions {\n /**\n * Reads the credential the refresh installs — typically the access token.\n *\n * Supplying it makes the queue skip a refresh whose work another caller has\n * already done. Without it, only calls that literally overlap in time are\n * collapsed.\n */\n getToken?: () => string | null | undefined;\n}\n\n/**\n * Deduplicate concurrent refresh calls. When multiple 401 responses arrive\n * at once, all of them share the same in-flight `refresh()` promise instead\n * of triggering N parallel refreshes.\n *\n * **In-flight sharing alone does not collapse a burst.** A page that fires\n * several requests at once gets several 401s back, but they do not land inside\n * one window: the stragglers arrive after the first refresh already resolved,\n * find no promise to join, and each starts another one — rotating a token that\n * is already fresh. Measured against a mock backend, five concurrent expired\n * requests took two refreshes, not one.\n *\n * Pass `getToken` to close that gap. Wired as `createApiClient`'s `refresh`, the\n * queue is told which token each rejected request carried: when that token is\n * no longer the current one, the rotation it was about to trigger already\n * happened, so the call returns at once and the replay picks up the new token.\n * The same five requests then take exactly one refresh, and so do twenty.\n *\n * Called by hand, with no context, the queue falls back to remembering the\n * token its last refresh produced and skipping while that token is in place.\n * That fallback cannot tell \"already refreshed\" from \"the refreshed token has\n * since expired\" — both leave the same string in the store — which is why the\n * client passes the context. Before 0.77.0 the client did not, and every\n * access token a refresh issued became unrefreshable once it expired: its `401`\n * skipped the refresh, the replay failed again, and `onUnauthorized` ended the\n * session.\n *\n * @param refresh - Performs the refresh and installs the new credentials.\n * @param options - Optional token reader that enables already-refreshed\n * detection.\n * @returns A function that refreshes at most once per rotation, accepting the\n * {@link RequestAuthContext} `createApiClient` passes on a `401`.\n *\n * @example\n * const refresh = createRefreshQueue(() => AuthService.refresh(), {\n * getToken: () => useAuthStore.getState().token,\n * });\n *\n * // In every request that hits 401:\n * await refresh();\n * // ...retry the original request\n */\nexport function createRefreshQueue(\n refresh: () => Promise<void>,\n options: CreateRefreshQueueOptions = {},\n): (context?: Pick<RequestAuthContext, \"token\">) => Promise<void> {\n const { getToken } = options;\n let current: Promise<void> | null = null;\n let lastIssued: string | null = null;\n\n return (context?: Pick<RequestAuthContext, \"token\">) => {\n if (current) return current;\n\n if (getToken) {\n const now = getToken() ?? null;\n const rotated = context\n ? context.token !== null && now !== null && context.token !== now\n : lastIssued !== null && now === lastIssued;\n if (rotated) return Promise.resolve();\n }\n\n current = (async () => {\n try {\n await refresh();\n if (getToken) lastIssued = getToken() ?? null;\n } finally {\n current = null;\n }\n })();\n return current;\n };\n}\n"],"mappings":";AAuDA,SAAgB,EACZ,GACA,IAAqC,CAAC,GACwB;CAC9D,IAAM,EAAE,gBAAa,GACjB,IAAgC,MAChC,IAA4B;CAEhC,QAAQ,MAAgD;EACpD,IAAI,GAAS,OAAO;EAEpB,IAAI,GAAU;GACV,IAAM,IAAM,EAAS,KAAK;GAI1B,IAHgB,IACV,EAAQ,UAAU,QAAQ,MAAQ,QAAQ,EAAQ,UAAU,IAC5D,MAAe,QAAQ,MAAQ,GACxB,OAAO,QAAQ,QAAQ;EACxC;EAUA,OARA,KAAW,YAAY;GACnB,IAAI;IAEA,AADA,MAAM,EAAQ,GACV,MAAU,IAAa,EAAS,KAAK;GAC7C,UAAU;IACN,IAAU;GACd;EACJ,EAAA,CAAG,GACI;CACX;AACJ"}
package/dist/br/pix.cjs CHANGED
@@ -1,2 +1,2 @@
1
- const e=require("../forms/br-validators.cjs");var t=class extends Error{constructor(e){super(e),this.name=`PixError`}},n=4129,r=65535,i=65535,a=`00`,o=`01`,s=`26`,c=`52`,l=`53`,u=`54`,d=`58`,f=`59`,p=`60`,m=`61`,h=`62`,g=`63`,_=`00`,v=`01`,y=`02`,b=`25`,x=`05`,S=`br.gov.bcb.pix`,C=`01`,w=`11`,T=`12`,E=`0000`,D=`986`,O=`BR`,k=`***`,A=25,j=15,M=25,N=99,P=77;function F(e){let t=new TextEncoder().encode(e),a=r;for(let e of t){a^=e<<8;for(let e=0;e<8;e+=1)a=a&32768?(a<<1^n)&i:a<<1&i}return a.toString(16).toUpperCase().padStart(4,`0`)}function I(e){return e.replace(/\D/g,``)}function L(e,n){let r=e.normalize(`NFD`).replace(/\p{Diacritic}/gu,``);if(/[^\x20-\x7E]/.test(r))throw new t(`${n} has characters the BR Code cannot carry: ${JSON.stringify(e)}. Use unaccented ASCII.`);return r}function R(e,n){if(n.length>N)throw new t(`Tag ${e} is ${n.length} characters; the EMV length field holds at most ${N}.`);return`${e}${String(n.length).padStart(2,`0`)}${n}`}function z(t){let n=t.trim();if(n===``)return null;if(n.includes(`@`))return/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(n)&&n.length<=P?`email`:null;if(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(n))return`evp`;let r=I(n);return r.length===14?e.validateCNPJ(r)?`cnpj`:null:r.length===11&&e.validateCPF(r)?`cpf`:n.startsWith(`+`)?/^\+55\d{10,11}$/.test(`+${r}`)?`phone`:null:r.length===10||r.length===11||(r.length===12||r.length===13)&&r.startsWith(`55`)?`phone`:null}function B(e){let n=e.trim(),r=z(n);if(r===null)throw new t(`Not a Pix key: ${JSON.stringify(e)}. Expected a CPF, CNPJ, e-mail, phone (+5511987654321) or EVP (UUID).`);if(r===`email`||r===`evp`)return{type:r,value:n.toLowerCase()};if(r===`phone`){let e=I(n);return{type:r,value:`+55${e.length>11?e.slice(2):e}`}}return{type:r,value:I(n)}}function V(e){if(!Number.isFinite(e))throw new t(`Amount must be a finite number, got ${e}.`);if(e<=0)throw new t(`Amount must be positive, got ${e}. Omit \`amount\` for a QR whose value the payer types.`);let n=e.toFixed(2);if(n.length>13)throw new t(`Amount ${n} does not fit tag 54, which holds 13 characters.`);return n}function H(e,n,r){let i=L(e.trim(),r);if(i===``)throw new t(`${r} is required.`);if(i.length>n)throw new t(`${r} is ${i.length} characters; the BR Code allows ${n}. Shorten it — truncating here would silently change what the payer sees.`);return i}function U(e){let n=e?.trim()??``;if(n===``||n===k)return R(h,R(x,k));if(!RegExp(`^[A-Za-z0-9]{1,${M}}$`).test(n))throw new t(`txid must be 1 to ${M} letters or digits, got ${JSON.stringify(e)}.`);return R(h,R(x,n))}function W(e){let{value:n}=B(e.key),r=R(_,S)+R(v,n),i=e.description?.trim();if(i!==void 0&&i!==``&&(r+=R(y,L(i,`description`))),r.length>N)throw new t(`Tag 26 is ${r.length} characters, over the ${N} the length field allows. Shorten \`description\`.`);return R(s,r)}function G(e){let n=L(e.url.trim(),`url`).replace(/^https?:\/\//i,``);if(n===``)throw new t(`url is required for a dynamic BR Code.`);let r=R(_,S)+R(b,n);if(r.length>N)throw new t(`Tag 26 is ${r.length} characters, over the ${N} the length field allows. Shorten the payload URL.`);return R(s,r)}function K(e){let t=e.kind===`dynamic`,n=e.oneTime??t,r=R(a,C);r+=R(o,n?T:w),r+=t?G(e):W(e),r+=R(c,E),r+=R(l,D),!t&&e.amount!==void 0&&(r+=R(u,V(e.amount))),r+=R(d,O),r+=R(f,H(e.merchantName,A,`merchantName`)),r+=R(p,H(e.merchantCity,j,`merchantCity`));let i=e.postalCode===void 0?``:I(e.postalCode);i!==``&&(r+=R(m,i)),r+=U(t?k:e.txid);let s=`${r}${g}04`;return`${s}${F(s)}`}function q(e,n){let r=[],i=0;for(;i<e.length;){let a=e.slice(i,i+2),o=e.slice(i+2,i+4);if(!/^\d{2}$/.test(a)||!/^\d{2}$/.test(o))throw new t(`Malformed TLV in ${n} at offset ${i}: expected a 2-digit tag and length.`);let s=Number(o),c=i+4;if(c+s>e.length)throw new t(`Tag ${a} in ${n} declares ${s} characters but only ${e.length-c} remain.`);r.push({id:a,value:e.slice(c,c+s)}),i=c+s}return r}function J(e,t){return e.find(e=>e.id===t)?.value}function Y(e,n={}){let{requireCrc:r=!0}=n,i=e.trim();if(i.length<8)throw new t(`Payload is too short to be a BR Code.`);let a=i.length-8;if(i.slice(a,a+4)!==`${g}04`)throw new t("Payload does not end in a `6304` CRC tag.");let C=i.slice(-4).toUpperCase(),w=F(i.slice(0,-4)),E=C===w;if(!E&&r)throw new t(`CRC mismatch: payload says ${C}, recomputed ${w}.`);let D=q(i.slice(0,a),`payload`),O=J(D,s);if(O===void 0)throw new t(`Payload has no tag 26 (merchant account information).`);let k=q(O,`tag 26`),A=J(k,_);if(A?.toLowerCase()!==S)throw new t(`Tag 26 is not a Pix account: expected GUI ${S}, got ${JSON.stringify(A)}.`);let j=J(k,b),M=J(k,v),N=J(D,u),P=J(q(J(D,h)??``,`tag 62`),x);return{kind:j!==void 0&&M===void 0?`dynamic`:`static`,...M===void 0?{}:{key:M,keyType:z(M)??void 0},...j===void 0?{}:{url:j},merchantName:J(D,f)??``,merchantCity:J(D,p)??``,...N===void 0?{}:{amount:Number(N)},currency:J(D,l)??``,countryCode:J(D,d)??``,merchantCategoryCode:J(D,c)??``,...P===void 0?{}:{txid:P},...J(k,y)===void 0?{}:{description:J(k,y)},...J(D,m)===void 0?{}:{postalCode:J(D,m)},oneTime:J(D,o)===T,crc:C,crcValid:E,fields:D}}exports.PixError=t,exports.normalizePixKey=B,exports.parsePixPayload=Y,exports.pixCrc16=F,exports.pixKeyType=z,exports.pixPayload=K;
1
+ const e=require("../forms/br-validators.cjs");var t=class extends Error{constructor(e){super(e),this.name=`PixError`}},n=4129,r=65535,i=65535,a=`00`,o=`01`,s=`26`,c=`52`,l=`53`,u=`54`,d=`58`,f=`59`,p=`60`,m=`61`,h=`62`,g=`63`,_=`00`,v=`01`,y=`02`,b=`25`,x=`05`,S=`br.gov.bcb.pix`,C=`01`,w=`11`,T=`12`,E=`0000`,D=`986`,O=`BR`,k=`***`,A=25,j=15,M=25,N=99,P=77,F=/^\d+(?:\.\d{0,2})?$/,I=13;function L(e){let t=new TextEncoder().encode(e),a=r;for(let e of t){a^=e<<8;for(let e=0;e<8;e+=1)a=a&32768?(a<<1^n)&i:a<<1&i}return a.toString(16).toUpperCase().padStart(4,`0`)}function R(e){return e.replace(/\D/g,``)}function z(e,n){let r=e.normalize(`NFD`).replace(/\p{Diacritic}/gu,``);if(/[^\x20-\x7E]/.test(r))throw new t(`${n} has characters the BR Code cannot carry: ${JSON.stringify(e)}. Use unaccented ASCII.`);return r}var B=/^[\x20-\x7E]*$/;function V(e,n){if(!B.test(n))throw new t(`Tag ${e} has characters outside the EMV common character set (printable ASCII): ${JSON.stringify(n)}.`);if(n.length>N)throw new t(`Tag ${e} is ${n.length} characters; the EMV length field holds at most ${N}.`);return`${e}${String(n.length).padStart(2,`0`)}${n}`}function H(t){let n=t.trim();if(n===``)return null;if(n.includes(`@`))return/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(n)&&n.length<=P?`email`:null;if(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(n))return`evp`;let r=R(n);return r.length===14?e.validateCNPJ(r)?`cnpj`:null:r.length===11&&e.validateCPF(r)?`cpf`:n.startsWith(`+`)?/^\+55\d{10,11}$/.test(`+${r}`)?`phone`:null:r.length===10||r.length===11||(r.length===12||r.length===13)&&r.startsWith(`55`)?`phone`:null}function U(e){let n=e.trim(),r=H(n);if(r===null)throw new t(`Not a Pix key: ${JSON.stringify(e)}. Expected a CPF, CNPJ, e-mail, phone (+5511987654321) or EVP (UUID).`);if(r===`email`||r===`evp`)return{type:r,value:n.toLowerCase()};if(r===`phone`){let e=R(n);return{type:r,value:`+55${e.length>11?e.slice(2):e}`}}return{type:r,value:R(n)}}function W(e){if(!Number.isFinite(e))throw new t(`Amount must be a finite number, got ${e}.`);if(e<=0)throw new t(`Amount must be positive, got ${e}. Omit \`amount\` for a QR whose value the payer types.`);let n=e.toFixed(2);if(Number(n)<=0)throw new t(`Amount ${e} rounds to ${n} at two decimals. Omit \`amount\` for a QR whose value the payer types.`);if(n.length>13)throw new t(`Amount ${n} does not fit tag 54, which holds 13 characters.`);return n}function G(e,n,r){let i=z(e.trim(),r);if(i===``)throw new t(`${r} is required.`);if(i.length>n)throw new t(`${r} is ${i.length} characters; the BR Code allows ${n}. Shorten it — truncating here would silently change what the payer sees.`);return i}function K(e){let n=e?.trim()??``;if(n===``||n===k)return V(h,V(x,k));if(!RegExp(`^[A-Za-z0-9]{1,${M}}$`).test(n))throw new t(`txid must be 1 to ${M} letters or digits, got ${JSON.stringify(e)}.`);return V(h,V(x,n))}function q(e){let{value:n}=U(e.key),r=V(_,S)+V(v,n),i=e.description?.trim();if(i!==void 0&&i!==``&&(r+=V(y,z(i,`description`))),r.length>N)throw new t(`Tag 26 is ${r.length} characters, over the ${N} the length field allows. Shorten \`description\`.`);return V(s,r)}function J(e){let n=z(e.url.trim(),`url`).replace(/^https?:\/\//i,``);if(n===``)throw new t(`url is required for a dynamic BR Code.`);let r=V(_,S)+V(b,n);if(r.length>N)throw new t(`Tag 26 is ${r.length} characters, over the ${N} the length field allows. Shorten the payload URL.`);return V(s,r)}function Y(e){let t=e.kind===`dynamic`,n=e.oneTime??t,r=V(a,C);r+=V(o,n?T:w),r+=t?J(e):q(e),r+=V(c,E),r+=V(l,D),!t&&e.amount!==void 0&&(r+=V(u,W(e.amount))),r+=V(d,O),r+=V(f,G(e.merchantName,A,`merchantName`)),r+=V(p,G(e.merchantCity,j,`merchantCity`));let i=e.postalCode===void 0?``:R(e.postalCode);i!==``&&(r+=V(m,i)),r+=K(t?k:e.txid);let s=`${r}${g}04`;return`${s}${L(s)}`}function X(e,n){let r=Array.from(e),i=[],a=0;for(;a<r.length;){let e=r.slice(a,a+2).join(``),o=r.slice(a+2,a+4).join(``);if(!/^\d{2}$/.test(e)||!/^\d{2}$/.test(o))throw new t(`Malformed TLV in ${n} at offset ${a}: expected a 2-digit tag and length.`);let s=Number(o),c=a+4;if(c+s>r.length)throw new t(`Tag ${e} in ${n} declares ${s} characters but only ${r.length-c} remain.`);if(i.some(t=>t.id===e))throw new t(`Tag ${e} in ${n} appears more than once; a BR Code carries each tag once per level.`);i.push({id:e,value:r.slice(c,c+s).join(``)}),a=c+s}return i}function Z(e,t){return e.find(e=>e.id===t)?.value}function Q(e){if(e.length>I||!F.test(e))throw new t(`Tag 54 (amount) must be an amount like "25.50", with "." as the decimal mark, at most two decimals and ${I} characters; got ${JSON.stringify(e)}.`);return Number(e)}function $(e,n={}){let{requireCrc:r=!0}=n,i=e.trim();if(i.length<8)throw new t(`Payload is too short to be a BR Code.`);let a=i.length-8;if(i.slice(a,a+4)!==`${g}04`)throw new t("Payload does not end in a `6304` CRC tag.");let C=i.slice(-4).toUpperCase(),w=L(i.slice(0,-4)),E=C===w;if(!E&&r)throw new t(`CRC mismatch: payload says ${C}, recomputed ${w}.`);let D=X(i.slice(0,a),`payload`),O=Z(D,s);if(O===void 0)throw new t(`Payload has no tag 26 (merchant account information).`);let k=X(O,`tag 26`),A=Z(k,_);if(A?.toLowerCase()!==S)throw new t(`Tag 26 is not a Pix account: expected GUI ${S}, got ${JSON.stringify(A)}.`);let j=Z(k,b),M=Z(k,v);if(j!==void 0&&M!==void 0)throw new t(`Tag 26 carries both a key (01) and a URL (25); a BR Code is either static or dynamic, not both.`);let N=Z(D,u),P=Z(X(Z(D,h)??``,`tag 62`),x);return{kind:j===void 0?`static`:`dynamic`,...M===void 0?{}:{key:M,keyType:H(M)??void 0},...j===void 0?{}:{url:j},merchantName:Z(D,f)??``,merchantCity:Z(D,p)??``,...N===void 0?{}:{amount:Q(N)},currency:Z(D,l)??``,countryCode:Z(D,d)??``,merchantCategoryCode:Z(D,c)??``,...P===void 0?{}:{txid:P},...Z(k,y)===void 0?{}:{description:Z(k,y)},...Z(D,m)===void 0?{}:{postalCode:Z(D,m)},oneTime:Z(D,o)===T,crc:C,crcValid:E,fields:D}}exports.PixError=t,exports.normalizePixKey=U,exports.parsePixPayload=$,exports.pixCrc16=L,exports.pixKeyType=H,exports.pixPayload=Y;
2
2
  //# sourceMappingURL=pix.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"pix.cjs","names":[],"sources":["../../src/br/pix.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — EMV®QRCPS-MPM for Pix: the TLV writer, the\n * CRC16-CCITT, the field catalogue with its nesting, key-type detection and the\n * parser that has to accept what other banks emit. Reader and writer live together\n * because each one is the other's test — a payload this file builds is a payload it\n * must parse back.\n */\nimport { validateCNPJ, validateCPF } from \"@/forms/br-validators\";\n\n/**\n * A Pix payload could not be built or read.\n *\n * Its own class so a caller can tell \"the operator typed a bad key\" apart from a\n * bug, and so an `ErrorBoundary` can render a form error instead of a crash.\n */\nexport class PixError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"PixError\";\n }\n}\n\n/** The five key formats DICT accepts. */\nexport type PixKeyType = \"cpf\" | \"cnpj\" | \"email\" | \"phone\" | \"evp\";\n\n/** A key after normalisation, ready to go into the payload. */\nexport interface NormalizedPixKey {\n type: PixKeyType;\n /** The exact string written into the BR Code. */\n value: string;\n}\n\n/** One TLV as it appeared in a payload, unknown tags included. */\nexport interface PixField {\n /** Two-character tag, e.g. `\"59\"`. */\n id: string;\n /** Raw value, still encoded when the tag is itself a template. */\n value: string;\n}\n\n/** A BR Code that carries the key — the one you print on a poster. */\nexport interface PixStaticInput {\n kind?: \"static\";\n /** CPF, CNPJ, e-mail, phone or EVP. Validated and normalised. */\n key: string;\n /** Payee name. Truncated by the spec at 25 characters — longer throws. */\n merchantName: string;\n /** Payee city. Truncated by the spec at 15 characters — longer throws. */\n merchantCity: string;\n /** Amount in BRL. Omit for a payer-chooses-the-value QR. */\n amount?: number;\n /** Reference the PSP echoes back, `[A-Za-z0-9]{1,25}`. Defaults to `\"***\"`. */\n txid?: string;\n /** Free text shown by some wallets. Goes into tag 26, sub-tag 02. */\n description?: string;\n /** CEP, digits only. Optional tag 61. */\n postalCode?: string;\n /** Single-use QR: sets tag 01 to `\"12\"` instead of `\"11\"`. */\n oneTime?: boolean;\n}\n\n/** A BR Code that carries a URL the wallet fetches to learn the amount. */\nexport interface PixDynamicInput {\n kind: \"dynamic\";\n /**\n * `payloadLocation` — the https URL the wallet GETs, **without** the scheme,\n * exactly as BACEN specifies (`pix.example.com/qr/v2/abc`).\n */\n url: string;\n merchantName: string;\n merchantCity: string;\n postalCode?: string;\n /** Single-use QR. Defaults to `true`, which is what a dynamic QR normally is. */\n oneTime?: boolean;\n}\n\n/** Everything `pixPayload` accepts. */\nexport type PixInput = PixStaticInput | PixDynamicInput;\n\n/** A payload taken apart again. */\nexport interface PixData {\n /** `\"dynamic\"` when tag 26 carried a URL instead of a key. */\n kind: \"static\" | \"dynamic\";\n /** Present on a static payload. */\n key?: string;\n keyType?: PixKeyType;\n /** Present on a dynamic payload. */\n url?: string;\n merchantName: string;\n merchantCity: string;\n /** Reais. `undefined` when the payer chooses the amount. */\n amount?: number;\n /** ISO 4217 numeric. `\"986\"` for BRL. */\n currency: string;\n countryCode: string;\n merchantCategoryCode: string;\n /** `\"***\"` on a reusable static QR that identifies no single transaction. */\n txid?: string;\n description?: string;\n postalCode?: string;\n /** Tag 01 read as `\"12\"`. */\n oneTime: boolean;\n /** The four hex characters that closed the payload. */\n crc: string;\n /** Whether those four characters match a recomputed CRC. */\n crcValid: boolean;\n /** Every top-level TLV, in payload order, unknown tags included. */\n fields: PixField[];\n}\n\n/** Options for {@link parsePixPayload}. */\nexport interface ParsePixOptions {\n /**\n * Throw when the checksum does not match. Default `true`.\n *\n * Turn it off only to inspect a payload you already know is broken: a BR Code\n * whose CRC fails has been corrupted in transit, and the account it now points\n * at is not the account the payee published.\n */\n requireCrc?: boolean;\n}\n\n/**\n * Generator polynomial of CRC-16/CCITT-FALSE, `x^16 + x^12 + x^5 + 1`.\n *\n * Taken from the CRC catalogue entry `CRC-16/IBM-3740` (alias CCITT-FALSE):\n * `width=16 poly=0x1021 init=0xffff refin=false refout=false xorout=0x0000\n * check=0x29b1`. The BACEN \"Manual de Padrões para Iniciação do Pix\" names this\n * exact variant for tag 63.\n */\nconst CRC16_POLYNOMIAL = 0x1021;\n\n/** Register preset of CRC-16/CCITT-FALSE. Not zero — that is a different variant. */\nconst CRC16_INITIAL = 0xffff;\n\n/** Keeps the shift register 16 bits wide. */\nconst CRC16_MASK = 0xffff;\n\nconst TAG_PAYLOAD_FORMAT = \"00\";\nconst TAG_POINT_OF_INITIATION = \"01\";\nconst TAG_MERCHANT_ACCOUNT_INFO = \"26\";\nconst TAG_MERCHANT_CATEGORY_CODE = \"52\";\nconst TAG_TRANSACTION_CURRENCY = \"53\";\nconst TAG_TRANSACTION_AMOUNT = \"54\";\nconst TAG_COUNTRY_CODE = \"58\";\nconst TAG_MERCHANT_NAME = \"59\";\nconst TAG_MERCHANT_CITY = \"60\";\nconst TAG_POSTAL_CODE = \"61\";\nconst TAG_ADDITIONAL_DATA = \"62\";\nconst TAG_CRC = \"63\";\n\nconst MAI_TAG_GUI = \"00\";\nconst MAI_TAG_KEY = \"01\";\nconst MAI_TAG_DESCRIPTION = \"02\";\nconst MAI_TAG_URL = \"25\";\nconst ADDITIONAL_TAG_TXID = \"05\";\n\n/** Globally Unique Identifier that marks tag 26 as a Pix account. */\nconst PIX_GUI = \"br.gov.bcb.pix\";\n\nconst PAYLOAD_FORMAT_VERSION = \"01\";\nconst POINT_OF_INITIATION_REUSABLE = \"11\";\nconst POINT_OF_INITIATION_SINGLE_USE = \"12\";\nconst DEFAULT_MERCHANT_CATEGORY_CODE = \"0000\";\nconst CURRENCY_BRL = \"986\";\nconst COUNTRY_BR = \"BR\";\nconst TXID_UNSPECIFIED = \"***\";\n\nconst MAX_MERCHANT_NAME = 25;\nconst MAX_MERCHANT_CITY = 15;\nconst MAX_TXID = 25;\nconst MAX_TLV_VALUE = 99;\nconst MAX_EMAIL_KEY = 77;\n\n/**\n * CRC-16/CCITT-FALSE of a string, as four upper-case hex characters.\n *\n * Bitwise rather than table-driven: 200-odd characters at 8 shifts each is\n * nothing, and a 256-entry table is 2 KB of payload every consumer of the `/br`\n * entry would carry.\n *\n * The input is taken as UTF-8 bytes. A Pix payload is ASCII by construction —\n * {@link pixPayload} rejects anything else — but the function is exported and a\n * caller may hand it arbitrary text, and hashing UTF-16 code units would then\n * disagree with every other implementation.\n *\n * @param input - Bytes to run through the register.\n * @returns Four upper-case hex characters, zero-padded.\n *\n * @example\n * pixCrc16(\"123456789\"); // \"29B1\" — the catalogue check value\n */\nexport function pixCrc16(input: string): string {\n const bytes = new TextEncoder().encode(input);\n let crc = CRC16_INITIAL;\n for (const byte of bytes) {\n crc ^= byte << 8;\n for (let bit = 0; bit < 8; bit += 1) {\n crc =\n (crc & 0x8000) !== 0\n ? ((crc << 1) ^ CRC16_POLYNOMIAL) & CRC16_MASK\n : (crc << 1) & CRC16_MASK;\n }\n }\n return crc.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/** Digits of a possibly masked value. */\nfunction digits(value: string): string {\n return value.replace(/\\D/g, \"\");\n}\n\n/**\n * Drop diacritics and reject whatever is left outside printable ASCII.\n *\n * The BR Code character set has no room for accents, and a wallet that meets one\n * either fails to parse the QR or shows mojibake. Stripping them is lossy but\n * legible (\"São Paulo\" → \"Sao Paulo\"), which beats both alternatives; anything\n * that is not a diacritic — an emoji, a CJK character — is a mistake the caller\n * has to see, so it throws.\n */\nfunction toPayloadText(value: string, field: string): string {\n const stripped = value.normalize(\"NFD\").replace(/\\p{Diacritic}/gu, \"\");\n if (/[^\\x20-\\x7E]/.test(stripped)) {\n throw new PixError(\n `${field} has characters the BR Code cannot carry: ${JSON.stringify(value)}. ` +\n \"Use unaccented ASCII.\",\n );\n }\n return stripped;\n}\n\n/** One `ID + 2-digit length + value` triple. */\nfunction tlv(id: string, value: string): string {\n if (value.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag ${id} is ${value.length} characters; the EMV length field holds at most ${MAX_TLV_VALUE}.`,\n );\n }\n return `${id}${String(value.length).padStart(2, \"0\")}${value}`;\n}\n\n/**\n * Classify a Pix key, or `null` when it matches no accepted format.\n *\n * !!! warning \"CPF and a national phone number are both eleven digits\"\n * `\"11987654321\"` is a valid mobile number and could be a CPF. The check\n * digits break the tie: an 11-digit string is a CPF when its DV validates and\n * a phone otherwise. Pass phone keys as `+5511987654321` to remove the guess\n * entirely.\n *\n * @param key - Raw key, masked or not.\n * @returns The key type, or `null`.\n */\nexport function pixKeyType(key: string): PixKeyType | null {\n const trimmed = key.trim();\n if (trimmed === \"\") return null;\n\n if (trimmed.includes(\"@\")) {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/.test(trimmed) && trimmed.length <= MAX_EMAIL_KEY\n ? \"email\"\n : null;\n }\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)) {\n return \"evp\";\n }\n\n const numbers = digits(trimmed);\n if (numbers.length === 14) return validateCNPJ(numbers) ? \"cnpj\" : null;\n if (numbers.length === 11 && validateCPF(numbers)) return \"cpf\";\n if (trimmed.startsWith(\"+\")) {\n return /^\\+55\\d{10,11}$/.test(`+${numbers}`) ? \"phone\" : null;\n }\n if (numbers.length === 10 || numbers.length === 11) return \"phone\";\n if (numbers.length === 12 || numbers.length === 13) {\n return numbers.startsWith(\"55\") ? \"phone\" : null;\n }\n return null;\n}\n\n/**\n * Validate a Pix key and return the exact string to write into the payload.\n *\n * Normalisation per type: CPF and CNPJ lose their mask, a phone becomes E.164\n * with the `+55` country code, an EVP is lower-cased, and an e-mail is\n * lower-cased because DICT stores it that way — a key that differs only in case\n * would otherwise fail to resolve.\n *\n * All validation lives in {@link pixKeyType}; past that gate the normalisation is\n * total, which is why the phone branch strips a country code purely on length\n * rather than re-checking the shape.\n *\n * @param key - Raw key, masked or not.\n * @returns The type and the normalised value.\n * @throws {PixError} When the key matches no accepted format, or when a document\n * key fails its check digits.\n *\n * @example\n * normalizePixKey(\"123.456.789-09\"); // { type: \"cpf\", value: \"12345678909\" }\n * normalizePixKey(\"(11) 98765-4321\"); // { type: \"phone\", value: \"+5511987654321\" }\n */\nexport function normalizePixKey(key: string): NormalizedPixKey {\n const trimmed = key.trim();\n const type = pixKeyType(trimmed);\n if (type === null) {\n throw new PixError(\n `Not a Pix key: ${JSON.stringify(key)}. Expected a CPF, CNPJ, e-mail, ` +\n \"phone (+5511987654321) or EVP (UUID).\",\n );\n }\n\n if (type === \"email\") return { type, value: trimmed.toLowerCase() };\n if (type === \"evp\") return { type, value: trimmed.toLowerCase() };\n if (type === \"phone\") {\n const numbers = digits(trimmed);\n return { type, value: `+55${numbers.length > 11 ? numbers.slice(2) : numbers}` };\n }\n return { type, value: digits(trimmed) };\n}\n\n/** Format an amount the way tag 54 wants it: dot separator, two decimals. */\nfunction toAmountField(amount: number): string {\n if (!Number.isFinite(amount)) {\n throw new PixError(`Amount must be a finite number, got ${amount}.`);\n }\n if (amount <= 0) {\n throw new PixError(\n `Amount must be positive, got ${amount}. Omit \\`amount\\` for a QR whose value the payer types.`,\n );\n }\n const field = amount.toFixed(2);\n if (field.length > 13) {\n throw new PixError(`Amount ${field} does not fit tag 54, which holds 13 characters.`);\n }\n return field;\n}\n\n/** Bound the two free-text identity fields the spec caps hard. */\nfunction toBoundedText(value: string, max: number, field: string): string {\n const text = toPayloadText(value.trim(), field);\n if (text === \"\") throw new PixError(`${field} is required.`);\n if (text.length > max) {\n throw new PixError(\n `${field} is ${text.length} characters; the BR Code allows ${max}. Shorten it — ` +\n \"truncating here would silently change what the payer sees.\",\n );\n }\n return text;\n}\n\n/** Tag 62, which exists only to carry the txid. */\nfunction additionalDataField(txid: string | undefined): string {\n const value = txid?.trim() ?? \"\";\n if (value === \"\" || value === TXID_UNSPECIFIED) {\n return tlv(TAG_ADDITIONAL_DATA, tlv(ADDITIONAL_TAG_TXID, TXID_UNSPECIFIED));\n }\n if (!new RegExp(`^[A-Za-z0-9]{1,${MAX_TXID}}$`).test(value)) {\n throw new PixError(\n `txid must be 1 to ${MAX_TXID} letters or digits, got ${JSON.stringify(txid)}.`,\n );\n }\n return tlv(TAG_ADDITIONAL_DATA, tlv(ADDITIONAL_TAG_TXID, value));\n}\n\n/** Tag 26 for a static payload: GUI, key and the optional description. */\nfunction staticMerchantAccount(input: PixStaticInput): string {\n const { value } = normalizePixKey(input.key);\n let inner = tlv(MAI_TAG_GUI, PIX_GUI) + tlv(MAI_TAG_KEY, value);\n const description = input.description?.trim();\n if (description !== undefined && description !== \"\") {\n inner += tlv(MAI_TAG_DESCRIPTION, toPayloadText(description, \"description\"));\n }\n if (inner.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag 26 is ${inner.length} characters, over the ${MAX_TLV_VALUE} the length field allows. ` +\n \"Shorten `description`.\",\n );\n }\n return tlv(TAG_MERCHANT_ACCOUNT_INFO, inner);\n}\n\n/** Tag 26 for a dynamic payload: GUI plus the URL, and no key. */\nfunction dynamicMerchantAccount(input: PixDynamicInput): string {\n const url = toPayloadText(input.url.trim(), \"url\").replace(/^https?:\\/\\//i, \"\");\n if (url === \"\") throw new PixError(\"url is required for a dynamic BR Code.\");\n const inner = tlv(MAI_TAG_GUI, PIX_GUI) + tlv(MAI_TAG_URL, url);\n if (inner.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag 26 is ${inner.length} characters, over the ${MAX_TLV_VALUE} the length field allows. ` +\n \"Shorten the payload URL.\",\n );\n }\n return tlv(TAG_MERCHANT_ACCOUNT_INFO, inner);\n}\n\n/**\n * Build a Pix \"Copia e Cola\" payload — the string behind a Pix QR code.\n *\n * The format is EMVCo MPM: a flat list of `ID + 2-digit length + value` triples,\n * closed by tag 63 holding a CRC-16/CCITT-FALSE over **everything before it,\n * including the literal `6304` header of tag 63 itself**. That last detail is the\n * one implementations get wrong; see {@link pixCrc16}.\n *\n * Two shapes come out of here:\n *\n * - **static** — tag 26 carries the key, so the QR is self-contained and can be\n * printed. Amount optional; a txid of `\"***\"` means \"identifies no single\n * transaction\", which is what a reusable poster QR wants.\n * - **dynamic** — tag 26 carries a URL instead, and the wallet fetches the amount\n * and payee from the PSP. Use it when the value is per-order. Defaults to\n * single-use (tag 01 = `12`).\n *\n * The distinction matters and is not cosmetic: a static QR settles against\n * whatever the payer typed, a dynamic one against what the PSP served, so a\n * charge that must reconcile to a cent needs the dynamic form.\n *\n * @param input - Static or dynamic payload description.\n * @returns The full payload, CRC included, ready to render as a QR or to copy.\n * @throws {PixError} On an unrecognised key, a field over its length cap, a\n * non-positive amount, or text that is not representable in the BR Code.\n *\n * @example\n * pixPayload({\n * key: \"12345678909\",\n * merchantName: \"Loja Tempest\",\n * merchantCity: \"São Paulo\",\n * amount: 25.5,\n * txid: \"PEDIDO123\",\n * });\n */\nexport function pixPayload(input: PixInput): string {\n const dynamic = input.kind === \"dynamic\";\n const oneTime = input.oneTime ?? dynamic;\n\n let payload = tlv(TAG_PAYLOAD_FORMAT, PAYLOAD_FORMAT_VERSION);\n payload += tlv(\n TAG_POINT_OF_INITIATION,\n oneTime ? POINT_OF_INITIATION_SINGLE_USE : POINT_OF_INITIATION_REUSABLE,\n );\n payload += dynamic ? dynamicMerchantAccount(input) : staticMerchantAccount(input);\n payload += tlv(TAG_MERCHANT_CATEGORY_CODE, DEFAULT_MERCHANT_CATEGORY_CODE);\n payload += tlv(TAG_TRANSACTION_CURRENCY, CURRENCY_BRL);\n if (!dynamic && input.amount !== undefined) {\n payload += tlv(TAG_TRANSACTION_AMOUNT, toAmountField(input.amount));\n }\n payload += tlv(TAG_COUNTRY_CODE, COUNTRY_BR);\n payload += tlv(\n TAG_MERCHANT_NAME,\n toBoundedText(input.merchantName, MAX_MERCHANT_NAME, \"merchantName\"),\n );\n payload += tlv(\n TAG_MERCHANT_CITY,\n toBoundedText(input.merchantCity, MAX_MERCHANT_CITY, \"merchantCity\"),\n );\n const postalCode = input.postalCode === undefined ? \"\" : digits(input.postalCode);\n if (postalCode !== \"\") payload += tlv(TAG_POSTAL_CODE, postalCode);\n payload += additionalDataField(dynamic ? TXID_UNSPECIFIED : input.txid);\n\n const withCrcHeader = `${payload}${TAG_CRC}04`;\n return `${withCrcHeader}${pixCrc16(withCrcHeader)}`;\n}\n\n/**\n * Split a flat run of TLVs. Stops cleanly at the first malformed triple.\n *\n * @throws {PixError} When a length prefix is not two digits or runs past the end.\n */\nfunction parseTlv(input: string, where: string): PixField[] {\n const fields: PixField[] = [];\n let cursor = 0;\n while (cursor < input.length) {\n const id = input.slice(cursor, cursor + 2);\n const rawLength = input.slice(cursor + 2, cursor + 4);\n if (!/^\\d{2}$/.test(id) || !/^\\d{2}$/.test(rawLength)) {\n throw new PixError(\n `Malformed TLV in ${where} at offset ${cursor}: expected a 2-digit tag and length.`,\n );\n }\n const length = Number(rawLength);\n const start = cursor + 4;\n if (start + length > input.length) {\n throw new PixError(\n `Tag ${id} in ${where} declares ${length} characters but only ${input.length - start} remain.`,\n );\n }\n fields.push({ id, value: input.slice(start, start + length) });\n cursor = start + length;\n }\n return fields;\n}\n\n/** First value for a tag, or `undefined`. */\nfunction pick(fields: readonly PixField[], id: string): string | undefined {\n return fields.find((field) => field.id === id)?.value;\n}\n\n/**\n * Read a Pix \"Copia e Cola\" payload back into its parts.\n *\n * Tolerant by design: tags the SDK does not know about are kept verbatim in\n * {@link PixData.fields} instead of raising, because PSPs do add their own\n * templates and a reader that rejects them is useless in production. What is\n * *not* tolerated is a broken frame — a length prefix that runs off the end, a\n * missing tag 63 — or a checksum mismatch, which means the string was corrupted\n * and no longer names the account the payee published.\n *\n * @param payload - The copia-e-cola string. Surrounding whitespace is ignored.\n * @param options - See {@link ParsePixOptions}.\n * @returns The decoded payload.\n * @throws {PixError} On a malformed frame, a missing CRC tag, a tag 26 that is\n * not a Pix account, or — unless `requireCrc` is `false` — a CRC mismatch.\n *\n * @example\n * const data = parsePixPayload(copied);\n * console.log(data.key, data.amount, data.txid);\n */\nexport function parsePixPayload(payload: string, options: ParsePixOptions = {}): PixData {\n const { requireCrc = true } = options;\n const text = payload.trim();\n if (text.length < 8) throw new PixError(\"Payload is too short to be a BR Code.\");\n\n const crcHeaderAt = text.length - 8;\n if (text.slice(crcHeaderAt, crcHeaderAt + 4) !== `${TAG_CRC}04`) {\n throw new PixError(\"Payload does not end in a `6304` CRC tag.\");\n }\n const crc = text.slice(-4).toUpperCase();\n const expected = pixCrc16(text.slice(0, -4));\n const crcValid = crc === expected;\n if (!crcValid && requireCrc) {\n throw new PixError(`CRC mismatch: payload says ${crc}, recomputed ${expected}.`);\n }\n\n const fields = parseTlv(text.slice(0, crcHeaderAt), \"payload\");\n const merchantAccount = pick(fields, TAG_MERCHANT_ACCOUNT_INFO);\n if (merchantAccount === undefined) {\n throw new PixError(\"Payload has no tag 26 (merchant account information).\");\n }\n const account = parseTlv(merchantAccount, \"tag 26\");\n const gui = pick(account, MAI_TAG_GUI);\n if (gui?.toLowerCase() !== PIX_GUI) {\n throw new PixError(\n `Tag 26 is not a Pix account: expected GUI ${PIX_GUI}, got ${JSON.stringify(gui)}.`,\n );\n }\n\n const url = pick(account, MAI_TAG_URL);\n const key = pick(account, MAI_TAG_KEY);\n const amountField = pick(fields, TAG_TRANSACTION_AMOUNT);\n const txid = pick(\n parseTlv(pick(fields, TAG_ADDITIONAL_DATA) ?? \"\", \"tag 62\"),\n ADDITIONAL_TAG_TXID,\n );\n\n return {\n kind: url !== undefined && key === undefined ? \"dynamic\" : \"static\",\n ...(key === undefined ? {} : { key, keyType: pixKeyType(key) ?? undefined }),\n ...(url === undefined ? {} : { url }),\n merchantName: pick(fields, TAG_MERCHANT_NAME) ?? \"\",\n merchantCity: pick(fields, TAG_MERCHANT_CITY) ?? \"\",\n ...(amountField === undefined ? {} : { amount: Number(amountField) }),\n currency: pick(fields, TAG_TRANSACTION_CURRENCY) ?? \"\",\n countryCode: pick(fields, TAG_COUNTRY_CODE) ?? \"\",\n merchantCategoryCode: pick(fields, TAG_MERCHANT_CATEGORY_CODE) ?? \"\",\n ...(txid === undefined ? {} : { txid }),\n ...(pick(account, MAI_TAG_DESCRIPTION) === undefined\n ? {}\n : { description: pick(account, MAI_TAG_DESCRIPTION) }),\n ...(pick(fields, TAG_POSTAL_CODE) === undefined\n ? {}\n : { postalCode: pick(fields, TAG_POSTAL_CODE) }),\n oneTime: pick(fields, TAG_POINT_OF_INITIATION) === POINT_OF_INITIATION_SINGLE_USE,\n crc,\n crcValid,\n fields,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"pix.cjs","names":[],"sources":["../../src/br/pix.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — EMV®QRCPS-MPM for Pix: the TLV writer, the\n * CRC16-CCITT, the field catalogue with its nesting, key-type detection and the\n * parser that has to accept what other banks emit. Reader and writer live together\n * because each one is the other's test — a payload this file builds is a payload it\n * must parse back.\n */\nimport { validateCNPJ, validateCPF } from \"@/forms/br-validators\";\n\n/**\n * A Pix payload could not be built or read.\n *\n * Its own class so a caller can tell \"the operator typed a bad key\" apart from a\n * bug, and so an `ErrorBoundary` can render a form error instead of a crash.\n */\nexport class PixError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"PixError\";\n }\n}\n\n/** The five key formats DICT accepts. */\nexport type PixKeyType = \"cpf\" | \"cnpj\" | \"email\" | \"phone\" | \"evp\";\n\n/** A key after normalisation, ready to go into the payload. */\nexport interface NormalizedPixKey {\n type: PixKeyType;\n /** The exact string written into the BR Code. */\n value: string;\n}\n\n/** One TLV as it appeared in a payload, unknown tags included. */\nexport interface PixField {\n /** Two-character tag, e.g. `\"59\"`. */\n id: string;\n /** Raw value, still encoded when the tag is itself a template. */\n value: string;\n}\n\n/** A BR Code that carries the key — the one you print on a poster. */\nexport interface PixStaticInput {\n kind?: \"static\";\n /** CPF, CNPJ, e-mail, phone or EVP. Validated and normalised. */\n key: string;\n /** Payee name. Truncated by the spec at 25 characters — longer throws. */\n merchantName: string;\n /** Payee city. Truncated by the spec at 15 characters — longer throws. */\n merchantCity: string;\n /** Amount in BRL. Omit for a payer-chooses-the-value QR. */\n amount?: number;\n /** Reference the PSP echoes back, `[A-Za-z0-9]{1,25}`. Defaults to `\"***\"`. */\n txid?: string;\n /** Free text shown by some wallets. Goes into tag 26, sub-tag 02. */\n description?: string;\n /** CEP, digits only. Optional tag 61. */\n postalCode?: string;\n /** Single-use QR: sets tag 01 to `\"12\"` instead of `\"11\"`. */\n oneTime?: boolean;\n}\n\n/** A BR Code that carries a URL the wallet fetches to learn the amount. */\nexport interface PixDynamicInput {\n kind: \"dynamic\";\n /**\n * `payloadLocation` — the https URL the wallet GETs, **without** the scheme,\n * exactly as BACEN specifies (`pix.example.com/qr/v2/abc`).\n */\n url: string;\n merchantName: string;\n merchantCity: string;\n postalCode?: string;\n /** Single-use QR. Defaults to `true`, which is what a dynamic QR normally is. */\n oneTime?: boolean;\n}\n\n/** Everything `pixPayload` accepts. */\nexport type PixInput = PixStaticInput | PixDynamicInput;\n\n/** A payload taken apart again. */\nexport interface PixData {\n /** `\"dynamic\"` when tag 26 carried a URL instead of a key. */\n kind: \"static\" | \"dynamic\";\n /** Present on a static payload. */\n key?: string;\n keyType?: PixKeyType;\n /** Present on a dynamic payload. */\n url?: string;\n merchantName: string;\n merchantCity: string;\n /** Reais. `undefined` when the payer chooses the amount. */\n amount?: number;\n /** ISO 4217 numeric. `\"986\"` for BRL. */\n currency: string;\n countryCode: string;\n merchantCategoryCode: string;\n /** `\"***\"` on a reusable static QR that identifies no single transaction. */\n txid?: string;\n description?: string;\n postalCode?: string;\n /** Tag 01 read as `\"12\"`. */\n oneTime: boolean;\n /** The four hex characters that closed the payload. */\n crc: string;\n /** Whether those four characters match a recomputed CRC. */\n crcValid: boolean;\n /** Every top-level TLV, in payload order, unknown tags included. */\n fields: PixField[];\n}\n\n/** Options for {@link parsePixPayload}. */\nexport interface ParsePixOptions {\n /**\n * Throw when the checksum does not match. Default `true`.\n *\n * Turn it off only to inspect a payload you already know is broken: a BR Code\n * whose CRC fails has been corrupted in transit, and the account it now points\n * at is not the account the payee published.\n */\n requireCrc?: boolean;\n}\n\n/**\n * Generator polynomial of CRC-16/CCITT-FALSE, `x^16 + x^12 + x^5 + 1`.\n *\n * Taken from the CRC catalogue entry `CRC-16/IBM-3740` (alias CCITT-FALSE):\n * `width=16 poly=0x1021 init=0xffff refin=false refout=false xorout=0x0000\n * check=0x29b1`. The BACEN \"Manual de Padrões para Iniciação do Pix\" names this\n * exact variant for tag 63.\n */\nconst CRC16_POLYNOMIAL = 0x1021;\n\n/** Register preset of CRC-16/CCITT-FALSE. Not zero — that is a different variant. */\nconst CRC16_INITIAL = 0xffff;\n\n/** Keeps the shift register 16 bits wide. */\nconst CRC16_MASK = 0xffff;\n\nconst TAG_PAYLOAD_FORMAT = \"00\";\nconst TAG_POINT_OF_INITIATION = \"01\";\nconst TAG_MERCHANT_ACCOUNT_INFO = \"26\";\nconst TAG_MERCHANT_CATEGORY_CODE = \"52\";\nconst TAG_TRANSACTION_CURRENCY = \"53\";\nconst TAG_TRANSACTION_AMOUNT = \"54\";\nconst TAG_COUNTRY_CODE = \"58\";\nconst TAG_MERCHANT_NAME = \"59\";\nconst TAG_MERCHANT_CITY = \"60\";\nconst TAG_POSTAL_CODE = \"61\";\nconst TAG_ADDITIONAL_DATA = \"62\";\nconst TAG_CRC = \"63\";\n\nconst MAI_TAG_GUI = \"00\";\nconst MAI_TAG_KEY = \"01\";\nconst MAI_TAG_DESCRIPTION = \"02\";\nconst MAI_TAG_URL = \"25\";\nconst ADDITIONAL_TAG_TXID = \"05\";\n\n/** Globally Unique Identifier that marks tag 26 as a Pix account. */\nconst PIX_GUI = \"br.gov.bcb.pix\";\n\nconst PAYLOAD_FORMAT_VERSION = \"01\";\nconst POINT_OF_INITIATION_REUSABLE = \"11\";\nconst POINT_OF_INITIATION_SINGLE_USE = \"12\";\nconst DEFAULT_MERCHANT_CATEGORY_CODE = \"0000\";\nconst CURRENCY_BRL = \"986\";\nconst COUNTRY_BR = \"BR\";\nconst TXID_UNSPECIFIED = \"***\";\n\nconst MAX_MERCHANT_NAME = 25;\nconst MAX_MERCHANT_CITY = 15;\nconst MAX_TXID = 25;\nconst MAX_TLV_VALUE = 99;\nconst MAX_EMAIL_KEY = 77;\n\n/**\n * Tag 54 as EMV® QRCPS-MPM writes it: digits, and at most one `.` as the decimal\n * mark, which \"may be present even if there are no decimals\" (`\"98.\"`). Up to two\n * decimals, the minor unit ISO 4217 gives BRL (`986`). No sign, exponent, comma,\n * hex prefix or whitespace — every one of which `Number()` would happily read.\n *\n * Zero (`\"0\"`, `\"0.00\"`) is accepted: EMV asks for a non-zero amount, but the BACEN\n * BR Code manual (v2, Table 1) gives `\"0\"` as an example value of tag 54, and the\n * parser reads payloads other PSPs emit — strict with what is malformed, tolerant\n * with what the Bacen spec accepts. The builder stays stricter: `pixPayload`\n * still refuses a non-positive amount, because it only writes what we choose to emit.\n */\nconst AMOUNT_FIELD = /^\\d+(?:\\.\\d{0,2})?$/;\n\n/** Width of tag 54: \"01..13\" in the BACEN BR Code manual, Table 1. */\nconst MAX_AMOUNT_FIELD = 13;\n\n/**\n * CRC-16/CCITT-FALSE of a string, as four upper-case hex characters.\n *\n * Bitwise rather than table-driven: 200-odd characters at 8 shifts each is\n * nothing, and a 256-entry table is 2 KB of payload every consumer of the `/br`\n * entry would carry.\n *\n * The input is taken as UTF-8 bytes. A Pix payload is ASCII by construction —\n * {@link pixPayload} rejects anything else — but the function is exported and a\n * caller may hand it arbitrary text, and hashing UTF-16 code units would then\n * disagree with every other implementation.\n *\n * @param input - Bytes to run through the register.\n * @returns Four upper-case hex characters, zero-padded.\n *\n * @example\n * pixCrc16(\"123456789\"); // \"29B1\" — the catalogue check value\n */\nexport function pixCrc16(input: string): string {\n const bytes = new TextEncoder().encode(input);\n let crc = CRC16_INITIAL;\n for (const byte of bytes) {\n crc ^= byte << 8;\n for (let bit = 0; bit < 8; bit += 1) {\n crc =\n (crc & 0x8000) !== 0\n ? ((crc << 1) ^ CRC16_POLYNOMIAL) & CRC16_MASK\n : (crc << 1) & CRC16_MASK;\n }\n }\n return crc.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/** Digits of a possibly masked value. */\nfunction digits(value: string): string {\n return value.replace(/\\D/g, \"\");\n}\n\n/**\n * Drop diacritics and reject whatever is left outside printable ASCII.\n *\n * The BR Code character set has no room for accents, and a wallet that meets one\n * either fails to parse the QR or shows mojibake. Stripping them is lossy but\n * legible (\"São Paulo\" → \"Sao Paulo\"), which beats both alternatives; anything\n * that is not a diacritic — an emoji, a CJK character — is a mistake the caller\n * has to see, so it throws.\n */\nfunction toPayloadText(value: string, field: string): string {\n const stripped = value.normalize(\"NFD\").replace(/\\p{Diacritic}/gu, \"\");\n if (/[^\\x20-\\x7E]/.test(stripped)) {\n throw new PixError(\n `${field} has characters the BR Code cannot carry: ${JSON.stringify(value)}. ` +\n \"Use unaccented ASCII.\",\n );\n }\n return stripped;\n}\n\n/**\n * The EMV \"Common Character Set\" that an `ans` (Alphanumeric Special) field may\n * carry: printable ASCII, `0x20`–`0x7E`. Every root field of a BR Code is `ans`,\n * and the BACEN BR Code manual (v2, May 2020) only widened it from alphanumeric —\n * \"The restriction that established that only alphanumeric characters could be\n * used has been removed\" — not past it. Only the alternate-language template\n * (tag 64) is `String` and may carry another character set, and the builder\n * never writes it.\n */\nconst COMMON_CHARACTER_SET = /^[\\x20-\\x7E]*$/;\n\n/**\n * One `ID + 2-digit length + value` triple.\n *\n * The length is \"the number of characters included in the [value] field\" (EMV\n * MPM, as restated by the HKMA Common QR Code Specification §2; the BACEN manual:\n * \"size specifies how many characters\"). Inside the common character set a\n * character is one UTF-16 unit and one UTF-8 byte, so `value.length` is exact —\n * which is why a value outside it throws instead of being measured.\n *\n * @throws {PixError} When the value leaves the common character set or exceeds\n * the two-digit length.\n */\nfunction tlv(id: string, value: string): string {\n if (!COMMON_CHARACTER_SET.test(value)) {\n throw new PixError(\n `Tag ${id} has characters outside the EMV common character set (printable ASCII): ` +\n `${JSON.stringify(value)}.`,\n );\n }\n if (value.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag ${id} is ${value.length} characters; the EMV length field holds at most ${MAX_TLV_VALUE}.`,\n );\n }\n return `${id}${String(value.length).padStart(2, \"0\")}${value}`;\n}\n\n/**\n * Classify a Pix key, or `null` when it matches no accepted format.\n *\n * !!! warning \"CPF and a national phone number are both eleven digits\"\n * `\"11987654321\"` is a valid mobile number and could be a CPF. The check\n * digits break the tie: an 11-digit string is a CPF when its DV validates and\n * a phone otherwise. Pass phone keys as `+5511987654321` to remove the guess\n * entirely.\n *\n * @param key - Raw key, masked or not.\n * @returns The key type, or `null`.\n */\nexport function pixKeyType(key: string): PixKeyType | null {\n const trimmed = key.trim();\n if (trimmed === \"\") return null;\n\n if (trimmed.includes(\"@\")) {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]{2,}$/.test(trimmed) && trimmed.length <= MAX_EMAIL_KEY\n ? \"email\"\n : null;\n }\n if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmed)) {\n return \"evp\";\n }\n\n const numbers = digits(trimmed);\n if (numbers.length === 14) return validateCNPJ(numbers) ? \"cnpj\" : null;\n if (numbers.length === 11 && validateCPF(numbers)) return \"cpf\";\n if (trimmed.startsWith(\"+\")) {\n return /^\\+55\\d{10,11}$/.test(`+${numbers}`) ? \"phone\" : null;\n }\n if (numbers.length === 10 || numbers.length === 11) return \"phone\";\n if (numbers.length === 12 || numbers.length === 13) {\n return numbers.startsWith(\"55\") ? \"phone\" : null;\n }\n return null;\n}\n\n/**\n * Validate a Pix key and return the exact string to write into the payload.\n *\n * Normalisation per type: CPF and CNPJ lose their mask, a phone becomes E.164\n * with the `+55` country code, an EVP is lower-cased, and an e-mail is\n * lower-cased because DICT stores it that way — a key that differs only in case\n * would otherwise fail to resolve.\n *\n * All validation lives in {@link pixKeyType}; past that gate the normalisation is\n * total, which is why the phone branch strips a country code purely on length\n * rather than re-checking the shape.\n *\n * @param key - Raw key, masked or not.\n * @returns The type and the normalised value.\n * @throws {PixError} When the key matches no accepted format, or when a document\n * key fails its check digits.\n *\n * @example\n * normalizePixKey(\"123.456.789-09\"); // { type: \"cpf\", value: \"12345678909\" }\n * normalizePixKey(\"(11) 98765-4321\"); // { type: \"phone\", value: \"+5511987654321\" }\n */\nexport function normalizePixKey(key: string): NormalizedPixKey {\n const trimmed = key.trim();\n const type = pixKeyType(trimmed);\n if (type === null) {\n throw new PixError(\n `Not a Pix key: ${JSON.stringify(key)}. Expected a CPF, CNPJ, e-mail, ` +\n \"phone (+5511987654321) or EVP (UUID).\",\n );\n }\n\n if (type === \"email\") return { type, value: trimmed.toLowerCase() };\n if (type === \"evp\") return { type, value: trimmed.toLowerCase() };\n if (type === \"phone\") {\n const numbers = digits(trimmed);\n return { type, value: `+55${numbers.length > 11 ? numbers.slice(2) : numbers}` };\n }\n return { type, value: digits(trimmed) };\n}\n\n/**\n * Format an amount the way tag 54 wants it: dot separator, two decimals.\n *\n * Positivity is checked on the rounded field, not on the input: `0.004` is\n * positive and still writes `\"0.00\"`, a zero-value QR for a positive charge.\n */\nfunction toAmountField(amount: number): string {\n if (!Number.isFinite(amount)) {\n throw new PixError(`Amount must be a finite number, got ${amount}.`);\n }\n if (amount <= 0) {\n throw new PixError(\n `Amount must be positive, got ${amount}. Omit \\`amount\\` for a QR whose value the payer types.`,\n );\n }\n const field = amount.toFixed(2);\n if (Number(field) <= 0) {\n throw new PixError(\n `Amount ${amount} rounds to ${field} at two decimals. Omit \\`amount\\` for a QR whose value the payer types.`,\n );\n }\n if (field.length > 13) {\n throw new PixError(`Amount ${field} does not fit tag 54, which holds 13 characters.`);\n }\n return field;\n}\n\n/** Bound the two free-text identity fields the spec caps hard. */\nfunction toBoundedText(value: string, max: number, field: string): string {\n const text = toPayloadText(value.trim(), field);\n if (text === \"\") throw new PixError(`${field} is required.`);\n if (text.length > max) {\n throw new PixError(\n `${field} is ${text.length} characters; the BR Code allows ${max}. Shorten it — ` +\n \"truncating here would silently change what the payer sees.\",\n );\n }\n return text;\n}\n\n/** Tag 62, which exists only to carry the txid. */\nfunction additionalDataField(txid: string | undefined): string {\n const value = txid?.trim() ?? \"\";\n if (value === \"\" || value === TXID_UNSPECIFIED) {\n return tlv(TAG_ADDITIONAL_DATA, tlv(ADDITIONAL_TAG_TXID, TXID_UNSPECIFIED));\n }\n if (!new RegExp(`^[A-Za-z0-9]{1,${MAX_TXID}}$`).test(value)) {\n throw new PixError(\n `txid must be 1 to ${MAX_TXID} letters or digits, got ${JSON.stringify(txid)}.`,\n );\n }\n return tlv(TAG_ADDITIONAL_DATA, tlv(ADDITIONAL_TAG_TXID, value));\n}\n\n/** Tag 26 for a static payload: GUI, key and the optional description. */\nfunction staticMerchantAccount(input: PixStaticInput): string {\n const { value } = normalizePixKey(input.key);\n let inner = tlv(MAI_TAG_GUI, PIX_GUI) + tlv(MAI_TAG_KEY, value);\n const description = input.description?.trim();\n if (description !== undefined && description !== \"\") {\n inner += tlv(MAI_TAG_DESCRIPTION, toPayloadText(description, \"description\"));\n }\n if (inner.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag 26 is ${inner.length} characters, over the ${MAX_TLV_VALUE} the length field allows. ` +\n \"Shorten `description`.\",\n );\n }\n return tlv(TAG_MERCHANT_ACCOUNT_INFO, inner);\n}\n\n/** Tag 26 for a dynamic payload: GUI plus the URL, and no key. */\nfunction dynamicMerchantAccount(input: PixDynamicInput): string {\n const url = toPayloadText(input.url.trim(), \"url\").replace(/^https?:\\/\\//i, \"\");\n if (url === \"\") throw new PixError(\"url is required for a dynamic BR Code.\");\n const inner = tlv(MAI_TAG_GUI, PIX_GUI) + tlv(MAI_TAG_URL, url);\n if (inner.length > MAX_TLV_VALUE) {\n throw new PixError(\n `Tag 26 is ${inner.length} characters, over the ${MAX_TLV_VALUE} the length field allows. ` +\n \"Shorten the payload URL.\",\n );\n }\n return tlv(TAG_MERCHANT_ACCOUNT_INFO, inner);\n}\n\n/**\n * Build a Pix \"Copia e Cola\" payload — the string behind a Pix QR code.\n *\n * The format is EMVCo MPM: a flat list of `ID + 2-digit length + value` triples,\n * closed by tag 63 holding a CRC-16/CCITT-FALSE over **everything before it,\n * including the literal `6304` header of tag 63 itself**. That last detail is the\n * one implementations get wrong; see {@link pixCrc16}.\n *\n * Two shapes come out of here:\n *\n * - **static** — tag 26 carries the key, so the QR is self-contained and can be\n * printed. Amount optional; a txid of `\"***\"` means \"identifies no single\n * transaction\", which is what a reusable poster QR wants.\n * - **dynamic** — tag 26 carries a URL instead, and the wallet fetches the amount\n * and payee from the PSP. Use it when the value is per-order. Defaults to\n * single-use (tag 01 = `12`).\n *\n * The distinction matters and is not cosmetic: a static QR settles against\n * whatever the payer typed, a dynamic one against what the PSP served, so a\n * charge that must reconcile to a cent needs the dynamic form.\n *\n * @param input - Static or dynamic payload description.\n * @returns The full payload, CRC included, ready to render as a QR or to copy.\n * @throws {PixError} On an unrecognised key, a field over its length cap, a\n * non-positive amount, or text that is not representable in the BR Code.\n *\n * @example\n * pixPayload({\n * key: \"12345678909\",\n * merchantName: \"Loja Tempest\",\n * merchantCity: \"São Paulo\",\n * amount: 25.5,\n * txid: \"PEDIDO123\",\n * });\n */\nexport function pixPayload(input: PixInput): string {\n const dynamic = input.kind === \"dynamic\";\n const oneTime = input.oneTime ?? dynamic;\n\n let payload = tlv(TAG_PAYLOAD_FORMAT, PAYLOAD_FORMAT_VERSION);\n payload += tlv(\n TAG_POINT_OF_INITIATION,\n oneTime ? POINT_OF_INITIATION_SINGLE_USE : POINT_OF_INITIATION_REUSABLE,\n );\n payload += dynamic ? dynamicMerchantAccount(input) : staticMerchantAccount(input);\n payload += tlv(TAG_MERCHANT_CATEGORY_CODE, DEFAULT_MERCHANT_CATEGORY_CODE);\n payload += tlv(TAG_TRANSACTION_CURRENCY, CURRENCY_BRL);\n if (!dynamic && input.amount !== undefined) {\n payload += tlv(TAG_TRANSACTION_AMOUNT, toAmountField(input.amount));\n }\n payload += tlv(TAG_COUNTRY_CODE, COUNTRY_BR);\n payload += tlv(\n TAG_MERCHANT_NAME,\n toBoundedText(input.merchantName, MAX_MERCHANT_NAME, \"merchantName\"),\n );\n payload += tlv(\n TAG_MERCHANT_CITY,\n toBoundedText(input.merchantCity, MAX_MERCHANT_CITY, \"merchantCity\"),\n );\n const postalCode = input.postalCode === undefined ? \"\" : digits(input.postalCode);\n if (postalCode !== \"\") payload += tlv(TAG_POSTAL_CODE, postalCode);\n payload += additionalDataField(dynamic ? TXID_UNSPECIFIED : input.txid);\n\n const withCrcHeader = `${payload}${TAG_CRC}04`;\n return `${withCrcHeader}${pixCrc16(withCrcHeader)}`;\n}\n\n/**\n * Split a flat run of TLVs. Stops cleanly at the first malformed triple.\n *\n * An ID may appear once per level. The BACEN BR Code manual (v2, May 2020)\n * states it for the root — \"there are no duplicate IDs in the document's root,\n * according to\" EMV® QRCPS-MPM;\n * the same holds here inside tags 26 and 62, which no conforming emitter\n * repeats either. Keeping the first of two values would give the caller one\n * reading of the payload while a wallet that keeps the last pays another.\n *\n * Lengths count characters (code points), as EMV defines them — see {@link tlv}.\n * A payload from another PSP may carry a `String` template (tag 64, or an\n * unreserved one) with non-ASCII text, where counting UTF-16 units would split\n * a character and misread every tag after it.\n *\n * @throws {PixError} When a length prefix is not two digits, runs past the end,\n * or an ID repeats at this level.\n */\nfunction parseTlv(text: string, where: string): PixField[] {\n const input = Array.from(text);\n const fields: PixField[] = [];\n let cursor = 0;\n while (cursor < input.length) {\n const id = input.slice(cursor, cursor + 2).join(\"\");\n const rawLength = input.slice(cursor + 2, cursor + 4).join(\"\");\n if (!/^\\d{2}$/.test(id) || !/^\\d{2}$/.test(rawLength)) {\n throw new PixError(\n `Malformed TLV in ${where} at offset ${cursor}: expected a 2-digit tag and length.`,\n );\n }\n const length = Number(rawLength);\n const start = cursor + 4;\n if (start + length > input.length) {\n throw new PixError(\n `Tag ${id} in ${where} declares ${length} characters but only ${input.length - start} remain.`,\n );\n }\n if (fields.some((field) => field.id === id)) {\n throw new PixError(\n `Tag ${id} in ${where} appears more than once; a BR Code carries each tag once per level.`,\n );\n }\n fields.push({ id, value: input.slice(start, start + length).join(\"\") });\n cursor = start + length;\n }\n return fields;\n}\n\n/** The value of a tag — unique per level, see {@link parseTlv} — or `undefined`. */\nfunction pick(fields: readonly PixField[], id: string): string | undefined {\n return fields.find((field) => field.id === id)?.value;\n}\n\n/**\n * Read tag 54 into reais.\n *\n * @throws {PixError} When the field is outside the EMV format\n * ({@link AMOUNT_FIELD}) or wider than 13 characters. Zero is read as `0`; see\n * {@link AMOUNT_FIELD} for why the reader accepts what `pixPayload` refuses.\n */\nfunction readAmountField(field: string): number {\n if (field.length > MAX_AMOUNT_FIELD || !AMOUNT_FIELD.test(field)) {\n throw new PixError(\n `Tag 54 (amount) must be an amount like \"25.50\", with \".\" as the decimal mark, ` +\n `at most two decimals and ${MAX_AMOUNT_FIELD} characters; got ${JSON.stringify(field)}.`,\n );\n }\n return Number(field);\n}\n\n/**\n * Read a Pix \"Copia e Cola\" payload back into its parts.\n *\n * Tolerant by design: tags the SDK does not know about are kept verbatim in\n * {@link PixData.fields} instead of raising, because PSPs do add their own\n * templates and a reader that rejects them is useless in production. What is\n * *not* tolerated is a broken frame — a length prefix that runs off the end, a\n * missing tag 63 — a checksum mismatch, which means the string was corrupted in\n * transit, or a payload with two readings: a tag repeated at the root or inside\n * tags 26 and 62, a tag 26 with both a key and a URL, or a tag 54 outside the EMV\n * amount format. A preview built from one reading while the wallet pays the\n * other is the failure those rules exist for.\n *\n * The CRC detects corruption, not authorship: whoever crafts a payload\n * recomputes it, so a valid CRC says nothing about who published the account.\n *\n * @param payload - The copia-e-cola string. Surrounding whitespace is ignored.\n * @param options - See {@link ParsePixOptions}.\n * @returns The decoded payload.\n * @throws {PixError} On a malformed frame, a missing CRC tag, a repeated tag, a\n * tag 26 that is not a Pix account or that carries both a key and a URL, an\n * amount outside the EMV format, or — unless `requireCrc` is `false` — a CRC\n * mismatch.\n *\n * @example\n * const data = parsePixPayload(copied);\n * console.log(data.key, data.amount, data.txid);\n */\nexport function parsePixPayload(payload: string, options: ParsePixOptions = {}): PixData {\n const { requireCrc = true } = options;\n const text = payload.trim();\n if (text.length < 8) throw new PixError(\"Payload is too short to be a BR Code.\");\n\n const crcHeaderAt = text.length - 8;\n if (text.slice(crcHeaderAt, crcHeaderAt + 4) !== `${TAG_CRC}04`) {\n throw new PixError(\"Payload does not end in a `6304` CRC tag.\");\n }\n const crc = text.slice(-4).toUpperCase();\n const expected = pixCrc16(text.slice(0, -4));\n const crcValid = crc === expected;\n if (!crcValid && requireCrc) {\n throw new PixError(`CRC mismatch: payload says ${crc}, recomputed ${expected}.`);\n }\n\n const fields = parseTlv(text.slice(0, crcHeaderAt), \"payload\");\n const merchantAccount = pick(fields, TAG_MERCHANT_ACCOUNT_INFO);\n if (merchantAccount === undefined) {\n throw new PixError(\"Payload has no tag 26 (merchant account information).\");\n }\n const account = parseTlv(merchantAccount, \"tag 26\");\n const gui = pick(account, MAI_TAG_GUI);\n if (gui?.toLowerCase() !== PIX_GUI) {\n throw new PixError(\n `Tag 26 is not a Pix account: expected GUI ${PIX_GUI}, got ${JSON.stringify(gui)}.`,\n );\n }\n\n const url = pick(account, MAI_TAG_URL);\n const key = pick(account, MAI_TAG_KEY);\n if (url !== undefined && key !== undefined) {\n throw new PixError(\n \"Tag 26 carries both a key (01) and a URL (25); a BR Code is either static or dynamic, not both.\",\n );\n }\n const amountField = pick(fields, TAG_TRANSACTION_AMOUNT);\n const txid = pick(\n parseTlv(pick(fields, TAG_ADDITIONAL_DATA) ?? \"\", \"tag 62\"),\n ADDITIONAL_TAG_TXID,\n );\n\n return {\n kind: url === undefined ? \"static\" : \"dynamic\",\n ...(key === undefined ? {} : { key, keyType: pixKeyType(key) ?? undefined }),\n ...(url === undefined ? {} : { url }),\n merchantName: pick(fields, TAG_MERCHANT_NAME) ?? \"\",\n merchantCity: pick(fields, TAG_MERCHANT_CITY) ?? \"\",\n ...(amountField === undefined ? {} : { amount: readAmountField(amountField) }),\n currency: pick(fields, TAG_TRANSACTION_CURRENCY) ?? \"\",\n countryCode: pick(fields, TAG_COUNTRY_CODE) ?? \"\",\n merchantCategoryCode: pick(fields, TAG_MERCHANT_CATEGORY_CODE) ?? \"\",\n ...(txid === undefined ? {} : { txid }),\n ...(pick(account, MAI_TAG_DESCRIPTION) === undefined\n ? {}\n : { description: pick(account, MAI_TAG_DESCRIPTION) }),\n ...(pick(fields, TAG_POSTAL_CODE) === undefined\n ? {}\n : { postalCode: pick(fields, TAG_POSTAL_CODE) }),\n oneTime: pick(fields, TAG_POINT_OF_INITIATION) === POINT_OF_INITIATION_SINGLE_USE,\n crc,\n crcValid,\n fields,\n };\n}\n"],"mappings":"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"}