@usequeek/app-sdk 0.7.2 → 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 +76 -4
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/delivery.d.ts +20 -2
- package/dist/delivery.js +31 -3
- package/dist/frame.d.ts +23 -1
- package/dist/frame.js +59 -1
- package/dist/hono.d.ts +16 -4
- package/dist/hono.js +24 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +3 -2
- package/dist/install-handlers.d.ts +5 -0
- package/dist/install-handlers.js +26 -4
- package/dist/manifest.d.ts +98 -0
- package/dist/manifest.js +54 -0
- package/dist/merchant-schema.d.ts +23354 -8669
- package/dist/proxy.d.ts +62 -2
- package/dist/proxy.js +89 -9
- 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
|
|
|
@@ -99,7 +99,11 @@ The handlers come in three layers that behave identically:
|
|
|
99
99
|
|
|
100
100
|
1. `handleInstallDelivery(input, options)` and `handleWebhookDelivery(input, options)` take raw body bytes plus headers and return `{ status, body }`. They use no request or response types, so any framework works.
|
|
101
101
|
2. `handleInstallRequest(request, options)` and `handleWebhookRequest(request, options)` adapt a Web `Request` to a `Response`.
|
|
102
|
-
3. `createInstallHandlers`, `createWebhookHandler` and `
|
|
102
|
+
3. `createInstallHandlers`, `createWebhookHandler`, `createProxyHandler` and `createAiToolHandler` from `@usequeek/app-sdk/hono`.
|
|
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.
|
|
103
107
|
|
|
104
108
|
### Status codes
|
|
105
109
|
|
|
@@ -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`). |
|
|
@@ -429,6 +433,73 @@ export async function GET(request: Request) {
|
|
|
429
433
|
|
|
430
434
|
The canonical string is `path`, `shop`, `ts` and the sorted, percent-encoded query pairs (without `sig`), joined with newlines. The signature is a hex HMAC-SHA256 keyed with the full `whsec_…` string, with no base64 decode. `kid` selects the installation, timestamps may skew by 5 minutes, each `jti` is single-use, and only `GET` is served. With Hono, mount `createProxyHandler({ store, path, onVerified })`.
|
|
431
435
|
|
|
436
|
+
Queek also signs a `surface` on every proxy call: `storefront` (a shopper), `dashboard` (your merchant page) or `ai` (Queek AI). Only Queek sets it, and it is covered by the signature, so a shopper cannot choose it. A handler that serves merchant data must pass `expectedSurface: "dashboard"`, which answers 403 `surface_mismatch` for a genuine call signed for another surface. `handleProxyRequest` never hands a `surface=ai` call to your responder.
|
|
437
|
+
|
|
438
|
+
## Queek AI tools and actions
|
|
439
|
+
|
|
440
|
+
Declare an `ai` section (`ManifestAi`) in your manifest and Queek AI, the assistant in the merchant dashboard, can use your app in two small ways. It needs no scope. The merchant approves it with the install, and again when you add or change a tool or action.
|
|
441
|
+
|
|
442
|
+
```toml
|
|
443
|
+
[ai]
|
|
444
|
+
summary = "Takes bookings for services."
|
|
445
|
+
|
|
446
|
+
[[ai.tools]]
|
|
447
|
+
key = "upcoming_bookings"
|
|
448
|
+
description = "Upcoming bookings, soonest first."
|
|
449
|
+
|
|
450
|
+
[[ai.tools.inputs]]
|
|
451
|
+
key = "days"
|
|
452
|
+
type = "integer"
|
|
453
|
+
label = "Days ahead"
|
|
454
|
+
min = 1
|
|
455
|
+
max = 30
|
|
456
|
+
|
|
457
|
+
[ai.tools.output]
|
|
458
|
+
list = true
|
|
459
|
+
|
|
460
|
+
[[ai.tools.output.fields]]
|
|
461
|
+
key = "date"
|
|
462
|
+
type = "date"
|
|
463
|
+
|
|
464
|
+
[[ai.actions]]
|
|
465
|
+
key = "add_date_exception"
|
|
466
|
+
description = "Open the Hours page with a date filled in."
|
|
467
|
+
page = "/admin/hours"
|
|
468
|
+
|
|
469
|
+
[[ai.actions.inputs]]
|
|
470
|
+
key = "date"
|
|
471
|
+
type = "date"
|
|
472
|
+
label = "Date"
|
|
473
|
+
required = true
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Tools read. Queek calls your proxy as a signed `GET ai/{key}` with `surface=ai` and the tool's inputs. Serve them with `handleAiToolRequest(request, tool, { subpath, secrets, tools })`, or mount `createAiToolHandler({ subpath, secrets, tools })` from `@usequeek/app-sdk/hono` at the `ai` segment of your proxy. `secrets(kid)` returns the proxy secrets of that installation, current first:
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
import { createAiToolHandler } from "@usequeek/app-sdk/hono";
|
|
480
|
+
|
|
481
|
+
app.route(
|
|
482
|
+
"/proxy/ai",
|
|
483
|
+
createAiToolHandler({
|
|
484
|
+
subpath: "booking", // the manifest extensions.proxy.subpath
|
|
485
|
+
secrets: async (kid) => proxySecretsFor(kid),
|
|
486
|
+
tools: { upcoming_bookings: async (call) => ({ rows: await upcoming(call.kid, call.params.days) }) },
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
A `surface=ai` call is served only by a declared tool handler (403 otherwise), and a storefront or dashboard call at that path is refused. Answer an object, never a bare array: `{ rows: [...] }` for a list tool (`output.list: true`), the declared fields object otherwise. A handler that answers anything else gets a 500. Return only the declared output fields, because Queek drops the rest. Tool inputs are structured, with no free text. `call.actorRole` is the member's role, never a user id. Pass `replayStore` (your installation store) to make each call single-use: the `jti` is claimed like the storefront proxy does, and a replay answers 401. Without it a captured call can be replayed for 5 minutes, which for a read-only tool repeats only the read.
|
|
492
|
+
|
|
493
|
+
Actions never write. Queek AI only opens your `page` (a path under your merchant page) with the values filled in, and shows "Opened by Queek AI, check these values before saving" above it. Receive the values with `onAiAction`, stage them in your form, show them editable, and write nothing until the merchant presses your own Save:
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
import { onAiAction } from "@usequeek/app-sdk/browser";
|
|
497
|
+
|
|
498
|
+
onAiAction(({ action, values }) => stage(action, values), { dashboardOrigin });
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
`MANIFEST_AI_RESERVED_INPUT_KEYS` lists the input names Queek signs or owns (`sig`, `ts`, `surface`, `order_id` and more), and `isReservedAiInputKey` checks one. A manifest that declares an input with such a name, or any `logged_in_customer_` name, is refused. A setting can opt in to being changed by Queek AI with `ai_editable: true` (`ManifestSetting`, `aiEditableSettingKeys`). A `secret` never can, and each change still needs the merchant's confirmation.
|
|
502
|
+
|
|
432
503
|
## Scopes
|
|
433
504
|
|
|
434
505
|
Apps declare required scopes (granted at install) and optional scopes (requested later, revocable).
|
|
@@ -469,7 +540,8 @@ Every export has JSDoc in its type definitions. By module:
|
|
|
469
540
|
| `install-handlers` | `handleInstallDelivery`, `handleInstallRequest`, `saveResyncedInstallation` |
|
|
470
541
|
| `webhooks` | `handleWebhookDelivery`, `handleWebhookRequest` |
|
|
471
542
|
| `signatures` | `verifyQueekSignature`, `verifyQueekSignatureDetailed`, `signQueekPayload` |
|
|
472
|
-
| `proxy` | `verifyProxyQuery`, `verifyProxyDelivery`, `handleProxyRequest` |
|
|
543
|
+
| `proxy` | `verifyProxyQuery`, `verifyProxyDelivery`, `handleProxyRequest`, `handleAiToolRequest` |
|
|
544
|
+
| `manifest` | `ManifestSetting`, `ManifestAi` and its parts, `aiEditableSettingKeys`, `isReservedAiInputKey` |
|
|
473
545
|
| `store` | `SqliteInstallationStore`, `PostgresInstallationStore`, `createInstallationStore` |
|
|
474
546
|
| `resync`, `background` | `resyncFromQueek`, `runInstallationCatchup`, `detach` |
|
|
475
547
|
| `scopes` | `createInstallationScopesClient`, `AppScopeRequiredError` |
|
package/dist/browser.d.ts
CHANGED
|
@@ -20,5 +20,5 @@
|
|
|
20
20
|
* `react` dependency, so this entry can never silently regress.
|
|
21
21
|
*/
|
|
22
22
|
export { type AuthFetchOptions, BRIDGE_TOKEN_TIMEOUT_MS, type InstalledAuth, installAuthFetch, LAUNCH_TOKEN_PARAM, readLaunchToken, stripLaunchToken, } from "./auth-fetch.js";
|
|
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";
|
|
23
|
+
export { type AiAction, type AiActionValue, APP_SOURCE, type AppInboundMessage, type AppOutboundMessage, BRIDGE_VERSION, type BridgeTheme, DASHBOARD_SOURCE, type EmbedEvent, type EmbedEventTarget, type EmbedPostTarget, type FrameBridgeOptions, listenToDashboard, onAiAction, 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";
|
package/dist/browser.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* `react` dependency, so this entry can never silently regress.
|
|
21
21
|
*/
|
|
22
22
|
export { BRIDGE_TOKEN_TIMEOUT_MS, installAuthFetch, LAUNCH_TOKEN_PARAM, readLaunchToken, stripLaunchToken, } from "./auth-fetch.js";
|
|
23
|
-
export { APP_SOURCE, BRIDGE_VERSION, DASHBOARD_SOURCE, listenToDashboard, sendNavigated, sendOpen, sendPickResource, sendReady, sendResize, sendSaveBar, sendTitle, } from "./frame.js";
|
|
23
|
+
export { APP_SOURCE, BRIDGE_VERSION, DASHBOARD_SOURCE, listenToDashboard, onAiAction, sendNavigated, sendOpen, sendPickResource, sendReady, sendResize, sendSaveBar, sendTitle, } from "./frame.js";
|
|
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";
|
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/frame.d.ts
CHANGED
|
@@ -121,6 +121,16 @@ export type AppOutboundMessage = {
|
|
|
121
121
|
selectionIds?: string[];
|
|
122
122
|
requestId?: string;
|
|
123
123
|
};
|
|
124
|
+
/** A value Queek AI filled into an action input: the declared types only, never an object. */
|
|
125
|
+
export type AiActionValue = string | number | boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Queek AI opened one of the app's declared `ai.actions` and sent its values. The values are a SUGGESTION: stage them
|
|
128
|
+
* in the form, show them editable, and write nothing until the merchant presses the app's own Save.
|
|
129
|
+
*/
|
|
130
|
+
export interface AiAction {
|
|
131
|
+
action: string;
|
|
132
|
+
values: Record<string, AiActionValue>;
|
|
133
|
+
}
|
|
124
134
|
export type AppInboundMessage = {
|
|
125
135
|
source: typeof DASHBOARD_SOURCE;
|
|
126
136
|
type: "token";
|
|
@@ -156,7 +166,10 @@ export type AppInboundMessage = {
|
|
|
156
166
|
source: typeof DASHBOARD_SOURCE;
|
|
157
167
|
type: "resource-pick-cancelled";
|
|
158
168
|
requestId?: string;
|
|
159
|
-
}
|
|
169
|
+
} | ({
|
|
170
|
+
source: typeof DASHBOARD_SOURCE;
|
|
171
|
+
type: "ai-action";
|
|
172
|
+
} & AiAction);
|
|
160
173
|
export interface EmbedEvent {
|
|
161
174
|
origin: string;
|
|
162
175
|
data: unknown;
|
|
@@ -188,6 +201,7 @@ export interface FrameBridgeOptions {
|
|
|
188
201
|
onNavigate?: (path: string) => void;
|
|
189
202
|
onResourcePicked?: (items: ResourceItem[], requestId?: string) => void;
|
|
190
203
|
onResourcePickCancelled?: (requestId?: string) => void;
|
|
204
|
+
onAiAction?: (action: AiAction) => void;
|
|
191
205
|
}
|
|
192
206
|
export interface ReadyOptions {
|
|
193
207
|
capabilities?: string[];
|
|
@@ -276,6 +290,14 @@ export declare function sendPickResource(dashboardOrigin: string, target: EmbedP
|
|
|
276
290
|
* Never touches any secret.
|
|
277
291
|
*/
|
|
278
292
|
export declare function listenToDashboard(options: FrameBridgeOptions): () => void;
|
|
293
|
+
/**
|
|
294
|
+
* Receive the action Queek AI opened this page for, from the exact dashboard origin only (the same gate as the token).
|
|
295
|
+
* Like `installAuthFetch`, it listens on the global window and accepts only messages from the embedding parent
|
|
296
|
+
* window; pass `target` / `expectSource` to override (`expectSource: null` turns the source check off).
|
|
297
|
+
* Stage `values` in your form and show them editable; NEVER save on load, only the merchant's own Save writes.
|
|
298
|
+
* Returns an unsubscribe function.
|
|
299
|
+
*/
|
|
300
|
+
export declare function onAiAction(handler: (action: AiAction) => void, options: Pick<FrameBridgeOptions, "dashboardOrigin" | "target" | "expectSource">): () => void;
|
|
279
301
|
/**
|
|
280
302
|
* The single choke point for everything the app posts: caps every string,
|
|
281
303
|
* allow-lists every enum, clamps every number, re-gates `open` targets
|
package/dist/frame.js
CHANGED
|
@@ -64,6 +64,39 @@ function reqText(value, max) {
|
|
|
64
64
|
}
|
|
65
65
|
return capText(value, max);
|
|
66
66
|
}
|
|
67
|
+
const AI_KEY = /^[a-z][a-z0-9_]{0,39}$/;
|
|
68
|
+
const MAX_AI_ACTION_VALUES = 8;
|
|
69
|
+
const MAX_AI_TEXT_LENGTH = 500;
|
|
70
|
+
/** One `ai-action` message: a declared key, and up to 8 scalar values (a `null` is an unset input and is dropped). */
|
|
71
|
+
function parseAiAction(data) {
|
|
72
|
+
const { action, values } = data;
|
|
73
|
+
if (typeof action !== "string" || !AI_KEY.test(action)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (!isRecord(values) || Array.isArray(values)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const entries = Object.entries(values).filter(([, value]) => value !== null);
|
|
80
|
+
if (entries.length > MAX_AI_ACTION_VALUES) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const clean = {};
|
|
84
|
+
for (const [key, value] of entries) {
|
|
85
|
+
if (!AI_KEY.test(key)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (typeof value === "string") {
|
|
89
|
+
clean[key] = capText(value, MAX_AI_TEXT_LENGTH);
|
|
90
|
+
}
|
|
91
|
+
else if (typeof value === "boolean" || (typeof value === "number" && Number.isFinite(value))) {
|
|
92
|
+
clean[key] = value;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { action, values: clean };
|
|
99
|
+
}
|
|
67
100
|
function isThemeMode(value) {
|
|
68
101
|
return value === "light" || value === "dark";
|
|
69
102
|
}
|
|
@@ -283,6 +316,10 @@ export function parseInboundMessage(data) {
|
|
|
283
316
|
}
|
|
284
317
|
return cancelled;
|
|
285
318
|
}
|
|
319
|
+
case "ai-action": {
|
|
320
|
+
const aiAction = parseAiAction(data);
|
|
321
|
+
return aiAction === null ? null : { source: DASHBOARD_SOURCE, type: "ai-action", ...aiAction };
|
|
322
|
+
}
|
|
286
323
|
default:
|
|
287
324
|
return null;
|
|
288
325
|
}
|
|
@@ -522,7 +559,7 @@ export function sendPickResource(dashboardOrigin, target, request) {
|
|
|
522
559
|
* Never touches any secret.
|
|
523
560
|
*/
|
|
524
561
|
export function listenToDashboard(options) {
|
|
525
|
-
const { dashboardOrigin, onToken, onResizeAck, onTheme, onTitleAction, onSaveBarAction, onNavigate, onResourcePicked, onResourcePickCancelled, } = options;
|
|
562
|
+
const { dashboardOrigin, onToken, onResizeAck, onTheme, onTitleAction, onSaveBarAction, onNavigate, onResourcePicked, onResourcePickCancelled, onAiAction, } = options;
|
|
526
563
|
const target = options.target;
|
|
527
564
|
const expectSource = options.expectSource;
|
|
528
565
|
const onMessage = (event) => {
|
|
@@ -572,11 +609,32 @@ export function listenToDashboard(options) {
|
|
|
572
609
|
case "resource-pick-cancelled":
|
|
573
610
|
onResourcePickCancelled?.(message.requestId);
|
|
574
611
|
break;
|
|
612
|
+
case "ai-action":
|
|
613
|
+
onAiAction?.({ action: message.action, values: message.values });
|
|
614
|
+
break;
|
|
575
615
|
}
|
|
576
616
|
};
|
|
577
617
|
target?.addEventListener("message", onMessage);
|
|
578
618
|
return () => target?.removeEventListener("message", onMessage);
|
|
579
619
|
}
|
|
620
|
+
/**
|
|
621
|
+
* Receive the action Queek AI opened this page for, from the exact dashboard origin only (the same gate as the token).
|
|
622
|
+
* Like `installAuthFetch`, it listens on the global window and accepts only messages from the embedding parent
|
|
623
|
+
* window; pass `target` / `expectSource` to override (`expectSource: null` turns the source check off).
|
|
624
|
+
* Stage `values` in your form and show them editable; NEVER save on load, only the merchant's own Save writes.
|
|
625
|
+
* Returns an unsubscribe function.
|
|
626
|
+
*/
|
|
627
|
+
export function onAiAction(handler, options) {
|
|
628
|
+
const scope = globalThis;
|
|
629
|
+
const win = scope.window?.location !== undefined ? scope.window : undefined;
|
|
630
|
+
const expectSource = options.expectSource !== undefined ? options.expectSource : win?.parent;
|
|
631
|
+
return listenToDashboard({
|
|
632
|
+
dashboardOrigin: options.dashboardOrigin,
|
|
633
|
+
target: options.target ?? win,
|
|
634
|
+
expectSource: expectSource ?? undefined,
|
|
635
|
+
onAiAction: handler,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
580
638
|
/** Validate a title-bar action shape, then cap its strings. */
|
|
581
639
|
function clipTitleActionDef(value) {
|
|
582
640
|
const def = parseTitleActionDef(value);
|
package/dist/hono.d.ts
CHANGED
|
@@ -9,14 +9,17 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { Hono } from "hono";
|
|
11
11
|
import { type InstallHandlerOptions } from "./install-handlers.js";
|
|
12
|
-
import { type ProxyRequestOptions, type ProxyResponder } from "./proxy.js";
|
|
12
|
+
import { type AiToolRequestOptions, type ProxyRequestOptions, type ProxyResponder } from "./proxy.js";
|
|
13
13
|
import { type WebhookHandlerOptions } from "./webhooks.js";
|
|
14
14
|
export type { InstallCallbacks, InstallHandlerOptions } from "./install-handlers.js";
|
|
15
|
-
export type { ProxyRequestOptions, ProxyResponder } from "./proxy.js";
|
|
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
|
|
@@ -29,3 +32,12 @@ export declare function createWebhookHandler(options: WebhookHandlerOptions): Ho
|
|
|
29
32
|
export declare function createProxyHandler(options: ProxyRequestOptions & {
|
|
30
33
|
onVerified: ProxyResponder;
|
|
31
34
|
}): Hono;
|
|
35
|
+
/**
|
|
36
|
+
* Mount the Queek AI read tools (`GET /:tool`) — a thin wrapper built ONLY on
|
|
37
|
+
* `handleAiToolRequest`. Mount it at the `ai` segment of the proxy
|
|
38
|
+
* (`app.route("/proxy/ai", createAiToolHandler({ subpath, secrets, tools }))`);
|
|
39
|
+
* `subpath` is the manifest `extensions.proxy.subpath`, `secrets` resolves the
|
|
40
|
+
* proxy secrets of installation `kid`, and `tools` holds the handlers of the
|
|
41
|
+
* manifest `ai.tools`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createAiToolHandler(options: AiToolRequestOptions): Hono;
|
package/dist/hono.js
CHANGED
|
@@ -9,20 +9,28 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { Hono } from "hono";
|
|
11
11
|
import { handleInstallRequest } from "./install-handlers.js";
|
|
12
|
-
import { handleProxyRequest } from "./proxy.js";
|
|
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
|
/**
|
|
@@ -39,3 +47,16 @@ export function createProxyHandler(options) {
|
|
|
39
47
|
app.get("*", (c) => handleProxyRequest(c.req.raw, requestOptions, onVerified));
|
|
40
48
|
return app;
|
|
41
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Mount the Queek AI read tools (`GET /:tool`) — a thin wrapper built ONLY on
|
|
52
|
+
* `handleAiToolRequest`. Mount it at the `ai` segment of the proxy
|
|
53
|
+
* (`app.route("/proxy/ai", createAiToolHandler({ subpath, secrets, tools }))`);
|
|
54
|
+
* `subpath` is the manifest `extensions.proxy.subpath`, `secrets` resolves the
|
|
55
|
+
* proxy secrets of installation `kid`, and `tools` holds the handlers of the
|
|
56
|
+
* manifest `ai.tools`.
|
|
57
|
+
*/
|
|
58
|
+
export function createAiToolHandler(options) {
|
|
59
|
+
const app = new Hono();
|
|
60
|
+
app.get("/:tool", (c) => handleAiToolRequest(c.req.raw, c.req.param("tool"), options));
|
|
61
|
+
return app;
|
|
62
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,12 +14,13 @@ 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";
|
|
18
|
-
export { 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, 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";
|
|
17
|
+
export { type CoreDelivery, type CoreHeaders, type DeliveryResult, decodeBody, type InstallDelivery, readHeader, type WebhookDelivery, } from "./delivery.js";
|
|
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";
|
|
21
21
|
export { createLogger, type LogFields, type Logger, type LoggerOptions, type LogLevel, REDACTED, } from "./logger.js";
|
|
22
|
-
export {
|
|
22
|
+
export { aiEditableSettingKeys, isReservedAiInputKey, MANIFEST_AI_ACTION_INPUT_TYPES, MANIFEST_AI_OUTPUT_TYPES, MANIFEST_AI_RESERVED_INPUT_KEYS, MANIFEST_AI_RESERVED_INPUT_PREFIX, MANIFEST_AI_TOOL_INPUT_TYPES, MANIFEST_SETTING_TYPES, type ManifestAi, type ManifestAiAction, type ManifestAiInput, type ManifestAiInputType, type ManifestAiOutputField, type ManifestAiOutputType, type ManifestAiTool, type ManifestSetting, type ManifestSettingType, } from "./manifest.js";
|
|
23
|
+
export { type AiToolCall, type AiToolHandler, type AiToolOutput, type AiToolRequestOptions, buildProxyCanonicalString, handleAiToolRequest, handleProxyRequest, PROXY_CUSTOMER_PARAM, PROXY_KID_PARAM, PROXY_MAX_SKEW_SECONDS, PROXY_NONCE_PARAM, PROXY_SHOP_PARAM, PROXY_SIGNATURE_PARAM, PROXY_SURFACE_PARAM, PROXY_TIMESTAMP_PARAM, type ProxyFailure, type ProxyQuery, type ProxyRequestOptions, type ProxyResponder, type ProxyStoreOptions, type ProxySurface, type ProxyVerification, type ProxyVerifyOptions, signProxyQuery, verifyProxyDelivery, verifyProxyQuery, verifyProxyQueryDetailed, } from "./proxy.js";
|
|
23
24
|
export { type ResyncListItem, type ResyncOptions, type ResyncResult, resyncFromQueek, } from "./resync.js";
|
|
24
25
|
export { APP_SCOPE_REQUIRED_CODE, APP_SCOPE_UNDECLARED_CODE, AppScopeRequiredError, appScopeRequiredFromError, buildScopeRequestLink, createInstallationScopesClient, type InstallationScopes, type InstallationScopesClient, type InstallationScopesClientOptions, InvalidScopesError, isAppScopeRequired, MAX_SCOPE_LENGTH, MAX_SCOPE_LIST, normaliseScopeList, type QueryScopesOptions, type RequestScopesLinkOptions, type RevokeScopesOptions, revokeScopesPath, type ScopeRequestLinkOptions, splitInstallationScopes, } from "./scopes.js";
|
|
25
26
|
export { MAX_TIMESTAMP_SKEW_SECONDS, SECRET_PREFIX, type SignatureFailure, type SignatureInput, secretKeyBytes, signQueekPayload, type VerifyOptions, verifyQueekSignature, verifyQueekSignatureDetailed, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, } from "./signatures.js";
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,12 @@ export { apiHostsFromEnv, createQueekClient, DEFAULT_API_HOSTS, DEFAULT_MAX_RETR
|
|
|
15
15
|
export { collectedDefinitions, createRecord, sendAlert, setSetupNotice, } from "./collected.js";
|
|
16
16
|
export { decryptSecret, encryptSecret, parseStoreKey, storeKeyFingerprint, } from "./crypto.js";
|
|
17
17
|
export { decodeBody, readHeader, } from "./delivery.js";
|
|
18
|
-
export { APP_SOURCE, BRIDGE_VERSION, buildReadyMessage, capText, clipOutbound, DASHBOARD_SOURCE, 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, parseInboundMessage, parseOutboundMessage, sendAck, sendBridgeMessage, sendNavigated, sendOpen, sendPickResource, sendReady, sendResize, sendSaveBar, sendTitle, sendToast, } from "./frame.js";
|
|
18
|
+
export { APP_SOURCE, BRIDGE_VERSION, buildReadyMessage, capText, clipOutbound, DASHBOARD_SOURCE, 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, parseInboundMessage, parseOutboundMessage, sendAck, sendBridgeMessage, sendNavigated, sendOpen, sendPickResource, sendReady, sendResize, sendSaveBar, sendTitle, sendToast, } from "./frame.js";
|
|
19
19
|
export { INSTALL_EVENT, RESYNC_EVENT, SCOPES_UPDATE_EVENT, SETTINGS_EVENT, UNINSTALL_EVENT, } from "./handoff.js";
|
|
20
20
|
export { buildInstallationRecord, handleInstallDelivery, handleInstallRequest, installationScopesEqual, saveResyncedInstallation, saveScopesUpdateInstallation, } from "./install-handlers.js";
|
|
21
21
|
export { createLogger, REDACTED, } from "./logger.js";
|
|
22
|
-
export {
|
|
22
|
+
export { aiEditableSettingKeys, isReservedAiInputKey, MANIFEST_AI_ACTION_INPUT_TYPES, MANIFEST_AI_OUTPUT_TYPES, MANIFEST_AI_RESERVED_INPUT_KEYS, MANIFEST_AI_RESERVED_INPUT_PREFIX, MANIFEST_AI_TOOL_INPUT_TYPES, MANIFEST_SETTING_TYPES, } from "./manifest.js";
|
|
23
|
+
export { buildProxyCanonicalString, handleAiToolRequest, handleProxyRequest, PROXY_CUSTOMER_PARAM, PROXY_KID_PARAM, PROXY_MAX_SKEW_SECONDS, PROXY_NONCE_PARAM, PROXY_SHOP_PARAM, PROXY_SIGNATURE_PARAM, PROXY_SURFACE_PARAM, PROXY_TIMESTAMP_PARAM, signProxyQuery, verifyProxyDelivery, verifyProxyQuery, verifyProxyQueryDetailed, } from "./proxy.js";
|
|
23
24
|
export { resyncFromQueek, } from "./resync.js";
|
|
24
25
|
export { APP_SCOPE_REQUIRED_CODE, APP_SCOPE_UNDECLARED_CODE, AppScopeRequiredError, appScopeRequiredFromError, buildScopeRequestLink, createInstallationScopesClient, InvalidScopesError, isAppScopeRequired, MAX_SCOPE_LENGTH, MAX_SCOPE_LIST, normaliseScopeList, revokeScopesPath, splitInstallationScopes, } from "./scopes.js";
|
|
25
26
|
export { MAX_TIMESTAMP_SKEW_SECONDS, SECRET_PREFIX, secretKeyBytes, signQueekPayload, verifyQueekSignature, verifyQueekSignatureDetailed, WEBHOOK_ID_HEADER, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER, } from "./signatures.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);
|