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