@usequeek/app-sdk 0.8.0 → 0.8.1
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/README.md +6 -2
- package/dist/delivery.d.ts +20 -2
- package/dist/delivery.js +31 -3
- package/dist/hono.d.ts +5 -2
- package/dist/hono.js +10 -2
- package/dist/index.d.ts +1 -1
- package/dist/install-handlers.d.ts +5 -0
- package/dist/install-handlers.js +26 -4
- package/dist/merchant-schema.d.ts +11477 -4881
- package/dist/webhooks.d.ts +9 -3
- package/dist/webhooks.js +62 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ export const installRoute = (request: Request) => handleInstallRequest(request,
|
|
|
72
72
|
export const webhookRoute = (request: Request) => handleWebhookRequest(request, webhookOptions);
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
In Next.js App Router, export `installRoute` as `POST` from `app/api/install/route.ts` (and again from `uninstall/route.ts` and `settings/route.ts`), and `webhookRoute` as `POST` from `app/api/webhooks/route.ts`.
|
|
75
|
+
In Next.js App Router, export `installRoute` as `POST`, `GET` and `HEAD` from `app/api/install/route.ts` (and again from `uninstall/route.ts` and `settings/route.ts`), and `webhookRoute` as `POST`, `GET` and `HEAD` from `app/api/webhooks/route.ts`. The `GET` and `HEAD` exports answer the review probe below.
|
|
76
76
|
|
|
77
77
|
## Which entry do I import?
|
|
78
78
|
|
|
@@ -101,6 +101,10 @@ The handlers come in three layers that behave identically:
|
|
|
101
101
|
2. `handleInstallRequest(request, options)` and `handleWebhookRequest(request, options)` adapt a Web `Request` to a `Response`.
|
|
102
102
|
3. `createInstallHandlers`, `createWebhookHandler`, `createProxyHandler` and `createAiToolHandler` from `@usequeek/app-sdk/hono`.
|
|
103
103
|
|
|
104
|
+
### The review probe
|
|
105
|
+
|
|
106
|
+
Queek's app review checks that your install, uninstall and webhook URLs answer, and that the install URL verifies signatures. It sends `GET` requests: one unsigned (any 2xx passes), and on the install URL one signed with your app secret (must be 2xx) and one signed with a wrong secret (must be 4xx). The handlers above answer `GET` and `HEAD` on the install, uninstall, settings and webhook routes for you: unsigned gets 200 `{"ok":true}`, a signed probe is verified like a delivery over the empty body (200 if valid, 401 if not), and none of it runs your callbacks or touches the store. If you mount the handlers yourself, route `GET` and `HEAD` to the same function as `POST`. The Hono helpers already do.
|
|
107
|
+
|
|
104
108
|
### Status codes
|
|
105
109
|
|
|
106
110
|
| Route | Status | Meaning |
|
|
@@ -109,7 +113,7 @@ The handlers come in three layers that behave identically:
|
|
|
109
113
|
| | 400 | The body is not the expected envelope or payload. |
|
|
110
114
|
| | 401 | Missing signature header, stale timestamp, or bad signature. |
|
|
111
115
|
| | 404 | Unknown route or unknown installation. |
|
|
112
|
-
| | 405 | Not a `POST`. |
|
|
116
|
+
| | 405 | Not a `POST`, `GET` or `HEAD`. |
|
|
113
117
|
| | 409 | This `webhook-id` was already claimed. |
|
|
114
118
|
| | 500 | Your callback or the store write threw. The claim is released, so Queek's retry can land. |
|
|
115
119
|
| Topic webhook | 200 | Handled, a repeat (`deduped: true`), or a topic with no handler (`unhandled: true`). |
|
package/dist/delivery.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export interface CoreDelivery {
|
|
|
22
22
|
rawBody: Uint8Array | Buffer | string;
|
|
23
23
|
headers: CoreHeaders;
|
|
24
24
|
}
|
|
25
|
+
/** Layer-1 input for the topic webhook receiver. `GET` and `HEAD` are the review probe; any other method is a delivery. */
|
|
26
|
+
export interface WebhookDelivery extends CoreDelivery {
|
|
27
|
+
/** Defaults to `"POST"`. */
|
|
28
|
+
method?: string;
|
|
29
|
+
}
|
|
25
30
|
/** Layer-1 input for the install handoff, which genuinely needs routing. */
|
|
26
31
|
export interface InstallDelivery extends CoreDelivery {
|
|
27
32
|
/** Defaults to `"POST"`. Anything else answers 405. */
|
|
@@ -57,5 +62,18 @@ export type DeliveryErrorHandler = (error: unknown, failure: DeliveryFailure) =>
|
|
|
57
62
|
* response.
|
|
58
63
|
*/
|
|
59
64
|
export declare function reportDeliveryFailure(onError: DeliveryErrorHandler | undefined, error: unknown, failure: DeliveryFailure): void;
|
|
60
|
-
/** Build the layer-2 `Response` from a layer-1 result. */
|
|
61
|
-
export declare function toResponse(result: DeliveryResult): Response;
|
|
65
|
+
/** Build the layer-2 `Response` from a layer-1 result. A `HEAD` request gets the same status and headers with no body. */
|
|
66
|
+
export declare function toResponse(result: DeliveryResult, method?: string): Response;
|
|
67
|
+
/** True for the methods of the review probe (`GET`, `HEAD`). */
|
|
68
|
+
export declare function isProbeMethod(method: string | undefined): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* True when a probe carries no signature, the plain "is anything listening"
|
|
71
|
+
* check: it answers 200 without touching storage or any callback. A probe
|
|
72
|
+
* with a `webhook-signature` header is verified like a delivery.
|
|
73
|
+
*/
|
|
74
|
+
export declare function isUnsignedProbe(headers: CoreHeaders): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The answer to a review probe: never cached, and never the result of an
|
|
77
|
+
* app callback. `error` is set for a refused (401) probe.
|
|
78
|
+
*/
|
|
79
|
+
export declare function probeResult(status: 200 | 401, error?: string): DeliveryResult;
|
package/dist/delivery.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "./logger.js";
|
|
2
|
+
import { WEBHOOK_SIGNATURE_HEADER } from "./signatures.js";
|
|
2
3
|
/** Case-insensitive header lookup across `Headers` and plain (possibly lowercased, possibly array-valued) objects. */
|
|
3
4
|
export function readHeader(headers, name) {
|
|
4
5
|
if (headers instanceof Headers) {
|
|
@@ -45,7 +46,34 @@ export function reportDeliveryFailure(onError, error, failure) {
|
|
|
45
46
|
// Reporting must never change the HTTP answer.
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
|
-
/** Build the layer-2 `Response` from a layer-1 result. */
|
|
49
|
-
export function toResponse(result) {
|
|
50
|
-
|
|
49
|
+
/** Build the layer-2 `Response` from a layer-1 result. A `HEAD` request gets the same status and headers with no body. */
|
|
50
|
+
export function toResponse(result, method) {
|
|
51
|
+
const response = Response.json(result.body, { status: result.status, headers: result.headers });
|
|
52
|
+
if (method?.toUpperCase() !== "HEAD")
|
|
53
|
+
return response;
|
|
54
|
+
return new Response(null, { status: response.status, headers: response.headers });
|
|
55
|
+
}
|
|
56
|
+
/** True for the methods of the review probe (`GET`, `HEAD`). */
|
|
57
|
+
export function isProbeMethod(method) {
|
|
58
|
+
const upper = (method ?? "POST").toUpperCase();
|
|
59
|
+
return upper === "GET" || upper === "HEAD";
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* True when a probe carries no signature, the plain "is anything listening"
|
|
63
|
+
* check: it answers 200 without touching storage or any callback. A probe
|
|
64
|
+
* with a `webhook-signature` header is verified like a delivery.
|
|
65
|
+
*/
|
|
66
|
+
export function isUnsignedProbe(headers) {
|
|
67
|
+
return !readHeader(headers, WEBHOOK_SIGNATURE_HEADER);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The answer to a review probe: never cached, and never the result of an
|
|
71
|
+
* app callback. `error` is set for a refused (401) probe.
|
|
72
|
+
*/
|
|
73
|
+
export function probeResult(status, error) {
|
|
74
|
+
return {
|
|
75
|
+
status,
|
|
76
|
+
body: status === 200 ? { ok: true } : { ok: false, error },
|
|
77
|
+
headers: { "Cache-Control": "no-store" },
|
|
78
|
+
};
|
|
51
79
|
}
|
package/dist/hono.d.ts
CHANGED
|
@@ -14,9 +14,12 @@ import { type WebhookHandlerOptions } from "./webhooks.js";
|
|
|
14
14
|
export type { InstallCallbacks, InstallHandlerOptions } from "./install-handlers.js";
|
|
15
15
|
export type { AiToolRequestOptions, ProxyRequestOptions, ProxyResponder } from "./proxy.js";
|
|
16
16
|
export type { QueekWebhookEnvelope, SecretResolution, WebhookHandlerContext, WebhookHandlerFn, WebhookHandlerOptions, } from "./webhooks.js";
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Mount the signed install/uninstall/settings handoff (`POST /install`, `POST /uninstall`, `POST /settings`).
|
|
19
|
+
* `GET` and `HEAD` on the same paths answer Queek's app-review probe (no callback runs).
|
|
20
|
+
*/
|
|
18
21
|
export declare function createInstallHandlers(options: InstallHandlerOptions): Hono;
|
|
19
|
-
/** Mount the signed topic-delivery receiver (`POST /`). */
|
|
22
|
+
/** Mount the signed topic-delivery receiver (`POST /`). `GET` and `HEAD` answer Queek's app-review probe. */
|
|
20
23
|
export declare function createWebhookHandler(options: WebhookHandlerOptions): Hono;
|
|
21
24
|
/**
|
|
22
25
|
* Mount the signed app-proxy reader (`GET` only, proxy reads are
|
package/dist/hono.js
CHANGED
|
@@ -11,18 +11,26 @@ import { Hono } from "hono";
|
|
|
11
11
|
import { handleInstallRequest } from "./install-handlers.js";
|
|
12
12
|
import { handleAiToolRequest, handleProxyRequest, } from "./proxy.js";
|
|
13
13
|
import { handleWebhookRequest } from "./webhooks.js";
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Mount the signed install/uninstall/settings handoff (`POST /install`, `POST /uninstall`, `POST /settings`).
|
|
16
|
+
* `GET` and `HEAD` on the same paths answer Queek's app-review probe (no callback runs).
|
|
17
|
+
*/
|
|
15
18
|
export function createInstallHandlers(options) {
|
|
16
19
|
const app = new Hono();
|
|
17
20
|
app.post("/install", (c) => handleInstallRequest(c.req.raw, options));
|
|
18
21
|
app.post("/uninstall", (c) => handleInstallRequest(c.req.raw, options));
|
|
19
22
|
app.post("/settings", (c) => handleInstallRequest(c.req.raw, options));
|
|
23
|
+
// Hono answers HEAD from the GET route; layer 2 handles both methods.
|
|
24
|
+
app.get("/install", (c) => handleInstallRequest(c.req.raw, options));
|
|
25
|
+
app.get("/uninstall", (c) => handleInstallRequest(c.req.raw, options));
|
|
26
|
+
app.get("/settings", (c) => handleInstallRequest(c.req.raw, options));
|
|
20
27
|
return app;
|
|
21
28
|
}
|
|
22
|
-
/** Mount the signed topic-delivery receiver (`POST /`). */
|
|
29
|
+
/** Mount the signed topic-delivery receiver (`POST /`). `GET` and `HEAD` answer Queek's app-review probe. */
|
|
23
30
|
export function createWebhookHandler(options) {
|
|
24
31
|
const app = new Hono();
|
|
25
32
|
app.post("/", (c) => handleWebhookRequest(c.req.raw, options));
|
|
33
|
+
app.get("/", (c) => handleWebhookRequest(c.req.raw, options));
|
|
26
34
|
return app;
|
|
27
35
|
}
|
|
28
36
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { backgroundTaskCount, type CatchupFailure, type CatchupInstallation, typ
|
|
|
14
14
|
export { apiHostsFromEnv, type CursorPage, createQueekClient, DEFAULT_API_HOSTS, DEFAULT_MAX_RETRY_AFTER_MS, DEFAULT_REQUEST_TIMEOUT_MS, devApiHostsFromEnv, InvalidApiBaseError, isWriteMethod, MERCHANT_API_PATH, type MerchantPaths, newIdempotencyKey, type OperationResponse, type PaginateOptions, paginate, QueekApiError, type QueekClient, type QueekClientOptions, type QueekErrorDetails, queekApiErrorFromResponse, type RequestOptions, type ResolvedPaths, type RetryOptions, resolveApiBase, type StoreProfile, } from "./client.js";
|
|
15
15
|
export { type AlertBody, type AlertSeverity, type CollectedDefinition, type CollectedDefinitionCreate, type CollectedDefinitionUpdate, type CollectedField, collectedDefinitions, createRecord, type MerchantAlert, type SetupNotice, type SetupNoticeBody, type SetupNoticeItem, type SetupNoticeStatus, type SuccessEnvelope, sendAlert, setSetupNotice, } from "./collected.js";
|
|
16
16
|
export { decryptSecret, encryptSecret, parseStoreKey, storeKeyFingerprint, } from "./crypto.js";
|
|
17
|
-
export { type CoreDelivery, type CoreHeaders, type DeliveryResult, decodeBody, type InstallDelivery, readHeader, } from "./delivery.js";
|
|
17
|
+
export { type CoreDelivery, type CoreHeaders, type DeliveryResult, decodeBody, type InstallDelivery, readHeader, type WebhookDelivery, } from "./delivery.js";
|
|
18
18
|
export { type AiAction, type AiActionValue, APP_SOURCE, type AppInboundMessage, type AppOutboundMessage, BRIDGE_VERSION, type BridgeTheme, buildReadyMessage, capText, clipOutbound, DASHBOARD_SOURCE, type EmbedEvent, type EmbedEventTarget, type EmbedPostTarget, type FrameBridgeOptions, isAllowedImageUrl, isAllowedOpenTarget, listenToDashboard, MAX_ACTION_ID_LENGTH, MAX_CAPABILITIES, MAX_CAPABILITY_LENGTH, MAX_FILTER_LENGTH, MAX_HEADING_LENGTH, MAX_IMAGE_URL_LENGTH, MAX_LABEL_LENGTH, MAX_LOCALE_LENGTH, MAX_PATH_LENGTH, MAX_PICKED_ITEMS, MAX_REQUEST_ID_LENGTH, MAX_RESIZE_HEIGHT, MAX_RESOURCE_TITLE_LENGTH, MAX_SDK_VERSION_LENGTH, MAX_SECONDARY_ACTIONS, MAX_SELECTION_ID_LENGTH, MAX_SELECTION_IDS, MAX_TARGET_LENGTH, MAX_TOAST_DURATION_MS, MAX_TOAST_LENGTH, onAiAction, type PickResourceRequest, parseInboundMessage, parseOutboundMessage, type ReadyOptions, type ResourceItem, type SaveBarAction, type SaveBarState, sendAck, sendBridgeMessage, sendNavigated, sendOpen, sendPickResource, sendReady, sendResize, sendSaveBar, sendTitle, sendToast, type ThemeMode, type TitleActionDef, type TitleActionTone, type ToastTone, } from "./frame.js";
|
|
19
19
|
export { type HandoffEnvelope, type HandoffEnvelopeAny, type HandoffInstallationRef, type HandoffPidRef, type HandoffStore, INSTALL_EVENT, type InstallData, type InstallEnvelope, RESYNC_EVENT, type ResyncEnvelope, SCOPES_UPDATE_EVENT, type ScopesUpdateData, type ScopesUpdateEnvelope, SETTINGS_EVENT, type SettingsData, type SettingsEnvelope, UNINSTALL_EVENT, type UninstallData, type UninstallEnvelope, } from "./handoff.js";
|
|
20
20
|
export { buildInstallationRecord, handleInstallDelivery, handleInstallRequest, type InstallCallbacks, type InstallHandlerOptions, installationScopesEqual, saveResyncedInstallation, saveScopesUpdateInstallation, } from "./install-handlers.js";
|
|
@@ -105,6 +105,11 @@ export declare function saveResyncedInstallation(existing: InstallationRecord, d
|
|
|
105
105
|
* `webhook-id`, 404 for an unknown route, and 500 when your callback or the
|
|
106
106
|
* store write throws (Queek then retries).
|
|
107
107
|
*
|
|
108
|
+
* `GET` and `HEAD` on a route are the review probe: 200 `{ ok: true }` with
|
|
109
|
+
* `Cache-Control: no-store` and no callback; with a signature (over the
|
|
110
|
+
* empty body) 200 when it verifies and 401 when not. Any other method
|
|
111
|
+
* answers 405.
|
|
112
|
+
*
|
|
108
113
|
* @example
|
|
109
114
|
* const result = await handleInstallDelivery(
|
|
110
115
|
* { rawBody, headers, method: "POST", path: "/queek/install" },
|
package/dist/install-handlers.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeBody, readHeader, reportDeliveryFailure, toResponse, } from "./delivery.js";
|
|
1
|
+
import { decodeBody, isProbeMethod, isUnsignedProbe, probeResult, readHeader, reportDeliveryFailure, toResponse, } from "./delivery.js";
|
|
2
2
|
import { INSTALL_EVENT, RESYNC_EVENT, SCOPES_UPDATE_EVENT, SETTINGS_EVENT, UNINSTALL_EVENT, } from "./handoff.js";
|
|
3
3
|
import { MAX_TIMESTAMP_SKEW_SECONDS, verifyQueekSignatureDetailed, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, } from "./signatures.js";
|
|
4
4
|
import { normaliseNullablePid, normalisePid } from "./store.js";
|
|
@@ -112,6 +112,11 @@ export function saveResyncedInstallation(existing, data, nowIso = new Date().toI
|
|
|
112
112
|
* `webhook-id`, 404 for an unknown route, and 500 when your callback or the
|
|
113
113
|
* store write throws (Queek then retries).
|
|
114
114
|
*
|
|
115
|
+
* `GET` and `HEAD` on a route are the review probe: 200 `{ ok: true }` with
|
|
116
|
+
* `Cache-Control: no-store` and no callback; with a signature (over the
|
|
117
|
+
* empty body) 200 when it verifies and 401 when not. Any other method
|
|
118
|
+
* answers 405.
|
|
119
|
+
*
|
|
115
120
|
* @example
|
|
116
121
|
* const result = await handleInstallDelivery(
|
|
117
122
|
* { rawBody, headers, method: "POST", path: "/queek/install" },
|
|
@@ -120,10 +125,13 @@ export function saveResyncedInstallation(existing, data, nowIso = new Date().toI
|
|
|
120
125
|
* // reply with result.status and result.body
|
|
121
126
|
*/
|
|
122
127
|
export async function handleInstallDelivery(input, options) {
|
|
128
|
+
const route = trailingSegment(input.path ?? "");
|
|
129
|
+
if (isProbeMethod(input.method)) {
|
|
130
|
+
return ROUTES.has(route) ? probeDelivery(input.headers, options) : UNKNOWN_ROUTE;
|
|
131
|
+
}
|
|
123
132
|
if ((input.method ?? "POST").toUpperCase() !== "POST") {
|
|
124
133
|
return { status: 405, body: { ok: false, error: "method_not_allowed" } };
|
|
125
134
|
}
|
|
126
|
-
const route = trailingSegment(input.path ?? "");
|
|
127
135
|
const rawBody = decodeBody(input.rawBody);
|
|
128
136
|
if (route === "install")
|
|
129
137
|
return installDelivery(rawBody, input.headers, options);
|
|
@@ -131,7 +139,21 @@ export async function handleInstallDelivery(input, options) {
|
|
|
131
139
|
return uninstallDelivery(rawBody, input.headers, options);
|
|
132
140
|
if (route === "settings")
|
|
133
141
|
return settingsDelivery(rawBody, input.headers, options);
|
|
134
|
-
return
|
|
142
|
+
return UNKNOWN_ROUTE;
|
|
143
|
+
}
|
|
144
|
+
const ROUTES = new Set(["install", "uninstall", "settings"]);
|
|
145
|
+
const UNKNOWN_ROUTE = { status: 404, body: { ok: false, error: "unknown_route" } };
|
|
146
|
+
/**
|
|
147
|
+
* The review probe (`GET` / `HEAD` on a handoff route): answers 200 without
|
|
148
|
+
* running a callback or touching the store. A probe that carries a
|
|
149
|
+
* signature goes through the SAME `guard` as a delivery, over the empty
|
|
150
|
+
* body: 200 when it verifies, 401 when not. A valid probe claims nothing.
|
|
151
|
+
*/
|
|
152
|
+
async function probeDelivery(headers, options) {
|
|
153
|
+
if (isUnsignedProbe(headers))
|
|
154
|
+
return probeResult(200);
|
|
155
|
+
const checked = await guard(options, headers, "");
|
|
156
|
+
return checked.ok ? probeResult(200) : probeResult(401, checked.reason);
|
|
135
157
|
}
|
|
136
158
|
/** Last path segment of a pathname or full URL (query/fragment stripped, trailing slashes ignored). */
|
|
137
159
|
function trailingSegment(path) {
|
|
@@ -154,7 +176,7 @@ export async function handleInstallRequest(request, options) {
|
|
|
154
176
|
headers: request.headers,
|
|
155
177
|
method: request.method,
|
|
156
178
|
path: new URL(request.url).pathname,
|
|
157
|
-
}, options));
|
|
179
|
+
}, options), request.method);
|
|
158
180
|
}
|
|
159
181
|
async function guard(options, headers, rawBody) {
|
|
160
182
|
const id = readHeader(headers, WEBHOOK_ID_HEADER);
|