@usequeek/app-sdk 0.6.2 → 0.7.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 (107) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +340 -337
  3. package/dist/app-auth.d.ts +1 -2
  4. package/dist/app-auth.js +1 -2
  5. package/dist/auth-fetch.d.ts +12 -1
  6. package/dist/auth-fetch.js +13 -2
  7. package/dist/background.d.ts +9 -1
  8. package/dist/background.js +9 -1
  9. package/dist/browser.d.ts +1 -2
  10. package/dist/browser.js +1 -2
  11. package/dist/client.d.ts +130 -7
  12. package/dist/client.js +198 -41
  13. package/dist/collected.d.ts +0 -1
  14. package/dist/collected.js +0 -1
  15. package/dist/crypto.d.ts +11 -1
  16. package/dist/crypto.js +22 -7
  17. package/dist/delivery.d.ts +18 -1
  18. package/dist/delivery.js +26 -17
  19. package/dist/frame.d.ts +0 -1
  20. package/dist/frame.js +0 -1
  21. package/dist/handoff.d.ts +0 -1
  22. package/dist/handoff.js +0 -1
  23. package/dist/hono.d.ts +0 -1
  24. package/dist/hono.js +0 -1
  25. package/dist/index.d.ts +1 -2
  26. package/dist/index.js +1 -2
  27. package/dist/install-handlers.d.ts +22 -2
  28. package/dist/install-handlers.js +37 -7
  29. package/dist/logger.d.ts +7 -1
  30. package/dist/logger.js +7 -1
  31. package/dist/merchant-schema.d.ts +1184 -336
  32. package/dist/merchant-schema.js +0 -1
  33. package/dist/proxy.d.ts +2 -3
  34. package/dist/proxy.js +4 -5
  35. package/dist/react.d.ts +9 -1
  36. package/dist/react.js +10 -2
  37. package/dist/resync.d.ts +11 -1
  38. package/dist/resync.js +18 -16
  39. package/dist/scopes.d.ts +14 -1
  40. package/dist/scopes.js +18 -17
  41. package/dist/server.d.ts +0 -1
  42. package/dist/server.js +0 -1
  43. package/dist/session.d.ts +20 -1
  44. package/dist/session.js +20 -1
  45. package/dist/signatures.d.ts +26 -1
  46. package/dist/signatures.js +29 -3
  47. package/dist/store.d.ts +6 -1
  48. package/dist/store.js +0 -1
  49. package/dist/theme.d.ts +0 -1
  50. package/dist/theme.js +0 -1
  51. package/dist/tokens.d.ts +9 -1
  52. package/dist/tokens.js +23 -47
  53. package/dist/webhooks.d.ts +28 -3
  54. package/dist/webhooks.js +25 -7
  55. package/package.json +18 -2
  56. package/dist/app-auth.d.ts.map +0 -1
  57. package/dist/app-auth.js.map +0 -1
  58. package/dist/auth-fetch.d.ts.map +0 -1
  59. package/dist/auth-fetch.js.map +0 -1
  60. package/dist/background.d.ts.map +0 -1
  61. package/dist/background.js.map +0 -1
  62. package/dist/browser.d.ts.map +0 -1
  63. package/dist/browser.js.map +0 -1
  64. package/dist/client.d.ts.map +0 -1
  65. package/dist/client.js.map +0 -1
  66. package/dist/collected.d.ts.map +0 -1
  67. package/dist/collected.js.map +0 -1
  68. package/dist/crypto.d.ts.map +0 -1
  69. package/dist/crypto.js.map +0 -1
  70. package/dist/delivery.d.ts.map +0 -1
  71. package/dist/delivery.js.map +0 -1
  72. package/dist/frame.d.ts.map +0 -1
  73. package/dist/frame.js.map +0 -1
  74. package/dist/handoff.d.ts.map +0 -1
  75. package/dist/handoff.js.map +0 -1
  76. package/dist/hono.d.ts.map +0 -1
  77. package/dist/hono.js.map +0 -1
  78. package/dist/index.d.ts.map +0 -1
  79. package/dist/index.js.map +0 -1
  80. package/dist/install-handlers.d.ts.map +0 -1
  81. package/dist/install-handlers.js.map +0 -1
  82. package/dist/logger.d.ts.map +0 -1
  83. package/dist/logger.js.map +0 -1
  84. package/dist/merchant-schema.d.ts.map +0 -1
  85. package/dist/merchant-schema.js.map +0 -1
  86. package/dist/proxy.d.ts.map +0 -1
  87. package/dist/proxy.js.map +0 -1
  88. package/dist/react.d.ts.map +0 -1
  89. package/dist/react.js.map +0 -1
  90. package/dist/resync.d.ts.map +0 -1
  91. package/dist/resync.js.map +0 -1
  92. package/dist/scopes.d.ts.map +0 -1
  93. package/dist/scopes.js.map +0 -1
  94. package/dist/server.d.ts.map +0 -1
  95. package/dist/server.js.map +0 -1
  96. package/dist/session.d.ts.map +0 -1
  97. package/dist/session.js.map +0 -1
  98. package/dist/signatures.d.ts.map +0 -1
  99. package/dist/signatures.js.map +0 -1
  100. package/dist/store.d.ts.map +0 -1
  101. package/dist/store.js.map +0 -1
  102. package/dist/theme.d.ts.map +0 -1
  103. package/dist/theme.js.map +0 -1
  104. package/dist/tokens.d.ts.map +0 -1
  105. package/dist/tokens.js.map +0 -1
  106. package/dist/webhooks.d.ts.map +0 -1
  107. package/dist/webhooks.js.map +0 -1
@@ -8,4 +8,3 @@
8
8
  * Do not make direct changes to the file.
9
9
  */
10
10
  export {};
11
- //# sourceMappingURL=merchant-schema.js.map
package/dist/proxy.d.ts CHANGED
@@ -10,7 +10,7 @@ import { type InstallationRecord, type InstallationStore } from "./store.js";
10
10
  * sig = hex HMAC-SHA256 over
11
11
  * path + "\n" + shop + "\n" + ts + "\n" + sorted(k=v&...) (sig excluded),
12
12
  *
13
- * where each pair is `rawurlencode(key)=rawurlencode(value)` and keys sort
13
+ * where each pair is `encode(key)=encode(value)` (RFC 3986 percent-encoding) and keys sort
14
14
  * byte-wise. The HMAC key is the FULL `whsec_…` string — no base64 decode
15
15
  * step (the single most common integration bug cannot happen here).
16
16
  *
@@ -38,7 +38,7 @@ export type ProxyQuery = Record<string, string | string[] | null | undefined>;
38
38
  export type ProxyFailure = "missing_params" | "stale_timestamp" | "missing_secret" | "signature_mismatch" | "unknown_installation" | "replayed";
39
39
  /**
40
40
  * The canonical string the MAC covers, exactly as Queek builds it (sig
41
- * excluded, keys byte-sorted, rawurlencoded pairs).
41
+ * excluded, keys byte-sorted, percent-encoded pairs).
42
42
  */
43
43
  export declare function buildProxyCanonicalString(path: string, params: Record<string, string>): string;
44
44
  /**
@@ -117,4 +117,3 @@ export type ProxyResponder = (verified: {
117
117
  * anything else answers 405.
118
118
  */
119
119
  export declare function handleProxyRequest(request: Request, options: ProxyRequestOptions, onVerified: ProxyResponder): Promise<Response>;
120
- //# sourceMappingURL=proxy.d.ts.map
package/dist/proxy.js CHANGED
@@ -12,7 +12,7 @@ import { normalisePid } from "./store.js";
12
12
  * sig = hex HMAC-SHA256 over
13
13
  * path + "\n" + shop + "\n" + ts + "\n" + sorted(k=v&...) (sig excluded),
14
14
  *
15
- * where each pair is `rawurlencode(key)=rawurlencode(value)` and keys sort
15
+ * where each pair is `encode(key)=encode(value)` (RFC 3986 percent-encoding) and keys sort
16
16
  * byte-wise. The HMAC key is the FULL `whsec_…` string — no base64 decode
17
17
  * step (the single most common integration bug cannot happen here).
18
18
  *
@@ -34,13 +34,13 @@ export const PROXY_CUSTOMER_PARAM = "logged_in_customer_id";
34
34
  export const PROXY_KID_PARAM = "kid";
35
35
  /** Default allowed clock skew in seconds; the floor (60s) below applies regardless. */
36
36
  export const PROXY_MAX_SKEW_SECONDS = 300;
37
- /** RFC 3986 percent-encoding (PHP's `rawurlencode`): everything except `[A-Za-z0-9-_.~]` is `%XX` (upper-case hex). */
37
+ /** RFC 3986 percent-encoding: everything except `[A-Za-z0-9-_.~]` is `%XX` (upper-case hex), the encoding Queek signs with. */
38
38
  function proxyEncode(value) {
39
39
  return encodeURIComponent(value).replace(/[!'()*]/g, (ch) => {
40
40
  return `%${ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`;
41
41
  });
42
42
  }
43
- /** Scalar query values as strings (PHP `strval` semantics): null/undefined become `""`, everything else stringifies. */
43
+ /** Scalar query values as strings, as Queek's signer reads them: null becomes `""`, an array contributes its first value, everything else stringifies. `undefined` means absent. */
44
44
  function proxyString(value) {
45
45
  if (value === undefined)
46
46
  return undefined;
@@ -60,7 +60,7 @@ function normalizeQuery(query) {
60
60
  }
61
61
  /**
62
62
  * The canonical string the MAC covers, exactly as Queek builds it (sig
63
- * excluded, keys byte-sorted, rawurlencoded pairs).
63
+ * excluded, keys byte-sorted, percent-encoded pairs).
64
64
  */
65
65
  export function buildProxyCanonicalString(path, params) {
66
66
  const sorted = { ...params };
@@ -200,4 +200,3 @@ export async function handleProxyRequest(request, options, onVerified) {
200
200
  }
201
201
  return onVerified({ installation: verified.installation, params: verified.params });
202
202
  }
203
- //# sourceMappingURL=proxy.js.map
package/dist/react.d.ts CHANGED
@@ -72,7 +72,15 @@ export interface QueekProviderProps {
72
72
  */
73
73
  expectSource?: unknown;
74
74
  }
75
+ /**
76
+ * Owns the one bridge subscription for the embedded merchant page. Wrap the
77
+ * app root in it, then call `useQueek()` anywhere below.
78
+ *
79
+ * @example
80
+ * <QueekProvider dashboardOrigin="https://dashboard.usequeek.com" capabilities={["toast"]}>
81
+ * <App />
82
+ * </QueekProvider>
83
+ */
75
84
  export declare function QueekProvider(props: QueekProviderProps): ReactNode;
76
85
  /** Bridge actions + live theme over the framework-free core. */
77
86
  export declare function useQueek(): QueekApi;
78
- //# sourceMappingURL=react.d.ts.map
package/dist/react.js CHANGED
@@ -33,6 +33,15 @@ function resolveConfig(props, win) {
33
33
  expectSource: props.expectSource !== undefined ? props.expectSource : (win?.parent ?? undefined),
34
34
  };
35
35
  }
36
+ /**
37
+ * Owns the one bridge subscription for the embedded merchant page. Wrap the
38
+ * app root in it, then call `useQueek()` anywhere below.
39
+ *
40
+ * @example
41
+ * <QueekProvider dashboardOrigin="https://dashboard.usequeek.com" capabilities={["toast"]}>
42
+ * <App />
43
+ * </QueekProvider>
44
+ */
36
45
  export function QueekProvider(props) {
37
46
  const { dashboardOrigin, children, sdkVersion } = props;
38
47
  const win = useMemo(() => globalWindow(), []);
@@ -224,7 +233,7 @@ export function QueekProvider(props) {
224
233
  function useBridge() {
225
234
  const bridge = useContext(QueekBridgeContext);
226
235
  if (bridge === null) {
227
- throw new Error("useQueek must be used inside <QueekProvider>");
236
+ throw new Error("useQueek must be called inside <QueekProvider>. Wrap the app root in <QueekProvider>.");
228
237
  }
229
238
  return bridge;
230
239
  }
@@ -259,4 +268,3 @@ export function useQueek() {
259
268
  const pickResource = useCallback((request) => bridge.requestPick(request), [bridge]);
260
269
  return useMemo(() => ({ toast, saveBar, title, navigate, pickResource, theme: { mode } }), [toast, saveBar, title, navigate, pickResource, mode]);
261
270
  }
262
- //# sourceMappingURL=react.js.map
package/dist/resync.d.ts CHANGED
@@ -76,5 +76,15 @@ export interface ResyncListItem {
76
76
  scopes: string[];
77
77
  status: string;
78
78
  }
79
+ /**
80
+ * Recover connectivity after a database loss or a long outage: list the
81
+ * app's active installations and ask Queek to redeliver each one's secrets
82
+ * and settings over the signed install channel. A refusal for one
83
+ * installation never aborts the run; read `cooldownSkipped`,
84
+ * `pendingSkipped` and `purged` on the result.
85
+ *
86
+ * @example
87
+ * const result = await resyncFromQueek({ apiBase: "https://api.usequeek.com", tokens, store });
88
+ * console.log(result.resyncRequested.length, "installations asked to redeliver");
89
+ */
79
90
  export declare function resyncFromQueek(options: ResyncOptions): Promise<ResyncResult>;
80
- //# sourceMappingURL=resync.d.ts.map
package/dist/resync.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isInstallationGone, isInstallationPending, isResyncCooldown, RETRY_JITTER_MAX_MS, } from "./app-auth.js";
2
- import { QueekApiError, queekApiErrorFromResponse } from "./client.js";
2
+ import { DEFAULT_REQUEST_TIMEOUT_MS, QueekApiError, queekApiErrorFromResponse, requestSignal, transportError, tryParseJson, } from "./client.js";
3
3
  import { createLogger } from "./logger.js";
4
4
  import { resolveAppApiBase } from "./tokens.js";
5
5
  const MAX_LIST_ATTEMPTS = 3;
@@ -12,6 +12,17 @@ const RESYNC_BACKOFF_MAX_MS = 5_000;
12
12
  function defaultSleep(ms) {
13
13
  return new Promise((resolve) => setTimeout(resolve, ms));
14
14
  }
15
+ /**
16
+ * Recover connectivity after a database loss or a long outage: list the
17
+ * app's active installations and ask Queek to redeliver each one's secrets
18
+ * and settings over the signed install channel. A refusal for one
19
+ * installation never aborts the run; read `cooldownSkipped`,
20
+ * `pendingSkipped` and `purged` on the result.
21
+ *
22
+ * @example
23
+ * const result = await resyncFromQueek({ apiBase: "https://api.usequeek.com", tokens, store });
24
+ * console.log(result.resyncRequested.length, "installations asked to redeliver");
25
+ */
15
26
  export async function resyncFromQueek(options) {
16
27
  const fetchImpl = options.fetchImpl ?? fetch;
17
28
  const userAgent = options.userAgent ?? "queek-app/1.0";
@@ -29,14 +40,14 @@ export async function resyncFromQueek(options) {
29
40
  headers.set("User-Agent", userAgent);
30
41
  let response;
31
42
  try {
32
- response = await fetchImpl(`${appBase}${path}`, { method: init.method, headers });
43
+ response = await fetchImpl(`${appBase}${path}`, {
44
+ method: init.method,
45
+ headers,
46
+ signal: requestSignal(undefined, DEFAULT_REQUEST_TIMEOUT_MS),
47
+ });
33
48
  }
34
49
  catch (cause) {
35
- throw new QueekApiError({
36
- status: 0,
37
- code: "network_error",
38
- message: cause instanceof Error ? `Could not reach Queek: ${cause.message}` : "Could not reach Queek.",
39
- });
50
+ throw transportError(cause);
40
51
  }
41
52
  return response;
42
53
  }
@@ -192,12 +203,3 @@ export async function resyncFromQueek(options) {
192
203
  }
193
204
  return result;
194
205
  }
195
- function tryParseJson(text) {
196
- try {
197
- return JSON.parse(text);
198
- }
199
- catch {
200
- return { message: text.slice(0, 500) };
201
- }
202
- }
203
- //# sourceMappingURL=resync.js.map
package/dist/scopes.d.ts CHANGED
@@ -166,5 +166,18 @@ export interface InstallationScopesClient {
166
166
  requestScopes(scopes: string[], options?: RequestScopesLinkOptions): string;
167
167
  revokeScopes(scopes: string[], options?: RevokeScopesOptions): Promise<string[]>;
168
168
  }
169
+ /**
170
+ * Build the optional-scopes session for one installation: read the cached
171
+ * grant, build the dashboard consent link, and revoke optional scopes.
172
+ *
173
+ * @example
174
+ * const scopes = createInstallationScopesClient({
175
+ * installationId,
176
+ * store,
177
+ * signJwt: () => tokens.signJwt(),
178
+ * appSlug: "my-app",
179
+ * optionalScopes: ["merchant-orders-read"],
180
+ * });
181
+ * const { granted } = await scopes.queryScopes();
182
+ */
169
183
  export declare function createInstallationScopesClient(options: InstallationScopesClientOptions): InstallationScopesClient;
170
- //# sourceMappingURL=scopes.d.ts.map
package/dist/scopes.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isInstallationGone, UnknownInstallationError } from "./app-auth.js";
2
- import { newIdempotencyKey, QueekApiError, queekApiErrorFromResponse } from "./client.js";
2
+ import { DEFAULT_REQUEST_TIMEOUT_MS, newIdempotencyKey, QueekApiError, queekApiErrorFromResponse, requestSignal, transportError, tryParseJson, } from "./client.js";
3
3
  import { isAllowedOpenTarget } from "./frame.js";
4
4
  import { installationScopesEqual } from "./install-handlers.js";
5
5
  import { resolveAppApiBase } from "./tokens.js";
@@ -161,6 +161,20 @@ export function buildScopeRequestLink(options) {
161
161
  }
162
162
  return link;
163
163
  }
164
+ /**
165
+ * Build the optional-scopes session for one installation: read the cached
166
+ * grant, build the dashboard consent link, and revoke optional scopes.
167
+ *
168
+ * @example
169
+ * const scopes = createInstallationScopesClient({
170
+ * installationId,
171
+ * store,
172
+ * signJwt: () => tokens.signJwt(),
173
+ * appSlug: "my-app",
174
+ * optionalScopes: ["merchant-orders-read"],
175
+ * });
176
+ * const { granted } = await scopes.queryScopes();
177
+ */
164
178
  export function createInstallationScopesClient(options) {
165
179
  if (!options.installationId || options.installationId.trim() === "") {
166
180
  throw new InvalidScopesError("An installation id is required.");
@@ -239,15 +253,11 @@ export function createInstallationScopesClient(options) {
239
253
  method: "POST",
240
254
  headers,
241
255
  body: JSON.stringify({ scopes: requested }),
242
- signal: revokeOptions.signal,
256
+ signal: requestSignal(revokeOptions.signal, DEFAULT_REQUEST_TIMEOUT_MS),
243
257
  });
244
258
  }
245
259
  catch (cause) {
246
- throw new QueekApiError({
247
- status: 0,
248
- code: "network_error",
249
- message: cause instanceof Error ? `Could not reach Queek: ${cause.message}` : "Could not reach Queek.",
250
- });
260
+ throw transportError(cause, revokeOptions.signal);
251
261
  }
252
262
  const text = await response.text();
253
263
  const parsed = text === "" ? null : tryParseJson(text);
@@ -278,16 +288,7 @@ function parseRevokeBody(body) {
278
288
  const record = (typeof body === "object" && body !== null ? body : {});
279
289
  const scopes = record.scopes;
280
290
  if (!Array.isArray(scopes) || !scopes.every((scope) => typeof scope === "string")) {
281
- throw new Error("Malformed revoke response: expected {scopes: string[]}.");
291
+ throw new Error("Malformed revoke response: expected {scopes: string[]}. The grant was not refreshed; call queryScopes() after Queek delivers the scopes update, or retry.");
282
292
  }
283
293
  return [...scopes];
284
294
  }
285
- function tryParseJson(text) {
286
- try {
287
- return JSON.parse(text);
288
- }
289
- catch {
290
- return { message: text.slice(0, 500) };
291
- }
292
- }
293
- //# sourceMappingURL=scopes.js.map
package/dist/server.d.ts CHANGED
@@ -6,4 +6,3 @@
6
6
  */
7
7
  export type { SessionTokenBinding, SessionTokenClaims, SessionTokenFailure, VerifySessionTokenOptions, } from "./session.js";
8
8
  export { EMBED_SECRET_PREFIX, SESSION_CLOCK_TOLERANCE_SECONDS, sessionTokenInstallationId, verifySessionToken, verifySessionTokenDetailed, } from "./session.js";
9
- //# sourceMappingURL=server.d.ts.map
package/dist/server.js CHANGED
@@ -5,4 +5,3 @@
5
5
  * the main entry.
6
6
  */
7
7
  export { EMBED_SECRET_PREFIX, SESSION_CLOCK_TOLERANCE_SECONDS, sessionTokenInstallationId, verifySessionToken, verifySessionTokenDetailed, } from "./session.js";
8
- //# sourceMappingURL=server.js.map
package/dist/session.d.ts CHANGED
@@ -58,7 +58,27 @@ export interface VerifySessionTokenOptions {
58
58
  expected: SessionTokenBinding;
59
59
  clockToleranceSeconds?: number;
60
60
  }
61
+ /**
62
+ * True when `token` is a valid dashboard session token for the installation
63
+ * in `options.expected`. See {@link verifySessionTokenDetailed} for the
64
+ * reason a token was refused.
65
+ */
61
66
  export declare function verifySessionToken(token: string, options: VerifySessionTokenOptions): Promise<boolean>;
67
+ /**
68
+ * Verify a dashboard session token: algorithm pin, signature, audience,
69
+ * issuer, expiry (20 s clock tolerance), then the installation binding.
70
+ * Returns the claims on success and a stable `reason` on failure. Answer 401
71
+ * for every failure.
72
+ *
73
+ * @example
74
+ * const checked = await verifySessionTokenDetailed(token, {
75
+ * secret: installation.embedSecret ?? "",
76
+ * audience: "my-app",
77
+ * issuer: installation.apiBase,
78
+ * expected: { installationId, vendorId, appSlug: "my-app", appId },
79
+ * });
80
+ * if (!checked.ok) return new Response(checked.reason, { status: 401 });
81
+ */
62
82
  export declare function verifySessionTokenDetailed(token: string, options: VerifySessionTokenOptions): Promise<{
63
83
  ok: true;
64
84
  claims: SessionTokenClaims;
@@ -74,4 +94,3 @@ export declare function verifySessionTokenDetailed(token: string, options: Verif
74
94
  * Null for anything that is not a decodable JWT carrying the claim.
75
95
  */
76
96
  export declare function sessionTokenInstallationId(token: string): string | null;
77
- //# sourceMappingURL=session.d.ts.map
package/dist/session.js CHANGED
@@ -22,10 +22,30 @@ export const EMBED_SECRET_PREFIX = "embsec_";
22
22
  export const SESSION_TOKEN_ALG = "HS256";
23
23
  /** Clock skew the verifier tolerates, in seconds (matches the mint side). */
24
24
  export const SESSION_CLOCK_TOLERANCE_SECONDS = 20;
25
+ /**
26
+ * True when `token` is a valid dashboard session token for the installation
27
+ * in `options.expected`. See {@link verifySessionTokenDetailed} for the
28
+ * reason a token was refused.
29
+ */
25
30
  export async function verifySessionToken(token, options) {
26
31
  const result = await verifySessionTokenDetailed(token, options);
27
32
  return result.ok;
28
33
  }
34
+ /**
35
+ * Verify a dashboard session token: algorithm pin, signature, audience,
36
+ * issuer, expiry (20 s clock tolerance), then the installation binding.
37
+ * Returns the claims on success and a stable `reason` on failure. Answer 401
38
+ * for every failure.
39
+ *
40
+ * @example
41
+ * const checked = await verifySessionTokenDetailed(token, {
42
+ * secret: installation.embedSecret ?? "",
43
+ * audience: "my-app",
44
+ * issuer: installation.apiBase,
45
+ * expected: { installationId, vendorId, appSlug: "my-app", appId },
46
+ * });
47
+ * if (!checked.ok) return new Response(checked.reason, { status: 401 });
48
+ */
29
49
  export async function verifySessionTokenDetailed(token, options) {
30
50
  const envelope = await verifyTokenEnvelope(token, options);
31
51
  if (!envelope.ok)
@@ -169,4 +189,3 @@ function classifyJoseError(error) {
169
189
  }
170
190
  return "invalid_signature";
171
191
  }
172
- //# sourceMappingURL=session.js.map
@@ -41,16 +41,41 @@ export interface VerifyOptions {
41
41
  /** Skip the freshness check (NOT recommended; exposed for replaying stored fixtures). */
42
42
  skipFreshnessCheck?: boolean;
43
43
  }
44
+ /**
45
+ * True when `timestamp` is a plain unix-seconds integer within `maxSkewSeconds`
46
+ * of `nowSeconds`, in either direction. Anything else (`""`, `"1e3"`,
47
+ * `"0x10"`, `"12.5"`) is stale: the signed header must be what Queek sent.
48
+ */
49
+ export declare function isFreshTimestamp(timestamp: string, nowSeconds: number, maxSkewSeconds: number): boolean;
50
+ /** The decoded MAC key for a `whsec_` secret (a secret that is not valid base64 is used as raw text). */
44
51
  export declare function secretKeyBytes(secret: string): Buffer;
45
52
  /** One `v1,<base64>` signature for one secret, in the format Queek sends. */
46
53
  export declare function signQueekPayload(eventId: string, timestamp: number | string, body: string, secret: string): string;
47
54
  /** Failure reason when verification rejects a delivery. */
48
55
  export type SignatureFailure = "missing_headers" | "stale_timestamp" | "signature_mismatch";
56
+ /**
57
+ * True when one of the signatures in the header matches the body, and the
58
+ * timestamp is within the skew window. The compare is constant-time and
59
+ * covers every signature in the header (a secret rotation sends two).
60
+ *
61
+ * @example
62
+ * const ok = verifyQueekSignature({
63
+ * id: request.headers.get("webhook-id") ?? "",
64
+ * timestamp: request.headers.get("webhook-timestamp") ?? "",
65
+ * signatureHeader: request.headers.get("webhook-signature") ?? "",
66
+ * body: await request.text(), // the raw text, before any JSON.parse
67
+ * secret: installation.webhookSecret,
68
+ * });
69
+ */
49
70
  export declare function verifyQueekSignature(input: SignatureInput, options?: VerifyOptions): boolean;
71
+ /**
72
+ * Like {@link verifyQueekSignature}, but says why a delivery was rejected:
73
+ * `missing_headers`, `stale_timestamp` or `signature_mismatch`. Log the
74
+ * reason; answer 401 either way.
75
+ */
50
76
  export declare function verifyQueekSignatureDetailed(input: SignatureInput, options?: VerifyOptions): {
51
77
  ok: true;
52
78
  } | {
53
79
  ok: false;
54
80
  reason: SignatureFailure;
55
81
  };
56
- //# sourceMappingURL=signatures.d.ts.map
@@ -22,6 +22,15 @@ export const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
22
22
  export const SECRET_PREFIX = "whsec_";
23
23
  /** Maximum age of a delivery before it is rejected as stale (5 minutes, the Standard Webhooks default). */
24
24
  export const MAX_TIMESTAMP_SKEW_SECONDS = 5 * 60;
25
+ /**
26
+ * True when `timestamp` is a plain unix-seconds integer within `maxSkewSeconds`
27
+ * of `nowSeconds`, in either direction. Anything else (`""`, `"1e3"`,
28
+ * `"0x10"`, `"12.5"`) is stale: the signed header must be what Queek sent.
29
+ */
30
+ export function isFreshTimestamp(timestamp, nowSeconds, maxSkewSeconds) {
31
+ return /^\d{1,15}$/.test(timestamp) && Math.abs(nowSeconds - Number(timestamp)) <= maxSkewSeconds;
32
+ }
33
+ /** The decoded MAC key for a `whsec_` secret (a secret that is not valid base64 is used as raw text). */
25
34
  export function secretKeyBytes(secret) {
26
35
  const encoded = secret.startsWith(SECRET_PREFIX) ? secret.slice(SECRET_PREFIX.length) : secret;
27
36
  try {
@@ -44,19 +53,37 @@ export function signQueekPayload(eventId, timestamp, body, secret) {
44
53
  .digest();
45
54
  return `v1,${mac.toString("base64")}`;
46
55
  }
56
+ /**
57
+ * True when one of the signatures in the header matches the body, and the
58
+ * timestamp is within the skew window. The compare is constant-time and
59
+ * covers every signature in the header (a secret rotation sends two).
60
+ *
61
+ * @example
62
+ * const ok = verifyQueekSignature({
63
+ * id: request.headers.get("webhook-id") ?? "",
64
+ * timestamp: request.headers.get("webhook-timestamp") ?? "",
65
+ * signatureHeader: request.headers.get("webhook-signature") ?? "",
66
+ * body: await request.text(), // the raw text, before any JSON.parse
67
+ * secret: installation.webhookSecret,
68
+ * });
69
+ */
47
70
  export function verifyQueekSignature(input, options = {}) {
48
71
  return verifyQueekSignatureDetailed(input, options).ok;
49
72
  }
73
+ /**
74
+ * Like {@link verifyQueekSignature}, but says why a delivery was rejected:
75
+ * `missing_headers`, `stale_timestamp` or `signature_mismatch`. Log the
76
+ * reason; answer 401 either way.
77
+ */
50
78
  export function verifyQueekSignatureDetailed(input, options = {}) {
51
79
  const { id, timestamp, body, signatureHeader, secret } = input;
52
80
  if (!id || !timestamp || !signatureHeader || !secret) {
53
81
  return { ok: false, reason: "missing_headers" };
54
82
  }
55
83
  if (!options.skipFreshnessCheck) {
56
- const ts = Number(timestamp);
57
84
  const now = options.nowSeconds ?? Math.floor(Date.now() / 1000);
58
85
  const maxSkew = options.maxSkewSeconds ?? MAX_TIMESTAMP_SKEW_SECONDS;
59
- if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkew) {
86
+ if (!isFreshTimestamp(timestamp, now, maxSkew)) {
60
87
  return { ok: false, reason: "stale_timestamp" };
61
88
  }
62
89
  }
@@ -72,4 +99,3 @@ export function verifyQueekSignatureDetailed(input, options = {}) {
72
99
  }
73
100
  return { ok: false, reason: "signature_mismatch" };
74
101
  }
75
- //# sourceMappingURL=signatures.js.map
package/dist/store.d.ts CHANGED
@@ -74,6 +74,12 @@ export interface WebhookSecretCandidate {
74
74
  installationId: string;
75
75
  secret: string;
76
76
  }
77
+ /**
78
+ * Where installations live. The SDK ships `SqliteInstallationStore` (local
79
+ * and test) and `PostgresInstallationStore` (production); both encrypt
80
+ * secrets at rest. Implement this interface to use another database, and
81
+ * keep `claimWebhookId` atomic: handlers rely on it to run each delivery once.
82
+ */
77
83
  export interface InstallationStore {
78
84
  saveInstallation(record: InstallationRecord): Promise<void> | void;
79
85
  getInstallation(installationId: string): Promise<InstallationRecord | null> | InstallationRecord | null;
@@ -303,4 +309,3 @@ export interface InstallationStoreSelector {
303
309
  * container-local database).
304
310
  */
305
311
  export declare function createInstallationStore(options: InstallationStoreSelector): InstallationStore;
306
- //# sourceMappingURL=store.d.ts.map
package/dist/store.js CHANGED
@@ -714,4 +714,3 @@ export function createInstallationStore(options) {
714
714
  storeKey: options.storeKey,
715
715
  });
716
716
  }
717
- //# sourceMappingURL=store.js.map
package/dist/theme.d.ts CHANGED
@@ -97,4 +97,3 @@ export interface ThemeListenerOptions {
97
97
  * Returns an unsubscribe function.
98
98
  */
99
99
  export declare function installThemeListener(options: ThemeListenerOptions): () => void;
100
- //# sourceMappingURL=theme.d.ts.map
package/dist/theme.js CHANGED
@@ -159,4 +159,3 @@ export function installThemeListener(options) {
159
159
  },
160
160
  });
161
161
  }
162
- //# sourceMappingURL=theme.js.map
package/dist/tokens.d.ts CHANGED
@@ -75,7 +75,14 @@ export interface TokenProviderOptions {
75
75
  * (`<origin>/api/v1/apps`). Throws `InvalidApiBaseError` before any fetch.
76
76
  */
77
77
  export declare function resolveAppApiBase(raw: string, allowedApiHosts?: string[]): string;
78
+ /** The app-API path that mints a token for one installation. */
78
79
  export declare function mintPath(installationId: string): string;
80
+ /**
81
+ * Mints, caches and refreshes installation tokens for one app. Tokens are
82
+ * minted with the app's RSA key, cached encrypted in the installation store,
83
+ * and shared by concurrent callers in one process. Build one with
84
+ * {@link createAppTokenProvider} and pass it to `createInstallationClient`.
85
+ */
79
86
  export declare class AppTokenProvider implements AppTokens {
80
87
  private readonly credential;
81
88
  private readonly store;
@@ -153,6 +160,8 @@ export interface InstallationClientOptions {
153
160
  userAgent?: string;
154
161
  /** Extra allowed `apiBase` hosts (test/local API hosts). */
155
162
  allowedApiHosts?: string[];
163
+ /** Time one attempt may take, in ms (default 30000). `0` disables the client timeout. */
164
+ timeoutMs?: number;
156
165
  }
157
166
  /**
158
167
  * A `QueekClient` whose every call goes through `acquireToken()`: the
@@ -174,4 +183,3 @@ export interface InstallationClientOptions {
174
183
  * does NOT auto-update (re-run codegen in the app; sunset signal at 1.0).
175
184
  */
176
185
  export declare function createInstallationClient<TPaths = MerchantPaths>(options: InstallationClientOptions): QueekClient<ResolvedPaths<TPaths>>;
177
- //# sourceMappingURL=tokens.d.ts.map