@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/merchant-schema.js
CHANGED
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
|
|
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
|
|
8
|
-
*
|
|
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 `
|
|
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>`,
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
41
|
-
*
|
|
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
|
|
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
|
|
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
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* `
|
|
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
|
|
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
|
|
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`.
|
|
118
|
-
* read-only
|
|
119
|
-
*
|
|
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
|
|
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
|
|
10
|
-
*
|
|
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 `
|
|
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>`,
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
/**
|
|
35
|
+
/** Default allowed clock skew in seconds; the floor (60s) below applies regardless. */
|
|
36
36
|
export const PROXY_MAX_SKEW_SECONDS = 300;
|
|
37
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
63
|
-
*
|
|
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
|
|
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
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* `
|
|
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`.
|
|
185
|
-
* read-only
|
|
186
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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}`, {
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
18
|
-
*
|
|
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 —
|
|
22
|
-
*
|
|
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 —
|
|
31
|
-
*
|
|
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
|
|
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
|
|
47
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
66
|
-
*
|
|
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
|
|
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 (
|
|
136
|
-
*
|
|
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
|
|
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
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
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
|