@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.
- package/LICENSE +1 -1
- package/README.md +339 -330
- package/dist/app-auth.d.ts +15 -17
- package/dist/app-auth.js +20 -23
- package/dist/auth-fetch.d.ts +14 -5
- package/dist/auth-fetch.js +15 -6
- package/dist/background.d.ts +10 -2
- package/dist/background.js +9 -1
- package/dist/browser.d.ts +2 -3
- package/dist/browser.js +2 -3
- package/dist/client.d.ts +138 -16
- package/dist/client.js +200 -43
- package/dist/collected.d.ts +13 -15
- package/dist/collected.js +4 -5
- package/dist/crypto.d.ts +11 -1
- package/dist/crypto.js +25 -10
- package/dist/delivery.d.ts +22 -9
- package/dist/delivery.js +26 -21
- package/dist/frame.d.ts +11 -12
- package/dist/frame.js +11 -12
- package/dist/handoff.d.ts +35 -39
- package/dist/handoff.js +9 -10
- package/dist/hono.d.ts +3 -4
- package/dist/hono.js +3 -4
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/install-handlers.d.ts +28 -9
- package/dist/install-handlers.js +48 -18
- package/dist/logger.d.ts +7 -1
- package/dist/logger.js +8 -2
- package/dist/merchant-schema.d.ts +15678 -6236
- package/dist/merchant-schema.js +0 -1
- package/dist/proxy.d.ts +21 -24
- package/dist/proxy.js +20 -22
- package/dist/react.d.ts +10 -2
- package/dist/react.js +11 -3
- package/dist/resync.d.ts +15 -5
- package/dist/resync.js +22 -20
- package/dist/scopes.d.ts +36 -27
- package/dist/scopes.js +37 -40
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -1
- package/dist/session.d.ts +35 -17
- package/dist/session.js +32 -15
- package/dist/signatures.d.ts +31 -5
- package/dist/signatures.js +36 -9
- package/dist/store.d.ts +26 -26
- package/dist/store.js +16 -20
- package/dist/theme.d.ts +1 -2
- package/dist/theme.js +1 -2
- package/dist/tokens.d.ts +18 -10
- package/dist/tokens.js +32 -57
- package/dist/webhooks.d.ts +30 -6
- package/dist/webhooks.js +27 -10
- package/package.json +21 -4
- package/dist/app-auth.d.ts.map +0 -1
- package/dist/app-auth.js.map +0 -1
- package/dist/auth-fetch.d.ts.map +0 -1
- package/dist/auth-fetch.js.map +0 -1
- package/dist/background.d.ts.map +0 -1
- package/dist/background.js.map +0 -1
- package/dist/browser.d.ts.map +0 -1
- package/dist/browser.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/collected.d.ts.map +0 -1
- package/dist/collected.js.map +0 -1
- package/dist/crypto.d.ts.map +0 -1
- package/dist/crypto.js.map +0 -1
- package/dist/delivery.d.ts.map +0 -1
- package/dist/delivery.js.map +0 -1
- package/dist/frame.d.ts.map +0 -1
- package/dist/frame.js.map +0 -1
- package/dist/handoff.d.ts.map +0 -1
- package/dist/handoff.js.map +0 -1
- package/dist/hono.d.ts.map +0 -1
- package/dist/hono.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/install-handlers.d.ts.map +0 -1
- package/dist/install-handlers.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/merchant-schema.d.ts.map +0 -1
- package/dist/merchant-schema.js.map +0 -1
- package/dist/proxy.d.ts.map +0 -1
- package/dist/proxy.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/react.js.map +0 -1
- package/dist/resync.d.ts.map +0 -1
- package/dist/resync.js.map +0 -1
- package/dist/scopes.d.ts.map +0 -1
- package/dist/scopes.js.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js.map +0 -1
- package/dist/signatures.d.ts.map +0 -1
- package/dist/signatures.js.map +0 -1
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js.map +0 -1
- package/dist/theme.d.ts.map +0 -1
- package/dist/theme.js.map +0 -1
- package/dist/tokens.d.ts.map +0 -1
- package/dist/tokens.js.map +0 -1
- package/dist/webhooks.d.ts.map +0 -1
- package/dist/webhooks.js.map +0 -1
package/dist/app-auth.d.ts
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The app credential
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 —
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
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
|
-
//
|
|
16
|
-
// one constant here, so a
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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 —
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/auth-fetch.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Same-origin session carriage for embedded apps (
|
|
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)
|
|
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
|
package/dist/auth-fetch.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Same-origin session carriage for embedded apps (
|
|
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)
|
|
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
|
package/dist/background.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/background.js
CHANGED
|
@@ -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
|
|
3
|
-
*
|
|
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
|
|
3
|
-
*
|
|
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`).
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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).
|
|
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
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* a
|
|
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
|
-
|
|
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>;
|