@usequeek/app-sdk 0.6.1 → 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 +339 -330
  3. package/dist/app-auth.d.ts +15 -17
  4. package/dist/app-auth.js +20 -23
  5. package/dist/auth-fetch.d.ts +14 -5
  6. package/dist/auth-fetch.js +15 -6
  7. package/dist/background.d.ts +10 -2
  8. package/dist/background.js +9 -1
  9. package/dist/browser.d.ts +2 -3
  10. package/dist/browser.js +2 -3
  11. package/dist/client.d.ts +138 -16
  12. package/dist/client.js +200 -43
  13. package/dist/collected.d.ts +13 -15
  14. package/dist/collected.js +4 -5
  15. package/dist/crypto.d.ts +11 -1
  16. package/dist/crypto.js +25 -10
  17. package/dist/delivery.d.ts +22 -9
  18. package/dist/delivery.js +26 -21
  19. package/dist/frame.d.ts +11 -12
  20. package/dist/frame.js +11 -12
  21. package/dist/handoff.d.ts +35 -39
  22. package/dist/handoff.js +9 -10
  23. package/dist/hono.d.ts +3 -4
  24. package/dist/hono.js +3 -4
  25. package/dist/index.d.ts +1 -2
  26. package/dist/index.js +1 -2
  27. package/dist/install-handlers.d.ts +28 -9
  28. package/dist/install-handlers.js +48 -18
  29. package/dist/logger.d.ts +7 -1
  30. package/dist/logger.js +8 -2
  31. package/dist/merchant-schema.d.ts +15678 -6236
  32. package/dist/merchant-schema.js +0 -1
  33. package/dist/proxy.d.ts +21 -24
  34. package/dist/proxy.js +20 -22
  35. package/dist/react.d.ts +10 -2
  36. package/dist/react.js +11 -3
  37. package/dist/resync.d.ts +15 -5
  38. package/dist/resync.js +22 -20
  39. package/dist/scopes.d.ts +36 -27
  40. package/dist/scopes.js +37 -40
  41. package/dist/server.d.ts +0 -1
  42. package/dist/server.js +0 -1
  43. package/dist/session.d.ts +35 -17
  44. package/dist/session.js +32 -15
  45. package/dist/signatures.d.ts +31 -5
  46. package/dist/signatures.js +36 -9
  47. package/dist/store.d.ts +26 -26
  48. package/dist/store.js +16 -20
  49. package/dist/theme.d.ts +1 -2
  50. package/dist/theme.js +1 -2
  51. package/dist/tokens.d.ts +18 -10
  52. package/dist/tokens.js +32 -57
  53. package/dist/webhooks.d.ts +30 -6
  54. package/dist/webhooks.js +27 -10
  55. package/package.json +21 -4
  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
@@ -1,22 +1,22 @@
1
1
  import { type InstallationRecord, type InstallationStore } from "./store.js";
2
2
  /**
3
3
  * Storefront app-proxy query verification, byte-for-byte compatible with
4
- * Queek's `App\Services\Apps\AppProxyService` (queek_backend).
4
+ * the way Queek signs proxy reads.
5
5
  *
6
6
  * Queek signs a READ fetch to the app's proxy handler with the installation's
7
- * `proxy_secret` (`signQuery`): the app verifies server-side before answering
8
- * the shopper. Canonical signature (Shopify-style, per-install secret):
7
+ * `proxy_secret`: the app verifies server-side before answering the
8
+ * shopper. Canonical signature (per-installation secret):
9
9
  *
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
  *
17
- * `path` is the QUEEK-side canonical path (`/apps/<subpath>/<rest>`, what
18
- * the backend signs in `fetch()`), NOT the app's local route: pass it
19
- * explicitly per route (see `handleProxyRequest`).
17
+ * `path` is the QUEEK-side canonical path (`/apps/<subpath>/<rest>`, the
18
+ * path Queek signs), NOT the app's local route: pass it explicitly per
19
+ * route (see `handleProxyRequest`).
20
20
  *
21
21
  * Layering mirrors the install/webhook handlers: layer 1
22
22
  * `verifyProxyDelivery` takes plain data (path + query) and returns a plain
@@ -30,19 +30,19 @@ export declare const PROXY_NONCE_PARAM = "jti";
30
30
  export declare const PROXY_SHOP_PARAM = "shop";
31
31
  export declare const PROXY_CUSTOMER_PARAM = "logged_in_customer_id";
32
32
  export declare const PROXY_KID_PARAM = "kid";
33
- /** Backend `apps.proxy_signature_skew_seconds` default; the floor (60s) below applies regardless. */
33
+ /** Default allowed clock skew in seconds; the floor (60s) below applies regardless. */
34
34
  export declare const PROXY_MAX_SKEW_SECONDS = 300;
35
35
  /** Query params as any framework holds them (duplicates collapse to the first value). */
36
36
  export type ProxyQuery = Record<string, string | string[] | null | undefined>;
37
37
  /** Failure reason when proxy verification rejects a query. */
38
38
  export type ProxyFailure = "missing_params" | "stale_timestamp" | "missing_secret" | "signature_mismatch" | "unknown_installation" | "replayed";
39
39
  /**
40
- * The canonical string the MAC covers — mirrors `AppProxyService::signature()`
41
- * exactly (sig excluded, keys byte-sorted, rawurlencoded pairs).
40
+ * The canonical string the MAC covers, exactly as Queek builds it (sig
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
  /**
45
- * One hex signature for one secret — mirrors `AppProxyService::signature()`.
45
+ * One hex signature for one secret, computed as Queek computes it.
46
46
  * Exported so tests (and dev harnesses) can recompute a signature
47
47
  * independently of the verifier; production traffic is signed by Queek.
48
48
  */
@@ -50,15 +50,14 @@ export declare function signProxyQuery(path: string, params: Record<string, stri
50
50
  export interface ProxyVerifyOptions {
51
51
  /** Unix seconds to judge freshness against (defaults to now). Exposed for tests. */
52
52
  nowSeconds?: number;
53
- /** Allowed clock skew in seconds (defaults to PROXY_MAX_SKEW_SECONDS; floored at 60 like the backend). */
53
+ /** Allowed clock skew in seconds (defaults to PROXY_MAX_SKEW_SECONDS; floored at 60). */
54
54
  maxSkewSeconds?: number;
55
55
  }
56
56
  /**
57
- * Pure verification over explicitly passed secrets — mirrors
58
- * `AppProxyService::verifyQuery()` (structure → skew → secret presence →
59
- * MAC over every active secret). No replay enforcement here: single-use
60
- * `jti` needs storage, so the store-backed `verifyProxyDelivery` claims it
61
- * (the framework-free translation of the backend's atomic `Cache::add`).
57
+ * Pure verification over explicitly passed secrets (structure → skew →
58
+ * secret presence → MAC over every active secret). No replay enforcement
59
+ * here: single-use `jti` needs storage, so the store-backed
60
+ * `verifyProxyDelivery` claims it atomically.
62
61
  */
63
62
  export declare function verifyProxyQueryDetailed(path: string, query: ProxyQuery, secrets: string[], options?: ProxyVerifyOptions): {
64
63
  ok: true;
@@ -72,8 +71,7 @@ export interface ProxyStoreOptions extends ProxyVerifyOptions {
72
71
  store: InstallationStore;
73
72
  /**
74
73
  * Skip the single-use `jti` claim (NOT recommended; exposed for tests and
75
- * for apps that enforce replay themselves). Defaults to enforcing, like
76
- * the backend.
74
+ * for apps that enforce replay themselves). Defaults to enforcing.
77
75
  */
78
76
  enforceReplay?: boolean;
79
77
  }
@@ -101,7 +99,7 @@ export declare function verifyProxyDelivery(input: {
101
99
  }, options: ProxyStoreOptions): Promise<ProxyVerification>;
102
100
  export interface ProxyRequestOptions extends ProxyStoreOptions {
103
101
  /**
104
- * The Queek-side canonical path the backend signed
102
+ * The Queek-side canonical path Queek signed
105
103
  * (`/apps/<subpath>/<rest>`, e.g. `/apps/booking/availability`) — the
106
104
  * app's local route is NOT it. Pass one per route.
107
105
  */
@@ -114,9 +112,8 @@ export type ProxyResponder = (verified: {
114
112
  }) => Response | Promise<Response>;
115
113
  /**
116
114
  * Layer 2: the Web-standard wrapper, built ONLY on layer 1 — reads the query
117
- * off the request URL, calls the core, builds the `Response`. Phase 1 is
118
- * read-only by binding rule (the backend only ever sends GET), so only
119
- * `GET` is served; anything else answers 405.
115
+ * off the request URL, calls the core, builds the `Response`. Proxy reads
116
+ * are read-only (Queek only ever sends GET), so only `GET` is served;
117
+ * anything else answers 405.
120
118
  */
121
119
  export declare function handleProxyRequest(request: Request, options: ProxyRequestOptions, onVerified: ProxyResponder): Promise<Response>;
122
- //# sourceMappingURL=proxy.d.ts.map
package/dist/proxy.js CHANGED
@@ -3,22 +3,22 @@ import { toResponse } from "./delivery.js";
3
3
  import { normalisePid } from "./store.js";
4
4
  /**
5
5
  * Storefront app-proxy query verification, byte-for-byte compatible with
6
- * Queek's `App\Services\Apps\AppProxyService` (queek_backend).
6
+ * the way Queek signs proxy reads.
7
7
  *
8
8
  * Queek signs a READ fetch to the app's proxy handler with the installation's
9
- * `proxy_secret` (`signQuery`): the app verifies server-side before answering
10
- * the shopper. Canonical signature (Shopify-style, per-install secret):
9
+ * `proxy_secret`: the app verifies server-side before answering the
10
+ * shopper. Canonical signature (per-installation secret):
11
11
  *
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
  *
19
- * `path` is the QUEEK-side canonical path (`/apps/<subpath>/<rest>`, what
20
- * the backend signs in `fetch()`), NOT the app's local route: pass it
21
- * explicitly per route (see `handleProxyRequest`).
19
+ * `path` is the QUEEK-side canonical path (`/apps/<subpath>/<rest>`, the
20
+ * path Queek signs), NOT the app's local route: pass it explicitly per
21
+ * route (see `handleProxyRequest`).
22
22
  *
23
23
  * Layering mirrors the install/webhook handlers: layer 1
24
24
  * `verifyProxyDelivery` takes plain data (path + query) and returns a plain
@@ -32,15 +32,15 @@ export const PROXY_NONCE_PARAM = "jti";
32
32
  export const PROXY_SHOP_PARAM = "shop";
33
33
  export const PROXY_CUSTOMER_PARAM = "logged_in_customer_id";
34
34
  export const PROXY_KID_PARAM = "kid";
35
- /** Backend `apps.proxy_signature_skew_seconds` default; the floor (60s) below applies regardless. */
35
+ /** Default allowed clock skew in seconds; the floor (60s) below applies regardless. */
36
36
  export const PROXY_MAX_SKEW_SECONDS = 300;
37
- /** PHP `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
- /** PHP `strval` for scalar query values: 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;
@@ -59,8 +59,8 @@ function normalizeQuery(query) {
59
59
  return out;
60
60
  }
61
61
  /**
62
- * The canonical string the MAC covers — mirrors `AppProxyService::signature()`
63
- * exactly (sig excluded, keys byte-sorted, rawurlencoded pairs).
62
+ * The canonical string the MAC covers, exactly as Queek builds it (sig
63
+ * excluded, keys byte-sorted, percent-encoded pairs).
64
64
  */
65
65
  export function buildProxyCanonicalString(path, params) {
66
66
  const sorted = { ...params };
@@ -70,7 +70,7 @@ export function buildProxyCanonicalString(path, params) {
70
70
  return `${path}\n${sorted[PROXY_SHOP_PARAM] ?? ""}\n${sorted[PROXY_TIMESTAMP_PARAM] ?? ""}\n${pairs.join("&")}`;
71
71
  }
72
72
  /**
73
- * One hex signature for one secret — mirrors `AppProxyService::signature()`.
73
+ * One hex signature for one secret, computed as Queek computes it.
74
74
  * Exported so tests (and dev harnesses) can recompute a signature
75
75
  * independently of the verifier; production traffic is signed by Queek.
76
76
  */
@@ -98,11 +98,10 @@ function proxyMacMatches(path, params, sig, secret) {
98
98
  return candidateBuf.length === expectedBuf.length && timingSafeEqual(candidateBuf, expectedBuf);
99
99
  }
100
100
  /**
101
- * Pure verification over explicitly passed secrets — mirrors
102
- * `AppProxyService::verifyQuery()` (structure → skew → secret presence →
103
- * MAC over every active secret). No replay enforcement here: single-use
104
- * `jti` needs storage, so the store-backed `verifyProxyDelivery` claims it
105
- * (the framework-free translation of the backend's atomic `Cache::add`).
101
+ * Pure verification over explicitly passed secrets (structure → skew →
102
+ * secret presence → MAC over every active secret). No replay enforcement
103
+ * here: single-use `jti` needs storage, so the store-backed
104
+ * `verifyProxyDelivery` claims it atomically.
106
105
  */
107
106
  export function verifyProxyQueryDetailed(path, query, secrets, options = {}) {
108
107
  const params = normalizeQuery(query);
@@ -181,9 +180,9 @@ export async function verifyProxyDelivery(input, options) {
181
180
  }
182
181
  /**
183
182
  * Layer 2: the Web-standard wrapper, built ONLY on layer 1 — reads the query
184
- * off the request URL, calls the core, builds the `Response`. Phase 1 is
185
- * read-only by binding rule (the backend only ever sends GET), so only
186
- * `GET` is served; anything else answers 405.
183
+ * off the request URL, calls the core, builds the `Response`. Proxy reads
184
+ * are read-only (Queek only ever sends GET), so only `GET` is served;
185
+ * anything else answers 405.
187
186
  */
188
187
  export async function handleProxyRequest(request, options, onVerified) {
189
188
  if (request.method.toUpperCase() !== "GET") {
@@ -201,4 +200,3 @@ export async function handleProxyRequest(request, options, onVerified) {
201
200
  }
202
201
  return onVerified({ installation: verified.installation, params: verified.params });
203
202
  }
204
- //# sourceMappingURL=proxy.js.map
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Optional React bindings for `@usequeek/app-sdk` (U2).
2
+ * Optional React bindings for `@usequeek/app-sdk`.
3
3
  *
4
4
  * Import from `@usequeek/app-sdk/react` (requires the optional `react`
5
5
  * peer) — a thin layer built ONLY on the framework-free core (`frame.js` /
@@ -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
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  /**
3
- * Optional React bindings for `@usequeek/app-sdk` (U2).
3
+ * Optional React bindings for `@usequeek/app-sdk`.
4
4
  *
5
5
  * Import from `@usequeek/app-sdk/react` (requires the optional `react`
6
6
  * peer) — a thin layer built ONLY on the framework-free core (`frame.js` /
@@ -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
@@ -2,7 +2,7 @@ import { type Logger } from "./logger.js";
2
2
  import type { InstallationStore } from "./store.js";
3
3
  import { type AppTokenProvider } from "./tokens.js";
4
4
  /**
5
- * Connectivity recovery (S1, SDK 0.2.0): after a database loss (or an
5
+ * Connectivity recovery: after a database loss (or an
6
6
  * outage that outlasted the webhook retries), re-list the app's
7
7
  * installations from Queek and re-handshake each one's secrets.
8
8
  *
@@ -13,7 +13,7 @@ import { type AppTokenProvider } from "./tokens.js";
13
13
  * settings arrive over the EXISTING signed install channel (same handoff
14
14
  * envelope as install), handled by the install handler — which accepts a
15
15
  * resync for an existing installation idempotently.
16
- * - Per-installation refusals (rev 8, distinguished by code): 409
16
+ * - Per-installation refusals (distinguished by code): 409
17
17
  * `app_installation_pending` retries later with backoff (bounded, then
18
18
  * skipped + recorded — NEVER purged); 429 `resync_cooldown` is the
19
19
  * rotation COOLDOWN (≤1/hour: skipped + recorded, no retry loop); any
@@ -37,14 +37,14 @@ import { type AppTokenProvider } from "./tokens.js";
37
37
  * resync cannot backfill them.
38
38
  */
39
39
  export interface ResyncOptions {
40
- /** The app's Queek backend hosting `/api/v1/apps` (normally any installation's `api_base`). */
40
+ /** The Queek API host serving `/api/v1/apps` (normally any installation's `api_base`). */
41
41
  apiBase: string;
42
42
  tokens: AppTokenProvider;
43
43
  store: InstallationStore;
44
44
  fetchImpl?: typeof fetch;
45
45
  /** Sent as User-Agent. Defaults to `queek-app/1.0`. */
46
46
  userAgent?: string;
47
- /** Extra allowed `apiBase` hosts (test/local backends). */
47
+ /** Extra allowed `apiBase` hosts (test/local API hosts). */
48
48
  allowedApiHosts?: string[];
49
49
  logger?: Logger;
50
50
  sleep?: (ms: number) => Promise<void>;
@@ -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
  }
@@ -49,7 +60,7 @@ export async function resyncFromQueek(options) {
49
60
  }
50
61
  // -- List (opaque keyset cursor, secret-free) ------------------------------
51
62
  // The cursor is never interpreted — echoed back verbatim until
52
- // `next_cursor` is null (rev 8: keyset on a stable unique key, scoped to
63
+ // `next_cursor` is null (keyset on a stable unique key, scoped to
53
64
  // the calling app; inserts/uninstalls mid-walk never skip or duplicate).
54
65
  const listed = [];
55
66
  let cursor = null;
@@ -120,7 +131,7 @@ export async function resyncFromQueek(options) {
120
131
  break;
121
132
  }
122
133
  if (response.status === 429) {
123
- // Per-app bucket (rev 8): back off on `Retry-After` (jittered) and
134
+ // Per-app bucket: back off on `Retry-After` (jittered) and
124
135
  // retry — never recorded as a cooldown skip. An exhausted budget
125
136
  // aborts the run: the whole app is throttled, so pushing on would
126
137
  // only deepen it; the caller retries later.
@@ -130,7 +141,7 @@ export async function resyncFromQueek(options) {
130
141
  continue;
131
142
  }
132
143
  if (response.status === 409 && isInstallationPending(error.code)) {
133
- // Pending installation (rev 8, mark persisted per B2 review r2):
144
+ // Pending installation:
134
145
  // retry later with backoff — NEVER purge. An exhausted budget
135
146
  // skips (recorded); the persisted mark survives restarts, so the
136
147
  // purge-absent step below (and after a restart) keeps the row.
@@ -169,7 +180,7 @@ export async function resyncFromQueek(options) {
169
180
  }
170
181
  // …and purge local rows Queek no longer lists (uninstall-while-down) —
171
182
  // EXCEPT rows carrying the persisted pending mark. The list covers
172
- // ACTIVE installations only (rev 8), so absence alone never purges a
183
+ // ACTIVE installations only, so absence alone never purges a
173
184
  // pending row — including after a process restart, because the mark
174
185
  // lives in the store column, not in memory. The row stays for a later
175
186
  // run, when it is active (listed + resynced), gone (404 → purged), or
@@ -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
@@ -1,8 +1,7 @@
1
1
  import { QueekApiError } from "./client.js";
2
2
  import type { InstallationStore } from "./store.js";
3
3
  /**
4
- * Optional scopes (plan item 6, Shopify `scopes.query / request / revoke`
5
- * parity — symbol names cited only).
4
+ * Optional scopes: query, request and revoke.
6
5
  *
7
6
  * - `queryScopes`: the cached effective grant split against the app's own
8
7
  * declared-optional list. Store-read only, never a fetch: the app-credential
@@ -14,25 +13,24 @@ import type { InstallationStore } from "./store.js";
14
13
  * consent — it hands the app a link shaped like the bridge
15
14
  * `open`/`navigate` targets (dashboard-relative path, or an absolute URL
16
15
  * under a given dashboard origin) for the app to open via `sendOpen` or
17
- * redirect to. The consent screen itself ships with the dashboard (plan
18
- * item 8), which honours this link shape.
16
+ * redirect to. Opening it requires a dashboard that supports the consent
17
+ * screen, which honours this link shape.
19
18
  * - `revokeScopes`: `POST
20
19
  * /api/v1/apps/installations/{installation}/scopes/revoke` with the app
21
- * JWT (same credential as mint/resync — never auth:sanctum, never the
22
- * installation token). Body `{scopes[]}` (1–50, backend
23
- * `RevokeAppScopesRequest`); an `Idempotency-Key` rides the write for
20
+ * JWT (same credential as mint/resync — not the installation token).
21
+ * Body `{scopes[]}` (1–50); an `Idempotency-Key` rides the write for
24
22
  * discipline (generated when the caller does not supply one — the client
25
23
  * write discipline), though the route is naturally idempotent: revoking
26
24
  * a never-granted optional scope is 200 with no change. Required scopes
27
25
  * refuse 422 `app_scope_required` (surfaced as `AppScopeRequiredError`).
28
26
  * On 200 the cached grant is refreshed from the response and the cached
29
27
  * token dropped only when the grant moved (same compare-and-clear as the
30
- * scopes_update handler — the backend rewrites live token rows in place,
31
- * so the drop is backstop, not mechanism).
28
+ * scopes_update handler — Queek rewrites live token rows in place, so
29
+ * the drop is a backstop, not the mechanism).
32
30
  *
33
31
  * Types here are hand-written: the revoke endpoint and the scopes_update
34
- * handoff live on the app-credential surface, which the frozen
35
- * `openapi/merchant.json` does not cover (types are generated app-side by
32
+ * handoff live on the app-credential surface, which the bundled
33
+ * `openapi/merchant.json` snapshot does not cover (types are generated app-side by
36
34
  * `queek app codegen`; the merchant spec file is never hand-edited).
37
35
  */
38
36
  /** 422 on the revoke endpoint: required scopes are never revocable (uninstall is the remedy). */
@@ -43,8 +41,8 @@ export declare const APP_SCOPE_UNDECLARED_CODE = "app_scope_undeclared";
43
41
  export declare function isAppScopeRequired(status: number, code: string | undefined): boolean;
44
42
  /**
45
43
  * A required-scope refusal, typed so callers switch on the class (or
46
- * `code`), never on `message`. Carries the offending scopes when the
47
- * backend named them.
44
+ * `code`), never on `message`. Carries the offending scopes when Queek
45
+ * named them.
48
46
  */
49
47
  export declare class AppScopeRequiredError extends QueekApiError {
50
48
  readonly scopes: string[];
@@ -57,13 +55,13 @@ export declare class InvalidScopesError extends Error {
57
55
  readonly code = "invalid_scopes";
58
56
  constructor(message: string);
59
57
  }
60
- /** Backend `RevokeAppScopesRequest` bounds, mirrored client-side. */
58
+ /** Bounds of the revoke request (1–50 scopes), mirrored client-side. */
61
59
  export declare const MAX_SCOPE_LIST = 50;
62
60
  export declare const MAX_SCOPE_LENGTH = 120;
63
61
  /**
64
62
  * Validate + normalise a scope list (trim, drop empties, dedupe keeping
65
- * first order). Mirrors the backend contract (`required array min:1
66
- * max:50`, string items) so a malformed call fails before any fetch.
63
+ * first order). Mirrors the revoke endpoint's rules (a required array of
64
+ * 1–50 string items) so a malformed call fails before any fetch.
67
65
  */
68
66
  export declare function normaliseScopeList(scopes: unknown): string[];
69
67
  /** The cached-grant split `queryScopes` returns. */
@@ -99,8 +97,7 @@ export interface ScopeRequestLinkOptions {
99
97
  * under `dashboardOrigin` (refused exactly like `sendOpen` refuses a bad
100
98
  * target).
101
99
  *
102
- * Link shape (the SDK↔dashboard contract the consent screen honours —
103
- * the only query route the dashboard `AppsPage` serves):
100
+ * Link shape (the contract the dashboard's consent screen honours):
104
101
  * `/apps?app={slug}&view=scopes&scopes={a},{b}` (each scope
105
102
  * URL-encoded, comma-separated). The link carries the slug + the scope
106
103
  * list only: the dashboard resolves the installation from the signed-in
@@ -132,14 +129,14 @@ export interface InstallationScopesClientOptions {
132
129
  store: InstallationStore;
133
130
  /**
134
131
  * A fresh app JWT per call — e.g. `() => provider.signJwt()`. The revoke
135
- * endpoint takes the same app credential as mint/resync (never
136
- * auth:sanctum, never the installation token).
132
+ * endpoint takes the same app credential as mint/resync (not the
133
+ * installation token).
137
134
  */
138
135
  signJwt: () => string;
139
136
  fetchImpl?: typeof fetch;
140
137
  /** Sent as User-Agent. Defaults to `queek-app/1.0`. */
141
138
  userAgent?: string;
142
- /** Extra allowed `apiBase` hosts (test/local backends). */
139
+ /** Extra allowed `apiBase` hosts (test/local API hosts). */
143
140
  allowedApiHosts?: string[];
144
141
  /** The app's dashboard slug (addresses the scope-request consent screen); per-call override wins. */
145
142
  appSlug?: string;
@@ -153,11 +150,10 @@ export interface InstallationScopesClientOptions {
153
150
  dashboardOrigin?: string;
154
151
  }
155
152
  /**
156
- * The per-installation scopes session (Shopify `scopes.query / request /
157
- * revoke` parity — symbol names cited only). A sibling of the installation
158
- * client, not a method on it: the installation client's exact type is a
159
- * committed contract (the generics compat guard), so the scopes surface
160
- * lives here, wired to the same store + token provider.
153
+ * The per-installation scopes session. A sibling of the installation
154
+ * client, not a method on it: the installation client's exact type is part
155
+ * of the public API (and is guarded by a generics type test), so the
156
+ * scopes surface lives here, wired to the same store + token provider.
161
157
  *
162
158
  * - `queryScopes` reads the cached grant (no network).
163
159
  * - `requestScopes` builds the dashboard deep link the merchant approves
@@ -170,5 +166,18 @@ export interface InstallationScopesClient {
170
166
  requestScopes(scopes: string[], options?: RequestScopesLinkOptions): string;
171
167
  revokeScopes(scopes: string[], options?: RevokeScopesOptions): Promise<string[]>;
172
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
+ */
173
183
  export declare function createInstallationScopesClient(options: InstallationScopesClientOptions): InstallationScopesClient;
174
- //# sourceMappingURL=scopes.d.ts.map