@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
@@ -1,21 +1,19 @@
1
1
  /**
2
- * The app credential (S1, SDK 0.2.0): ONE asymmetric RS256 key per app.
3
- * Queek holds the PUBLIC keys (`apps.public_keys`, kid-capped); the app
4
- * holds the PRIVATE key in `APP_PRIVATE_KEY` and signs short-lived app
5
- * JWTs that mint per-installation tokens (`acquireToken()`).
2
+ * The app credential: ONE asymmetric RS256 key per app. Queek holds the
3
+ * PUBLIC keys (kid-capped); the app holds the PRIVATE key in
4
+ * `APP_PRIVATE_KEY` and signs short-lived app JWTs that mint
5
+ * per-installation tokens (`acquireToken()`).
6
6
  *
7
- * Wire contract (BINDING for B2 and S1): every call under `/api/v1/apps`
8
- * carries `Authorization: Bearer <app JWT>` (RS256, header `kid`, claims
9
- * `iss` = app slug, `iat`, `exp`, `exp − iat` ≤ 600 s). `node:crypto` only
10
- * — no new dependency. The JWT and the private key are NEVER logged (the
7
+ * Every call under `/api/v1/apps` carries `Authorization: Bearer <app JWT>`
8
+ * (RS256, header `kid`, claims `iss` = app slug, `iat`, `exp`,
9
+ * `exp − iat` ≤ 600 s). `node:crypto` only — no new dependency. The JWT and the private key are NEVER logged (the
11
10
  * logger redacts `Bearer …` values; signers below never emit them).
12
11
  */
13
12
  /** 401 on an app-credential call: bad/expired JWT or unknown kid. Fatal for the app. */
14
13
  export declare const INVALID_CLIENT_CODE = "invalid_client";
15
14
  /**
16
15
  * 403 on an app-credential call: the app is disabled or its token epoch was
17
- * bumped by `app:revoke-tokens` (kill switch). Confirmed against B1
18
- * (`ApiError::APP_TOKEN_REVOKED`, returned in both cases); matched in
16
+ * bumped (kill switch, returned in both cases); matched in
19
17
  * exactly one place (`isAppTokenRevoked`).
20
18
  */
21
19
  export declare const APP_TOKEN_REVOKED_CODE = "app_token_revoked";
@@ -55,10 +53,10 @@ export declare function isInstallationPending(code: string | undefined): boolean
55
53
  /** True when a resync 429 code is the per-installation rotation cooldown (skip, recorded). */
56
54
  export declare function isResyncCooldown(code: string | undefined): boolean;
57
55
  /**
58
- * Merchant-API token-refusal codes (rev 7): a token in one of these states
56
+ * Merchant-API token-refusal codes: a token in one of these states
59
57
  * is dead — drop it, re-mint once, retry once. `api_key_revoked` covers the
60
- * uninstalled store, the removed kid, and the replaced K-slot; the re-mint
61
- * then answers 404 `app_installation_gone` (purge) or 401 `invalid_client`
58
+ * uninstalled store, the removed kid, and a token replaced by a newer one;
59
+ * the re-mint then answers 404 `app_installation_gone` (purge) or 401 `invalid_client`
62
60
  * (halt) where applicable. Every OTHER merchant 403 (plan, mode)
63
61
  * propagates to the caller without a mint.
64
62
  */
@@ -74,7 +72,7 @@ export declare const INVALID_CLIENT_KEY_CODE = "invalid_client_key";
74
72
  export declare function isTokenRefusal(status: number, code: string | undefined): boolean;
75
73
  /** True when a merchant-API 403 means the cached token predates a grant change. */
76
74
  export declare function isInsufficientScope(status: number, code: string | undefined): boolean;
77
- /** App-JWT `iat` is backdated this far (clock skew, GitHub's recommendation). */
75
+ /** App-JWT `iat` is backdated this far, so a small clock skew between the app and Queek cannot make a fresh token look not-yet-valid. */
78
76
  export declare const APP_JWT_SKEW_SECONDS = 60;
79
77
  /** App-JWT lifetime: `exp = iat + 540`, so `exp − iat` (540 s) stays ≤ the 600 s wire bound. */
80
78
  export declare const APP_JWT_TTL_SECONDS = 540;
@@ -112,7 +110,7 @@ export declare class UnknownInstallationError extends Error {
112
110
  constructor(installationId: string);
113
111
  }
114
112
  export interface AppCredential {
115
- /** `iss`: the app slug (as registered with `app:register`). */
113
+ /** `iss`: the app slug (as registered on the Queek Developer page). */
116
114
  appSlug: string;
117
115
  /** `kid`: which of the app's public keys Queek verifies against (`APP_KEY_ID`). */
118
116
  keyId: string;
@@ -128,7 +126,8 @@ export interface AppCredentialOptions {
128
126
  /**
129
127
  * Accepted `APP_PRIVATE_KEY` forms (env files are one line per variable):
130
128
  * raw PEM text, one-line PEM with literal `\n` escapes, or base64 of the
131
- * full PEM text (one line, no wrapping — what the Developer page shows).
129
+ * full PEM text (one line, no wrapping — the form `queek app dev` writes to
130
+ * `.queek/.env.local`; a server-generated key is shown once, never again).
132
131
  */
133
132
  export declare const APP_PRIVATE_KEY_FORMS = "raw PEM text, one-line PEM with literal \\n escapes, or base64 of the PEM text";
134
133
  /**
@@ -157,4 +156,3 @@ export interface SignAppJwtOptions {
157
156
  * compact JWT; NEVER logs it or the private key.
158
157
  */
159
158
  export declare function signAppJwt(options: SignAppJwtOptions): string;
160
- //# sourceMappingURL=app-auth.d.ts.map
package/dist/app-auth.js CHANGED
@@ -1,27 +1,24 @@
1
1
  import { createPrivateKey, createSign } from "node:crypto";
2
2
  /**
3
- * The app credential (S1, SDK 0.2.0): ONE asymmetric RS256 key per app.
4
- * Queek holds the PUBLIC keys (`apps.public_keys`, kid-capped); the app
5
- * holds the PRIVATE key in `APP_PRIVATE_KEY` and signs short-lived app
6
- * JWTs that mint per-installation tokens (`acquireToken()`).
3
+ * The app credential: ONE asymmetric RS256 key per app. Queek holds the
4
+ * PUBLIC keys (kid-capped); the app holds the PRIVATE key in
5
+ * `APP_PRIVATE_KEY` and signs short-lived app JWTs that mint
6
+ * per-installation tokens (`acquireToken()`).
7
7
  *
8
- * Wire contract (BINDING for B2 and S1): every call under `/api/v1/apps`
9
- * carries `Authorization: Bearer <app JWT>` (RS256, header `kid`, claims
10
- * `iss` = app slug, `iat`, `exp`, `exp − iat` ≤ 600 s). `node:crypto` only
11
- * — no new dependency. The JWT and the private key are NEVER logged (the
8
+ * Every call under `/api/v1/apps` carries `Authorization: Bearer <app JWT>`
9
+ * (RS256, header `kid`, claims `iss` = app slug, `iat`, `exp`,
10
+ * `exp − iat` ≤ 600 s). `node:crypto` only — no new dependency. The JWT and the private key are NEVER logged (the
12
11
  * logger redacts `Bearer …` values; signers below never emit them).
13
12
  */
14
13
  // ---------------------------------------------------------------------------
15
- // Wire-contract error codes. Each code the SDK reacts to lives in EXACTLY
16
- // one constant here, so a backend rename is a one-line change the parallel
17
- // B1/B2 build must confirm.
14
+ // Error codes on the wire. Each code the SDK reacts to lives in EXACTLY
15
+ // one constant here, so a rename on Queek's side is a one-line change.
18
16
  // ---------------------------------------------------------------------------
19
17
  /** 401 on an app-credential call: bad/expired JWT or unknown kid. Fatal for the app. */
20
18
  export const INVALID_CLIENT_CODE = "invalid_client";
21
19
  /**
22
20
  * 403 on an app-credential call: the app is disabled or its token epoch was
23
- * bumped by `app:revoke-tokens` (kill switch). Confirmed against B1
24
- * (`ApiError::APP_TOKEN_REVOKED`, returned in both cases); matched in
21
+ * bumped (kill switch, returned in both cases); matched in
25
22
  * exactly one place (`isAppTokenRevoked`).
26
23
  */
27
24
  export const APP_TOKEN_REVOKED_CODE = "app_token_revoked";
@@ -71,10 +68,10 @@ export function isResyncCooldown(code) {
71
68
  return code === RESYNC_COOLDOWN_CODE;
72
69
  }
73
70
  /**
74
- * Merchant-API token-refusal codes (rev 7): a token in one of these states
71
+ * Merchant-API token-refusal codes: a token in one of these states
75
72
  * is dead — drop it, re-mint once, retry once. `api_key_revoked` covers the
76
- * uninstalled store, the removed kid, and the replaced K-slot; the re-mint
77
- * then answers 404 `app_installation_gone` (purge) or 401 `invalid_client`
73
+ * uninstalled store, the removed kid, and a token replaced by a newer one;
74
+ * the re-mint then answers 404 `app_installation_gone` (purge) or 401 `invalid_client`
78
75
  * (halt) where applicable. Every OTHER merchant 403 (plan, mode)
79
76
  * propagates to the caller without a mint.
80
77
  */
@@ -100,7 +97,7 @@ export function isInsufficientScope(status, code) {
100
97
  // ---------------------------------------------------------------------------
101
98
  // Timing bounds (all in the open, all injectable in tests).
102
99
  // ---------------------------------------------------------------------------
103
- /** App-JWT `iat` is backdated this far (clock skew, GitHub's recommendation). */
100
+ /** App-JWT `iat` is backdated this far, so a small clock skew between the app and Queek cannot make a fresh token look not-yet-valid. */
104
101
  export const APP_JWT_SKEW_SECONDS = 60;
105
102
  /** App-JWT lifetime: `exp = iat + 540`, so `exp − iat` (540 s) stays ≤ the 600 s wire bound. */
106
103
  export const APP_JWT_TTL_SECONDS = 540;
@@ -146,14 +143,15 @@ export class AppMintHaltedError extends Error {
146
143
  export class UnknownInstallationError extends Error {
147
144
  code = "unknown_installation";
148
145
  constructor(installationId) {
149
- super(`Unknown installation ${installationId}: no local row (resync or reinstall first).`);
146
+ super(`Unknown installation ${installationId}: no local row. Recover with resyncFromQueek() (lists the app's active installations from Queek) or reinstall the app on the store.`);
150
147
  this.name = "UnknownInstallationError";
151
148
  }
152
149
  }
153
150
  /**
154
151
  * Accepted `APP_PRIVATE_KEY` forms (env files are one line per variable):
155
152
  * raw PEM text, one-line PEM with literal `\n` escapes, or base64 of the
156
- * full PEM text (one line, no wrapping — what the Developer page shows).
153
+ * full PEM text (one line, no wrapping — the form `queek app dev` writes to
154
+ * `.queek/.env.local`; a server-generated key is shown once, never again).
157
155
  */
158
156
  export const APP_PRIVATE_KEY_FORMS = "raw PEM text, one-line PEM with literal \\n escapes, or base64 of the PEM text";
159
157
  /**
@@ -183,15 +181,15 @@ export function loadAppCredential(options = {}) {
183
181
  const env = options.env ?? process.env;
184
182
  const appSlug = (options.appSlug ?? env.APP_SLUG ?? "").trim();
185
183
  if (appSlug === "") {
186
- throw new InvalidAppCredentialError("Missing app slug for `iss`: pass appSlug or set APP_SLUG to the slug registered with Queek.");
184
+ throw new InvalidAppCredentialError("Missing app slug for `iss`: pass appSlug or set APP_SLUG to the slug from the Queek Developer page (register the app there, or run `queek app dev` — it writes .queek/.env.local).");
187
185
  }
188
186
  const keyId = (options.keyId ?? env.APP_KEY_ID ?? "").trim();
189
187
  if (keyId === "") {
190
- throw new InvalidAppCredentialError("Missing APP_KEY_ID: set it to the kid registered with Queek (`php artisan app:register --public-key=…`).");
188
+ throw new InvalidAppCredentialError("Missing APP_KEY_ID: set it to the key id shown on the Queek Developer page for this app (`queek app dev` writes it to .queek/.env.local on first run).");
191
189
  }
192
190
  const privateKeyInput = (options.privateKeyPem ?? env.APP_PRIVATE_KEY ?? "").trim();
193
191
  if (privateKeyInput === "") {
194
- throw new InvalidAppCredentialError(`Missing APP_PRIVATE_KEY: set it to base64 of the app's RSA private key PEM (one line). Accepted forms: ${APP_PRIVATE_KEY_FORMS}.`);
192
+ throw new InvalidAppCredentialError(`Missing APP_PRIVATE_KEY: set it to the app's RSA private key — your offline-generated file, or the server-generated key (shown once on the Developer page; \`queek app dev\` stores it in .queek/.env.local). If lost, generate a new key. Accepted forms: ${APP_PRIVATE_KEY_FORMS}.`);
195
193
  }
196
194
  const privateKeyPem = decodePrivateKeyInput(privateKeyInput);
197
195
  try {
@@ -236,4 +234,3 @@ export function signAppJwt(options) {
236
234
  }
237
235
  return `${signingInput}.${signature.toString("base64url")}`;
238
236
  }
239
- //# sourceMappingURL=app-auth.js.map
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Same-origin session carriage for embedded apps (U-validation "Auth
3
- * carriage"): `installAuthFetch()` reads the dashboard token from the
2
+ * Same-origin session carriage for embedded apps: `installAuthFetch()` reads the dashboard token from the
4
3
  * first-load URL (`?queek_token=`, stripped immediately), exchanges it
5
4
  * ONCE for the app's own session, and attaches
6
5
  * `Authorization: Bearer <session>` to every same-origin fetch. A 401
@@ -25,8 +24,7 @@ import { type EmbedEventTarget, type EmbedPostTarget } from "./frame.js";
25
24
  * frame src contract (the ONLY token that may ride a URL, namespaced so it
26
25
  * cannot collide with an app's own `token` param). The token is the same
27
26
  * type the bridge `ready→token` flow returns on refresh — first load and
28
- * refresh differ only in transport (URL param vs postMessage), exactly
29
- * Shopify's `id_token`.
27
+ * refresh differ only in transport (URL param vs postMessage).
30
28
  */
31
29
  export declare const LAUNCH_TOKEN_PARAM = "queek_token";
32
30
  /** How long to wait for the dashboard's token reply after a `ready`. */
@@ -87,5 +85,16 @@ export declare function readLaunchToken(href: string, param?: string): string |
87
85
  * present.
88
86
  */
89
87
  export declare function stripLaunchToken(href: string, param?: string): string | null;
88
+ /**
89
+ * Install same-origin session carriage for an embedded page. Reads and strips
90
+ * the first-load token, exchanges it once, and returns a `fetch` that adds
91
+ * `Authorization: Bearer <session>` to same-origin requests.
92
+ *
93
+ * @example
94
+ * const auth = installAuthFetch({
95
+ * dashboardOrigin: "https://dashboard.usequeek.com",
96
+ * exchange: async (token) => (await fetch("/api/session", { method: "POST", body: token })).text(),
97
+ * });
98
+ * const response = await auth.fetch("/api/orders");
99
+ */
90
100
  export declare function installAuthFetch(options: AuthFetchOptions): InstalledAuth;
91
- //# sourceMappingURL=auth-fetch.d.ts.map
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Same-origin session carriage for embedded apps (U-validation "Auth
3
- * carriage"): `installAuthFetch()` reads the dashboard token from the
2
+ * Same-origin session carriage for embedded apps: `installAuthFetch()` reads the dashboard token from the
4
3
  * first-load URL (`?queek_token=`, stripped immediately), exchanges it
5
4
  * ONCE for the app's own session, and attaches
6
5
  * `Authorization: Bearer <session>` to every same-origin fetch. A 401
@@ -25,8 +24,7 @@ import { listenToDashboard, sendReady } from "./frame.js";
25
24
  * frame src contract (the ONLY token that may ride a URL, namespaced so it
26
25
  * cannot collide with an app's own `token` param). The token is the same
27
26
  * type the bridge `ready→token` flow returns on refresh — first load and
28
- * refresh differ only in transport (URL param vs postMessage), exactly
29
- * Shopify's `id_token`.
27
+ * refresh differ only in transport (URL param vs postMessage).
30
28
  */
31
29
  export const LAUNCH_TOKEN_PARAM = "queek_token";
32
30
  /** How long to wait for the dashboard's token reply after a `ready`. */
@@ -63,6 +61,18 @@ export function stripLaunchToken(href, param = LAUNCH_TOKEN_PARAM) {
63
61
  return null;
64
62
  }
65
63
  }
64
+ /**
65
+ * Install same-origin session carriage for an embedded page. Reads and strips
66
+ * the first-load token, exchanges it once, and returns a `fetch` that adds
67
+ * `Authorization: Bearer <session>` to same-origin requests.
68
+ *
69
+ * @example
70
+ * const auth = installAuthFetch({
71
+ * dashboardOrigin: "https://dashboard.usequeek.com",
72
+ * exchange: async (token) => (await fetch("/api/session", { method: "POST", body: token })).text(),
73
+ * });
74
+ * const response = await auth.fetch("/api/orders");
75
+ */
66
76
  export function installAuthFetch(options) {
67
77
  const param = options.param ?? LAUNCH_TOKEN_PARAM;
68
78
  const win = globalWindow();
@@ -70,7 +80,7 @@ export function installAuthFetch(options) {
70
80
  const replaceUrl = options.replaceUrl ?? ((url) => win?.history.replaceState(null, "", url));
71
81
  const fetchImpl = options.fetchImpl ?? globalThis.fetch ?? null;
72
82
  if (fetchImpl === null) {
73
- throw new Error("installAuthFetch needs a fetch implementation");
83
+ throw new Error("installAuthFetch needs a fetch implementation: pass `fetchImpl`, or run where a global `fetch` exists.");
74
84
  }
75
85
  const fetchFn = fetchImpl;
76
86
  const listenTarget = options.listenTarget ?? win ?? undefined;
@@ -260,4 +270,3 @@ export function installAuthFetch(options) {
260
270
  },
261
271
  };
262
272
  }
263
- //# sourceMappingURL=auth-fetch.js.map
@@ -15,11 +15,19 @@ export interface DetachOptions {
15
15
  label: string;
16
16
  onError: (error: unknown) => void;
17
17
  }
18
+ /**
19
+ * Run a promise in the background without leaving it unhandled: a rejection
20
+ * goes to `options.onError` with the `label`, never to an unhandled-rejection
21
+ * crash. Use it to answer a delivery fast and finish the work after.
22
+ *
23
+ * @example
24
+ * detach(syncCatalogue(installationId), { label: "catalogue sync", onError: (e) => logger.error("sync failed", { e: String(e) }) });
25
+ */
18
26
  export declare function detach(promise: Promise<unknown>, options: DetachOptions): void;
19
27
  /** How many background tasks are currently running (health/debug only). */
20
28
  export declare function backgroundTaskCount(): number;
21
29
  /**
22
- * Jittered per-installation catch-up for one container (S1, SDK 0.2.0):
30
+ * Jittered per-installation catch-up for one container:
23
31
  * iterate every stored installation and run `forInstallation` (a token
24
32
  * proof-call, a resync batch, an inbox poll — whatever the app needs
25
33
  * hourly), with:
@@ -64,5 +72,5 @@ export interface CatchupOptions {
64
72
  /** Injectable uniform [0,1) for jitter. Defaults to `Math.random`. */
65
73
  random?: () => number;
66
74
  }
75
+ /** Run `forInstallation` for every stored installation on a jittered schedule. See the notes above for the guarantees. */
67
76
  export declare function runInstallationCatchup(options: CatchupOptions): Promise<CatchupResult>;
68
- //# sourceMappingURL=background.d.ts.map
@@ -12,6 +12,14 @@
12
12
  * when replicas or an outbound queue need shared state.
13
13
  */
14
14
  const inFlight = new Set();
15
+ /**
16
+ * Run a promise in the background without leaving it unhandled: a rejection
17
+ * goes to `options.onError` with the `label`, never to an unhandled-rejection
18
+ * crash. Use it to answer a delivery fast and finish the work after.
19
+ *
20
+ * @example
21
+ * detach(syncCatalogue(installationId), { label: "catalogue sync", onError: (e) => logger.error("sync failed", { e: String(e) }) });
22
+ */
15
23
  export function detach(promise, options) {
16
24
  inFlight.add(promise);
17
25
  promise
@@ -29,6 +37,7 @@ export function backgroundTaskCount() {
29
37
  function defaultSleep(ms) {
30
38
  return new Promise((resolve) => setTimeout(resolve, ms));
31
39
  }
40
+ /** Run `forInstallation` for every stored installation on a jittered schedule. See the notes above for the guarantees. */
32
41
  export async function runInstallationCatchup(options) {
33
42
  const sleep = options.sleep ?? defaultSleep;
34
43
  const random = options.random ?? Math.random;
@@ -75,4 +84,3 @@ export async function runInstallationCatchup(options) {
75
84
  await Promise.all(Array.from({ length: Math.min(concurrency, resolved.length) }, () => worker()));
76
85
  return result;
77
86
  }
78
- //# sourceMappingURL=background.js.map
package/dist/browser.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Plain-browser entry for `@usequeek/app-sdk` (Booking fix): the embedded-app
3
- * bridge plus `installAuthFetch`, with ZERO Node-only modules.
2
+ * Plain-browser entry for `@usequeek/app-sdk`: the embedded-page bridge plus
3
+ * `installAuthFetch`, with ZERO Node-only modules.
4
4
  *
5
5
  * Import from `@usequeek/app-sdk/browser` in any browser bundle (Vite,
6
6
  * Rollup, esbuild, …). Importing the same helpers from the MAIN entry pulls
@@ -22,4 +22,3 @@
22
22
  export { type AuthFetchOptions, BRIDGE_TOKEN_TIMEOUT_MS, type InstalledAuth, installAuthFetch, LAUNCH_TOKEN_PARAM, readLaunchToken, stripLaunchToken, } from "./auth-fetch.js";
23
23
  export { APP_SOURCE, type AppInboundMessage, type AppOutboundMessage, BRIDGE_VERSION, type BridgeTheme, DASHBOARD_SOURCE, type EmbedEvent, type EmbedEventTarget, type EmbedPostTarget, type FrameBridgeOptions, listenToDashboard, type PickResourceRequest, type ReadyOptions, type ResourceItem, type SaveBarAction, type SaveBarState, sendNavigated, sendOpen, sendPickResource, sendReady, sendResize, sendSaveBar, sendTitle, type ThemeMode, type TitleActionDef, } from "./frame.js";
24
24
  export { applyTheme, getThemeModeFromUrl, installThemeListener, rememberThemeMode, syncThemeUrl, THEME_PARAM, THEME_STORAGE_KEY, type ThemeDocument, type ThemeDocumentElement, type ThemeListenerOptions, type ThemeStorage, type ThemeUrlOptions, themeBootstrapScript, } from "./theme.js";
25
- //# sourceMappingURL=browser.d.ts.map
package/dist/browser.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Plain-browser entry for `@usequeek/app-sdk` (Booking fix): the embedded-app
3
- * bridge plus `installAuthFetch`, with ZERO Node-only modules.
2
+ * Plain-browser entry for `@usequeek/app-sdk`: the embedded-page bridge plus
3
+ * `installAuthFetch`, with ZERO Node-only modules.
4
4
  *
5
5
  * Import from `@usequeek/app-sdk/browser` in any browser bundle (Vite,
6
6
  * Rollup, esbuild, …). Importing the same helpers from the MAIN entry pulls
@@ -24,4 +24,3 @@ export { APP_SOURCE, BRIDGE_VERSION, DASHBOARD_SOURCE, listenToDashboard, sendNa
24
24
  // NOTE: ThemeMode comes from ./frame.js only — theme.js merely re-exports
25
25
  // it, so listing it here too would be ambiguous.
26
26
  export { applyTheme, getThemeModeFromUrl, installThemeListener, rememberThemeMode, syncThemeUrl, THEME_PARAM, THEME_STORAGE_KEY, themeBootstrapScript, } from "./theme.js";
27
- //# sourceMappingURL=browser.js.map
package/dist/client.d.ts CHANGED
@@ -2,12 +2,11 @@ import type { paths } from "./merchant-schema.js";
2
2
  /**
3
3
  * Typed fetch client over Queek's public Merchant API.
4
4
  *
5
- * - Base URL comes from the install handoff (`api_base`). The backend sends
6
- * the BARE store host (`AppInstallService::installPayload()` passes
7
- * `rtrim(config('app.url'))` verbatim, e.g. `https://api.usequeek.com`);
8
- * the client appends `/api/v1/merchant` when it is absent, so a bare host
9
- * and a full merchant base both work. Never hardcoded otherwise, so test
10
- * stores ride their own host.
5
+ * - Base URL comes from the install handoff (`api_base`). Queek sends the
6
+ * BARE store host (e.g. `https://api.usequeek.com`); the client appends
7
+ * `/api/v1/merchant` when it is absent, so a bare host and a full
8
+ * merchant base both work. Never hardcoded otherwise, so test stores ride
9
+ * their own host.
11
10
  * - `apiBase` is validated at construction — https only, no credentials,
12
11
  * host on the allowlist — and throws BEFORE any fetch, so a
13
12
  * signed-but-stale or attacker-influenced handoff can never point
@@ -24,7 +23,12 @@ import type { paths } from "./merchant-schema.js";
24
23
  * is additive, so both are read). Switch on `code`, never on `message`.
25
24
  * - 429s expose `retryAfterMs` parsed from `Retry-After` (seconds or
26
25
  * HTTP-date). Plain `request()` never retries; `requestWithRetry()`
27
- * retries 429s and network errors with the SAME idempotency key.
26
+ * retries 429s, 502/503/504, timeouts and network errors with jittered
27
+ * exponential backoff and the SAME idempotency key on every attempt.
28
+ * - Every request has a timeout (`timeoutMs`, default 30 s) and accepts an
29
+ * `AbortSignal`. Failures that never produced an HTTP answer are thrown as
30
+ * `QueekApiError` with status 0 and the code `network_error`,
31
+ * `request_timeout` or `request_aborted`, and the original error as `cause`.
28
32
  *
29
33
  * Typed surface: `getStore()` is typed from the app's Merchant API
30
34
  * paths. Pass the app's own codegen output as the type argument
@@ -72,13 +76,13 @@ export declare class InvalidApiBaseError extends Error {
72
76
  constructor(message: string);
73
77
  }
74
78
  /**
75
- * Extra allowed `apiBase` hosts for test/local backends, from
79
+ * Extra allowed `apiBase` hosts for test and local setups, from
76
80
  * `QUEEK_API_HOSTS` (comma-separated bare hostnames). Parsed strictly: any
77
81
  * entry that is not a bare hostname throws — nothing is silently skipped.
78
82
  */
79
83
  export declare function apiHostsFromEnv(env?: NodeJS.ProcessEnv): string[];
80
84
  /**
81
- * DEV-ONLY extra allowed hosts (e.g. an HTTPS tunnel to a local backend),
85
+ * DEV-ONLY extra allowed hosts (e.g. an HTTPS tunnel to a local Queek API),
82
86
  * from `QUEEK_DEV_API_HOSTS`. REFUSED when NODE_ENV=production: a dev
83
87
  * tunnel host must never be allowlisted in production, even by accident.
84
88
  * Throws at client construction, before any fetch.
@@ -93,26 +97,40 @@ export declare function devApiHostsFromEnv(env?: NodeJS.ProcessEnv): string[];
93
97
  * QUEEK_DEV_API_HOSTS`.
94
98
  */
95
99
  export declare function resolveApiBase(raw: string, allowedApiHosts?: string[]): string;
100
+ /** Default time a single HTTP attempt may take before it fails with `request_timeout`. */
101
+ export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
102
+ /** Default ceiling for honouring a 429 `Retry-After` inside `requestWithRetry()`. */
103
+ export declare const DEFAULT_MAX_RETRY_AFTER_MS = 30000;
96
104
  export interface QueekClientOptions {
97
105
  /** Handoff `api_base` (bare store host or full merchant base). Validated at construction. */
98
106
  apiBase: string;
107
+ /** The installation credential, sent as `X-Client-Key`. Never logged and never put in an error message. */
99
108
  apiKey: string;
100
109
  fetchImpl?: typeof fetch;
110
+ /** Time one attempt may take, in ms (default 30000). `0` disables the client timeout. */
111
+ timeoutMs?: number;
101
112
  /** Sent as User-Agent. Defaults to `queek-app/1.0`. */
102
113
  userAgent?: string;
103
- /** Extra allowed `apiBase` hosts (test/local backends). Production default always applies. */
114
+ /** Extra allowed `apiBase` hosts (test/local API hosts). Production default always applies. */
104
115
  allowedApiHosts?: string[];
105
116
  }
106
117
  export interface RequestOptions {
118
+ /** Query string values. `undefined` entries are skipped; everything else is URL-encoded for you. */
107
119
  query?: Record<string, string | number | boolean | undefined>;
120
+ /** JSON-serialisable request body. */
108
121
  body?: unknown;
109
122
  /** Write idempotency (POST/PUT/PATCH/DELETE). Generated when omitted. */
110
123
  idempotencyKey?: string;
124
+ /** Correlation id sent as `X-Request-Id` (8 to 128 characters of `A-Za-z0-9_.:-`). Generated when omitted. */
111
125
  requestId?: string;
126
+ /** Aborts the call. An aborted call fails with code `request_aborted` and is never retried. */
112
127
  signal?: AbortSignal;
128
+ /** Overrides the client's per-attempt timeout for this call, in ms. `0` disables it. */
129
+ timeoutMs?: number;
113
130
  }
114
131
  /** True for HTTP methods that carry an `Idempotency-Key` (POST/PUT/PATCH/DELETE). */
115
132
  export declare function isWriteMethod(method: string): boolean;
133
+ /** A fresh `Idempotency-Key` value (a random UUID). */
116
134
  export declare function newIdempotencyKey(): string;
117
135
  export interface QueekErrorDetails {
118
136
  status: number;
@@ -123,7 +141,31 @@ export interface QueekErrorDetails {
123
141
  docUrl?: string;
124
142
  requestId?: string;
125
143
  retryAfterMs?: number;
144
+ /** The underlying error, when this one wraps another (a failed `fetch`, for example). */
145
+ cause?: unknown;
126
146
  }
147
+ /**
148
+ * The one error every Queek HTTP call throws. Switch on `code`, never on
149
+ * `message`.
150
+ *
151
+ * Codes that come from Queek are the API's own (`validation_failed`,
152
+ * `insufficient_scope`, `too_many_requests`, ...). Codes the SDK raises when
153
+ * no usable HTTP answer arrived (`status` is 0 for the first three):
154
+ * `network_error`, `request_timeout`, `request_aborted`, and
155
+ * `invalid_response` (an HTTP success whose body was not JSON).
156
+ *
157
+ * `requestId` matches the `X-Request-Id` response header: quote it to Queek
158
+ * support.
159
+ *
160
+ * @example
161
+ * try {
162
+ * await client.request("POST", "/orders", { body });
163
+ * } catch (error) {
164
+ * if (error instanceof QueekApiError && error.code === "validation_failed") {
165
+ * console.error(error.field, error.errors, error.requestId);
166
+ * }
167
+ * }
168
+ */
127
169
  export declare class QueekApiError extends Error {
128
170
  readonly status: number;
129
171
  readonly code: string;
@@ -136,7 +178,26 @@ export declare class QueekApiError extends Error {
136
178
  constructor(details: QueekErrorDetails);
137
179
  get isRateLimited(): boolean;
138
180
  get isAuthFailure(): boolean;
181
+ /**
182
+ * True when sending the same call again can succeed without any change:
183
+ * rate limits, gateway failures (502, 503, 504), timeouts and network
184
+ * errors. `requestWithRetry()` retries exactly these. A caller abort is
185
+ * never retryable.
186
+ */
187
+ get isRetryable(): boolean;
139
188
  }
189
+ /**
190
+ * The error for a call that never produced an HTTP answer. A caller abort is
191
+ * `request_aborted`, the client timeout is `request_timeout`, everything else
192
+ * (DNS, refused or reset connection, TLS) is `network_error`. The original
193
+ * error stays on `cause`.
194
+ */
195
+ export declare function transportError(cause: unknown, signal?: AbortSignal): QueekApiError;
196
+ /**
197
+ * Combine a caller signal with the per-attempt timeout. Returns the caller
198
+ * signal untouched when `timeoutMs` is 0 or negative.
199
+ */
200
+ export declare function requestSignal(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal | undefined;
140
201
  /**
141
202
  * Build a `QueekApiError` from a status + parsed body + headers. Shared
142
203
  * with the installation-token mint path (`tokens.ts`), so the merchant and
@@ -148,10 +209,17 @@ export interface RetryOptions {
148
209
  maxAttempts?: number;
149
210
  /** Base backoff between attempts in ms (default 250); doubles per attempt. */
150
211
  baseDelayMs?: number;
151
- /** Backoff ceiling in ms (default 5000). A 429 `Retry-After` can exceed it. */
212
+ /** Backoff ceiling in ms (default 5000). */
152
213
  maxDelayMs?: number;
214
+ /**
215
+ * Longest `Retry-After` the call will wait out, in ms (default 30000). A
216
+ * 429 that asks for more is thrown at once instead of holding the caller.
217
+ */
218
+ maxRetryAfterMs?: number;
153
219
  /** Injectable clock for tests. Defaults to a real setTimeout sleep. */
154
220
  sleep?: (ms: number) => Promise<void>;
221
+ /** Injectable randomness for the backoff jitter, in [0, 1). Defaults to `Math.random`. */
222
+ random?: () => number;
155
223
  }
156
224
  export interface QueekClient<TPaths = paths> {
157
225
  /** `GET /store` — the handed-off key proving itself. Typed from the app's paths (`TPaths`, default: bundled shim). */
@@ -159,12 +227,66 @@ export interface QueekClient<TPaths = paths> {
159
227
  /** Generic typed escape hatch: `request<T>("GET", "/orders", { query })`. Never retries. */
160
228
  request<T>(method: string, path: string, options?: RequestOptions): Promise<T>;
161
229
  /**
162
- * `request()` with bounded retries on 429s and network errors. The SAME
163
- * idempotency key is reused across attempts (generated once when the
164
- * caller does not supply one), so a retry can never duplicate a write;
165
- * a 429 `Retry-After` is honoured over the backoff schedule.
230
+ * `request()` with bounded retries on 429s, 502/503/504, timeouts and
231
+ * network errors, with jittered exponential backoff. The SAME idempotency
232
+ * key is reused across attempts (generated once when the caller does not
233
+ * supply one), so a retry can never duplicate a write; a 429
234
+ * `Retry-After` is honoured over the backoff schedule. A caller abort is
235
+ * never retried.
166
236
  */
167
237
  requestWithRetry<T>(method: string, path: string, options?: RequestOptions & RetryOptions): Promise<T>;
168
238
  }
239
+ /**
240
+ * The retry loop both clients share. `run` receives the idempotency key to
241
+ * send (generated ONCE for writes when the caller gave none), so every
242
+ * attempt of one write carries the same key.
243
+ */
244
+ export declare function runWithRetry<T>(method: string, options: RetryOptions & {
245
+ idempotencyKey?: string;
246
+ }, run: (idempotencyKey: string | undefined) => Promise<T>): Promise<T>;
247
+ /**
248
+ * Create a client for the Merchant API with one static installation key.
249
+ * Most apps want `createInstallationClient()` instead, which mints and
250
+ * refreshes the key for you.
251
+ *
252
+ * @example
253
+ * const client = createQueekClient({ apiBase: install.api_base, apiKey: process.env.QUEEK_KEY ?? "" });
254
+ * const store = await client.getStore();
255
+ */
169
256
  export declare function createQueekClient<TPaths = paths>(clientOptions: QueekClientOptions): QueekClient<ResolvedPaths<TPaths>>;
170
- //# sourceMappingURL=client.d.ts.map
257
+ /**
258
+ * Parse a non-JSON-safe error body: JSON when it parses, otherwise the first
259
+ * 500 characters as the error `message`.
260
+ */
261
+ export declare function tryParseJson(text: string): unknown;
262
+ /**
263
+ * Parse an HTTP success body. An empty body is `null`; a body that is not
264
+ * JSON is an `invalid_response` error, never a silently mistyped value.
265
+ */
266
+ export declare function parseSuccessBody<T>(response: Response, text: string): T;
267
+ /** One page of a cursor-paginated Merchant API list. */
268
+ export interface CursorPage<T> {
269
+ data: T[];
270
+ has_more: boolean;
271
+ next_cursor?: string | null;
272
+ }
273
+ export interface PaginateOptions {
274
+ /** Extra query parameters (filters, sort). `limit` and `starting_after` are managed for you. */
275
+ query?: Record<string, string | number | boolean | undefined>;
276
+ /** Rows per page. Each endpoint documents its own maximum. */
277
+ limit?: number;
278
+ signal?: AbortSignal;
279
+ /** Retry tuning for each page fetch. */
280
+ retry?: RetryOptions;
281
+ }
282
+ /**
283
+ * Iterate every row of a cursor-paginated list (`data`, `has_more`,
284
+ * `next_cursor`, driven by `starting_after`), fetching one page at a time
285
+ * with `requestWithRetry()`. Stops when `has_more` is false.
286
+ *
287
+ * @example
288
+ * for await (const order of paginate<{ p_id: string }>(client, "/orders", { limit: 50 })) {
289
+ * console.log(order.p_id);
290
+ * }
291
+ */
292
+ export declare function paginate<T>(client: Pick<QueekClient<unknown>, "requestWithRetry">, path: string, options?: PaginateOptions): AsyncGenerator<T, void, undefined>;