@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/client.js
CHANGED
|
@@ -88,11 +88,16 @@ export function resolveApiBase(raw, allowedApiHosts = []) {
|
|
|
88
88
|
const merchantPath = path.endsWith(MERCHANT_API_PATH) ? path : `${path}${MERCHANT_API_PATH}`;
|
|
89
89
|
return `https://${host}${url.port !== "" ? `:${url.port}` : ""}${merchantPath}`;
|
|
90
90
|
}
|
|
91
|
+
/** Default time a single HTTP attempt may take before it fails with `request_timeout`. */
|
|
92
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
93
|
+
/** Default ceiling for honouring a 429 `Retry-After` inside `requestWithRetry()`. */
|
|
94
|
+
export const DEFAULT_MAX_RETRY_AFTER_MS = 30_000;
|
|
91
95
|
const WRITE_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
92
96
|
/** True for HTTP methods that carry an `Idempotency-Key` (POST/PUT/PATCH/DELETE). */
|
|
93
97
|
export function isWriteMethod(method) {
|
|
94
98
|
return WRITE_METHODS.has(method.toUpperCase());
|
|
95
99
|
}
|
|
100
|
+
/** A fresh `Idempotency-Key` value (a random UUID). */
|
|
96
101
|
export function newIdempotencyKey() {
|
|
97
102
|
return randomUUID();
|
|
98
103
|
}
|
|
@@ -110,6 +115,28 @@ function parseRetryAfterMs(value) {
|
|
|
110
115
|
}
|
|
111
116
|
return null;
|
|
112
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* The one error every Queek HTTP call throws. Switch on `code`, never on
|
|
120
|
+
* `message`.
|
|
121
|
+
*
|
|
122
|
+
* Codes that come from Queek are the API's own (`validation_failed`,
|
|
123
|
+
* `insufficient_scope`, `too_many_requests`, ...). Codes the SDK raises when
|
|
124
|
+
* no usable HTTP answer arrived (`status` is 0 for the first three):
|
|
125
|
+
* `network_error`, `request_timeout`, `request_aborted`, and
|
|
126
|
+
* `invalid_response` (an HTTP success whose body was not JSON).
|
|
127
|
+
*
|
|
128
|
+
* `requestId` matches the `X-Request-Id` response header: quote it to Queek
|
|
129
|
+
* support.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* try {
|
|
133
|
+
* await client.request("POST", "/orders", { body });
|
|
134
|
+
* } catch (error) {
|
|
135
|
+
* if (error instanceof QueekApiError && error.code === "validation_failed") {
|
|
136
|
+
* console.error(error.field, error.errors, error.requestId);
|
|
137
|
+
* }
|
|
138
|
+
* }
|
|
139
|
+
*/
|
|
113
140
|
export class QueekApiError extends Error {
|
|
114
141
|
status;
|
|
115
142
|
code;
|
|
@@ -120,7 +147,7 @@ export class QueekApiError extends Error {
|
|
|
120
147
|
/** Set on 429s (and only there): how long to wait before retrying. */
|
|
121
148
|
retryAfterMs;
|
|
122
149
|
constructor(details) {
|
|
123
|
-
super(details.message);
|
|
150
|
+
super(details.message, details.cause === undefined ? undefined : { cause: details.cause });
|
|
124
151
|
this.name = "QueekApiError";
|
|
125
152
|
this.status = details.status;
|
|
126
153
|
this.code = details.code;
|
|
@@ -136,6 +163,62 @@ export class QueekApiError extends Error {
|
|
|
136
163
|
get isAuthFailure() {
|
|
137
164
|
return this.status === 401 || this.status === 403;
|
|
138
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* True when sending the same call again can succeed without any change:
|
|
168
|
+
* rate limits, gateway failures (502, 503, 504), timeouts and network
|
|
169
|
+
* errors. `requestWithRetry()` retries exactly these. A caller abort is
|
|
170
|
+
* never retryable.
|
|
171
|
+
*/
|
|
172
|
+
get isRetryable() {
|
|
173
|
+
return (this.status === 429 ||
|
|
174
|
+
this.status === 502 ||
|
|
175
|
+
this.status === 503 ||
|
|
176
|
+
this.status === 504 ||
|
|
177
|
+
this.code === "network_error" ||
|
|
178
|
+
this.code === "request_timeout");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The error for a call that never produced an HTTP answer. A caller abort is
|
|
183
|
+
* `request_aborted`, the client timeout is `request_timeout`, everything else
|
|
184
|
+
* (DNS, refused or reset connection, TLS) is `network_error`. The original
|
|
185
|
+
* error stays on `cause`.
|
|
186
|
+
*/
|
|
187
|
+
export function transportError(cause, signal) {
|
|
188
|
+
const name = cause instanceof Error ? cause.name : "";
|
|
189
|
+
const reason = cause instanceof Error && cause.message !== "" ? ` (${cause.message})` : "";
|
|
190
|
+
if (name === "TimeoutError") {
|
|
191
|
+
return new QueekApiError({
|
|
192
|
+
status: 0,
|
|
193
|
+
code: "request_timeout",
|
|
194
|
+
message: "Queek did not answer before the request timeout. Retry, raise `timeoutMs`, or use requestWithRetry().",
|
|
195
|
+
cause,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (signal?.aborted === true) {
|
|
199
|
+
return new QueekApiError({
|
|
200
|
+
status: 0,
|
|
201
|
+
code: "request_aborted",
|
|
202
|
+
message: "The request was aborted by its AbortSignal before Queek answered.",
|
|
203
|
+
cause,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return new QueekApiError({
|
|
207
|
+
status: 0,
|
|
208
|
+
code: "network_error",
|
|
209
|
+
message: `Could not reach Queek${reason}. Check the network connection and the api_base host, then retry; requestWithRetry() does this for you.`,
|
|
210
|
+
cause,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Combine a caller signal with the per-attempt timeout. Returns the caller
|
|
215
|
+
* signal untouched when `timeoutMs` is 0 or negative.
|
|
216
|
+
*/
|
|
217
|
+
export function requestSignal(signal, timeoutMs) {
|
|
218
|
+
if (!(timeoutMs > 0))
|
|
219
|
+
return signal;
|
|
220
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
221
|
+
return signal === undefined ? timeout : AbortSignal.any([signal, timeout]);
|
|
139
222
|
}
|
|
140
223
|
/**
|
|
141
224
|
* Build a `QueekApiError` from a status + parsed body + headers. Shared
|
|
@@ -153,7 +236,8 @@ export function queekApiErrorFromResponse(status, body, headers) {
|
|
|
153
236
|
return undefined;
|
|
154
237
|
};
|
|
155
238
|
const code = firstString(envelope.code, record.error_code) ?? (status === 429 ? "too_many_requests" : "server_error");
|
|
156
|
-
const message = firstString(envelope.message, record.message) ??
|
|
239
|
+
const message = firstString(envelope.message, record.message) ??
|
|
240
|
+
`Queek answered HTTP ${status} without an error message. Retry, and quote the request id if it persists.`;
|
|
157
241
|
const details = {
|
|
158
242
|
status,
|
|
159
243
|
code,
|
|
@@ -181,14 +265,56 @@ export function queekApiErrorFromResponse(status, body, headers) {
|
|
|
181
265
|
function defaultSleep(ms) {
|
|
182
266
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
183
267
|
}
|
|
184
|
-
|
|
185
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Milliseconds to wait before the next attempt, or `null` when the error
|
|
270
|
+
* must be thrown instead of retried (not retryable, or a `Retry-After`
|
|
271
|
+
* longer than `maxRetryAfterMs`). Backoff is exponential with equal jitter
|
|
272
|
+
* (half fixed, half random) so many clients do not retry in lockstep.
|
|
273
|
+
*/
|
|
274
|
+
function retryDelayMs(error, attempt, options) {
|
|
275
|
+
if (!error.isRetryable)
|
|
276
|
+
return null;
|
|
277
|
+
if (error.isRateLimited && error.retryAfterMs !== undefined) {
|
|
278
|
+
return error.retryAfterMs <= (options.maxRetryAfterMs ?? DEFAULT_MAX_RETRY_AFTER_MS)
|
|
279
|
+
? error.retryAfterMs
|
|
280
|
+
: null;
|
|
281
|
+
}
|
|
282
|
+
const ceiling = Math.min(options.maxDelayMs ?? 5000, (options.baseDelayMs ?? 250) * 2 ** attempt);
|
|
283
|
+
const random = options.random ?? Math.random;
|
|
284
|
+
return Math.floor(ceiling / 2 + (random() * ceiling) / 2);
|
|
186
285
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
286
|
+
/**
|
|
287
|
+
* The retry loop both clients share. `run` receives the idempotency key to
|
|
288
|
+
* send (generated ONCE for writes when the caller gave none), so every
|
|
289
|
+
* attempt of one write carries the same key.
|
|
290
|
+
*/
|
|
291
|
+
export async function runWithRetry(method, options, run) {
|
|
292
|
+
const maxAttempts = Math.max(1, Math.floor(options.maxAttempts ?? 3));
|
|
293
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
294
|
+
const idempotencyKey = options.idempotencyKey ?? (isWriteMethod(method) ? newIdempotencyKey() : undefined);
|
|
295
|
+
for (let attempt = 0;; attempt++) {
|
|
296
|
+
try {
|
|
297
|
+
return await run(idempotencyKey);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
if (!(error instanceof QueekApiError) || attempt >= maxAttempts - 1)
|
|
301
|
+
throw error;
|
|
302
|
+
const delay = retryDelayMs(error, attempt, options);
|
|
303
|
+
if (delay === null)
|
|
304
|
+
throw error;
|
|
305
|
+
await sleep(delay);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
191
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Create a client for the Merchant API with one static installation key.
|
|
311
|
+
* Most apps want `createInstallationClient()` instead, which mints and
|
|
312
|
+
* refreshes the key for you.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* const client = createQueekClient({ apiBase: install.api_base, apiKey: process.env.QUEEK_KEY ?? "" });
|
|
316
|
+
* const store = await client.getStore();
|
|
317
|
+
*/
|
|
192
318
|
export function createQueekClient(clientOptions) {
|
|
193
319
|
const base = resolveApiBase(clientOptions.apiBase, clientOptions.allowedApiHosts ?? []);
|
|
194
320
|
const fetchImpl = clientOptions.fetchImpl ?? fetch;
|
|
@@ -215,46 +341,23 @@ export function createQueekClient(clientOptions) {
|
|
|
215
341
|
if (isWriteMethod(upper)) {
|
|
216
342
|
headers.set("Idempotency-Key", options.idempotencyKey ?? newIdempotencyKey());
|
|
217
343
|
}
|
|
344
|
+
const signal = requestSignal(options.signal, options.timeoutMs ?? clientOptions.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS);
|
|
218
345
|
let response;
|
|
346
|
+
let text;
|
|
219
347
|
try {
|
|
220
|
-
response = await fetchImpl(url.toString(), { method: upper, headers, body, signal
|
|
348
|
+
response = await fetchImpl(url.toString(), { method: upper, headers, body, signal });
|
|
349
|
+
text = await response.text();
|
|
221
350
|
}
|
|
222
351
|
catch (cause) {
|
|
223
|
-
throw
|
|
224
|
-
status: 0,
|
|
225
|
-
code: "network_error",
|
|
226
|
-
message: cause instanceof Error ? `Could not reach Queek: ${cause.message}` : "Could not reach Queek.",
|
|
227
|
-
});
|
|
352
|
+
throw transportError(cause, options.signal);
|
|
228
353
|
}
|
|
229
|
-
const text = await response.text();
|
|
230
|
-
const parsed = text === "" ? null : tryParseJson(text);
|
|
231
354
|
if (!response.ok) {
|
|
232
|
-
throw queekApiErrorFromResponse(response.status,
|
|
355
|
+
throw queekApiErrorFromResponse(response.status, text === "" ? null : tryParseJson(text), response.headers);
|
|
233
356
|
}
|
|
234
|
-
return
|
|
357
|
+
return parseSuccessBody(response, text);
|
|
235
358
|
}
|
|
236
359
|
async function requestWithRetry(method, path, options = {}) {
|
|
237
|
-
|
|
238
|
-
const baseDelayMs = options.baseDelayMs ?? 250;
|
|
239
|
-
const maxDelayMs = options.maxDelayMs ?? 5000;
|
|
240
|
-
const sleep = options.sleep ?? defaultSleep;
|
|
241
|
-
// One key for every attempt: Queek replays same-key+same-body writes
|
|
242
|
-
// instead of duplicating them, so retrying a write is safe.
|
|
243
|
-
const idempotencyKey = options.idempotencyKey ?? (isWriteMethod(method) ? newIdempotencyKey() : undefined);
|
|
244
|
-
let lastError;
|
|
245
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
246
|
-
try {
|
|
247
|
-
return await request(method, path, { ...options, idempotencyKey });
|
|
248
|
-
}
|
|
249
|
-
catch (error) {
|
|
250
|
-
lastError = error;
|
|
251
|
-
const retryable = error instanceof QueekApiError && isRetryable(error);
|
|
252
|
-
if (!retryable || attempt === maxAttempts - 1)
|
|
253
|
-
throw error;
|
|
254
|
-
await sleep(retryDelayMs(error, attempt, baseDelayMs, maxDelayMs));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
throw lastError;
|
|
360
|
+
return runWithRetry(method, options, (idempotencyKey) => request(method, path, { ...options, idempotencyKey }));
|
|
258
361
|
}
|
|
259
362
|
return {
|
|
260
363
|
getStore: (signal) => request("GET", "/store", { signal }),
|
|
@@ -262,7 +365,11 @@ export function createQueekClient(clientOptions) {
|
|
|
262
365
|
requestWithRetry,
|
|
263
366
|
};
|
|
264
367
|
}
|
|
265
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Parse a non-JSON-safe error body: JSON when it parses, otherwise the first
|
|
370
|
+
* 500 characters as the error `message`.
|
|
371
|
+
*/
|
|
372
|
+
export function tryParseJson(text) {
|
|
266
373
|
try {
|
|
267
374
|
return JSON.parse(text);
|
|
268
375
|
}
|
|
@@ -270,4 +377,54 @@ function tryParseJson(text) {
|
|
|
270
377
|
return { message: text.slice(0, 500) };
|
|
271
378
|
}
|
|
272
379
|
}
|
|
273
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Parse an HTTP success body. An empty body is `null`; a body that is not
|
|
382
|
+
* JSON is an `invalid_response` error, never a silently mistyped value.
|
|
383
|
+
*/
|
|
384
|
+
export function parseSuccessBody(response, text) {
|
|
385
|
+
if (text === "")
|
|
386
|
+
return null;
|
|
387
|
+
try {
|
|
388
|
+
return JSON.parse(text);
|
|
389
|
+
}
|
|
390
|
+
catch (cause) {
|
|
391
|
+
throw new QueekApiError({
|
|
392
|
+
status: response.status,
|
|
393
|
+
code: "invalid_response",
|
|
394
|
+
message: `Queek answered HTTP ${response.status} with a body that is not JSON. Retry; if it persists, quote the request id to Queek support.`,
|
|
395
|
+
requestId: response.headers.get("x-request-id") ?? undefined,
|
|
396
|
+
cause,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Iterate every row of a cursor-paginated list (`data`, `has_more`,
|
|
402
|
+
* `next_cursor`, driven by `starting_after`), fetching one page at a time
|
|
403
|
+
* with `requestWithRetry()`. Stops when `has_more` is false.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* for await (const order of paginate<{ p_id: string }>(client, "/orders", { limit: 50 })) {
|
|
407
|
+
* console.log(order.p_id);
|
|
408
|
+
* }
|
|
409
|
+
*/
|
|
410
|
+
export async function* paginate(client, path, options = {}) {
|
|
411
|
+
let cursor;
|
|
412
|
+
while (true) {
|
|
413
|
+
const page = await client.requestWithRetry("GET", path, {
|
|
414
|
+
...options.retry,
|
|
415
|
+
query: { ...options.query, limit: options.limit, starting_after: cursor },
|
|
416
|
+
signal: options.signal,
|
|
417
|
+
});
|
|
418
|
+
yield* page.data;
|
|
419
|
+
if (!page.has_more)
|
|
420
|
+
return;
|
|
421
|
+
if (typeof page.next_cursor !== "string" || page.next_cursor === "" || page.next_cursor === cursor) {
|
|
422
|
+
throw new QueekApiError({
|
|
423
|
+
status: 200,
|
|
424
|
+
code: "invalid_response",
|
|
425
|
+
message: `Queek reported more rows for ${path} but sent no new next_cursor, so paging stopped. Retry; if it persists, quote the request id to Queek support.`,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
cursor = page.next_cursor;
|
|
429
|
+
}
|
|
430
|
+
}
|
package/dist/collected.d.ts
CHANGED
package/dist/collected.js
CHANGED
package/dist/crypto.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse `APP_ENCRYPTION_KEY` (base64 or hex of 32 bytes, or a raw 32-character
|
|
3
|
+
* string) into the AES-256 key. Prefer random bytes: a raw string key is only
|
|
4
|
+
* as strong as the text you chose. Throws with a fix when the value is
|
|
5
|
+
* missing or the wrong length.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const key = parseStoreKey(process.env.APP_ENCRYPTION_KEY);
|
|
9
|
+
*/
|
|
1
10
|
export declare function parseStoreKey(raw: string | undefined): Buffer;
|
|
2
11
|
/** Non-secret fingerprint for logs (first 4 bytes, hex). Reveals nothing usable. */
|
|
3
12
|
export declare function storeKeyFingerprint(key: Buffer): string;
|
|
13
|
+
/** Encrypt `plaintext` with AES-256-GCM into a `v1.<iv>.<ciphertext+tag>` envelope. */
|
|
4
14
|
export declare function encryptSecret(plaintext: string, key: Buffer): string;
|
|
15
|
+
/** Decrypt an envelope made by {@link encryptSecret}. Throws a plain `Error` (with `cause`) on a wrong key or altered value. */
|
|
5
16
|
export declare function decryptSecret(envelope: string, key: Buffer): string;
|
|
6
|
-
//# sourceMappingURL=crypto.d.ts.map
|
package/dist/crypto.js
CHANGED
|
@@ -11,6 +11,15 @@ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
|
11
11
|
*/
|
|
12
12
|
const ENVELOPE_VERSION = "v1";
|
|
13
13
|
const IV_BYTES = 12;
|
|
14
|
+
/**
|
|
15
|
+
* Parse `APP_ENCRYPTION_KEY` (base64 or hex of 32 bytes, or a raw 32-character
|
|
16
|
+
* string) into the AES-256 key. Prefer random bytes: a raw string key is only
|
|
17
|
+
* as strong as the text you chose. Throws with a fix when the value is
|
|
18
|
+
* missing or the wrong length.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const key = parseStoreKey(process.env.APP_ENCRYPTION_KEY);
|
|
22
|
+
*/
|
|
14
23
|
export function parseStoreKey(raw) {
|
|
15
24
|
if (!raw || raw.trim() === "") {
|
|
16
25
|
throw new Error("Missing store encryption key: set APP_ENCRYPTION_KEY to base64 or hex of 32 random bytes (`queek app dev` mints one into .queek/.env.local on first run; otherwise generate with node -e \"console.log(require('node:crypto').randomBytes(32).toString('base64'))\" and keep it in your secrets manager — key + DB loss = working data lost).");
|
|
@@ -38,7 +47,7 @@ export function parseStoreKey(raw) {
|
|
|
38
47
|
candidates.push(Buffer.from(value, "utf8"));
|
|
39
48
|
const key = candidates.find((candidate) => candidate.length === 32);
|
|
40
49
|
if (!key) {
|
|
41
|
-
throw new Error("Invalid APP_ENCRYPTION_KEY: must decode to exactly 32 bytes (base64 or hex).");
|
|
50
|
+
throw new Error("Invalid APP_ENCRYPTION_KEY: it must decode to exactly 32 bytes (base64 or hex). Generate one with `node -e \"console.log(require('node:crypto').randomBytes(32).toString('base64'))\"`.");
|
|
42
51
|
}
|
|
43
52
|
return key;
|
|
44
53
|
}
|
|
@@ -46,31 +55,37 @@ export function parseStoreKey(raw) {
|
|
|
46
55
|
export function storeKeyFingerprint(key) {
|
|
47
56
|
return key.subarray(0, 4).toString("hex");
|
|
48
57
|
}
|
|
58
|
+
/** Encrypt `plaintext` with AES-256-GCM into a `v1.<iv>.<ciphertext+tag>` envelope. */
|
|
49
59
|
export function encryptSecret(plaintext, key) {
|
|
50
60
|
if (key.length !== 32)
|
|
51
|
-
throw new Error(
|
|
61
|
+
throw new Error(`Encryption key must be exactly 32 bytes, got ${key.length}. Build it with parseStoreKey().`);
|
|
52
62
|
const iv = randomBytes(IV_BYTES);
|
|
53
63
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
54
64
|
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
55
65
|
const tag = cipher.getAuthTag();
|
|
56
66
|
return [ENVELOPE_VERSION, iv.toString("base64"), Buffer.concat([ciphertext, tag]).toString("base64")].join(".");
|
|
57
67
|
}
|
|
68
|
+
/** Decrypt an envelope made by {@link encryptSecret}. Throws a plain `Error` (with `cause`) on a wrong key or altered value. */
|
|
58
69
|
export function decryptSecret(envelope, key) {
|
|
59
70
|
if (key.length !== 32)
|
|
60
|
-
throw new Error(
|
|
71
|
+
throw new Error(`Encryption key must be exactly 32 bytes, got ${key.length}. Build it with parseStoreKey().`);
|
|
61
72
|
const parts = envelope.split(".");
|
|
62
73
|
if (parts.length !== 3 || parts[0] !== ENVELOPE_VERSION) {
|
|
63
|
-
throw new Error("Unrecognized secret envelope
|
|
74
|
+
throw new Error("Unrecognized secret envelope: expected `v1.<iv>.<ciphertext>`. The stored value was not written by this SDK or was truncated.");
|
|
64
75
|
}
|
|
65
76
|
const iv = Buffer.from(parts[1], "base64");
|
|
66
77
|
const raw = Buffer.from(parts[2], "base64");
|
|
67
78
|
if (iv.length !== IV_BYTES || raw.length < 17) {
|
|
68
|
-
throw new Error("Corrupt secret envelope.");
|
|
79
|
+
throw new Error("Corrupt secret envelope: the IV or ciphertext has the wrong length. Restore the row from a backup.");
|
|
69
80
|
}
|
|
70
81
|
const tag = raw.subarray(raw.length - 16);
|
|
71
82
|
const ciphertext = raw.subarray(0, raw.length - 16);
|
|
72
83
|
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
73
84
|
decipher.setAuthTag(tag);
|
|
74
|
-
|
|
85
|
+
try {
|
|
86
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
87
|
+
}
|
|
88
|
+
catch (cause) {
|
|
89
|
+
throw new Error("Could not decrypt a stored secret: it was written with a different APP_ENCRYPTION_KEY, or the value was changed. Restore the original key; without it the secret cannot be recovered.", { cause });
|
|
90
|
+
}
|
|
75
91
|
}
|
|
76
|
-
//# sourceMappingURL=crypto.js.map
|
package/dist/delivery.d.ts
CHANGED
|
@@ -39,6 +39,23 @@ export interface DeliveryResult {
|
|
|
39
39
|
export declare function readHeader(headers: CoreHeaders, name: string): string | undefined;
|
|
40
40
|
/** Decode untouched body bytes to the exact string Queek signed. */
|
|
41
41
|
export declare function decodeBody(rawBody: Uint8Array | Buffer | string): string;
|
|
42
|
+
/** Where a callback failed, passed to `onError`. */
|
|
43
|
+
export interface DeliveryFailure {
|
|
44
|
+
/** `install`, `uninstall`, `settings`, `scopes_update` or `webhook`. */
|
|
45
|
+
stage: string;
|
|
46
|
+
/** The `webhook-id` header of the delivery that failed. */
|
|
47
|
+
eventId: string;
|
|
48
|
+
/** The topic, for webhook deliveries. */
|
|
49
|
+
topic?: string;
|
|
50
|
+
}
|
|
51
|
+
/** Receives the error a callback threw. The delivery still answers 500 so Queek retries. */
|
|
52
|
+
export type DeliveryErrorHandler = (error: unknown, failure: DeliveryFailure) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Surface the error a callback threw, so a 500 is never a mystery. Calls the
|
|
55
|
+
* app's `onError` when given, otherwise logs the error name and message
|
|
56
|
+
* (secrets redacted) to stderr. A throwing `onError` cannot change the
|
|
57
|
+
* response.
|
|
58
|
+
*/
|
|
59
|
+
export declare function reportDeliveryFailure(onError: DeliveryErrorHandler | undefined, error: unknown, failure: DeliveryFailure): void;
|
|
42
60
|
/** Build the layer-2 `Response` from a layer-1 result. */
|
|
43
61
|
export declare function toResponse(result: DeliveryResult): Response;
|
|
44
|
-
//# sourceMappingURL=delivery.d.ts.map
|
package/dist/delivery.js
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Shared plumbing for the framework-free delivery core (layer 1).
|
|
3
|
-
*
|
|
4
|
-
* Layer 1 functions take plain data — untouched body bytes plus headers —
|
|
5
|
-
* and return plain `{ status, body }` results. They reference zero
|
|
6
|
-
* request/response types (no `Request`, no framework), so any framework
|
|
7
|
-
* that can hand over the raw body bytes can use them. Layer 2
|
|
8
|
-
* (`handleInstallRequest` / `handleWebhookRequest`) adapts the Web
|
|
9
|
-
* standard onto this core; `@usequeek/app-sdk/hono` adapts Hono onto
|
|
10
|
-
* layer 2.
|
|
11
|
-
*
|
|
12
|
-
* The raw-body rule: `rawBody` must be the exact bytes Queek signed (the
|
|
13
|
-
* Standard Webhooks signature covers `{id}.{timestamp}.{body}`). Never pass
|
|
14
|
-
* a parsed-then-restringified body — JSON re-serialization changes bytes
|
|
15
|
-
* (spacing, key order) and the signature will not verify.
|
|
16
|
-
*/
|
|
1
|
+
import { createLogger } from "./logger.js";
|
|
17
2
|
/** Case-insensitive header lookup across `Headers` and plain (possibly lowercased, possibly array-valued) objects. */
|
|
18
3
|
export function readHeader(headers, name) {
|
|
19
4
|
if (headers instanceof Headers) {
|
|
@@ -35,8 +20,32 @@ export function decodeBody(rawBody) {
|
|
|
35
20
|
return rawBody;
|
|
36
21
|
return new TextDecoder("utf-8").decode(rawBody);
|
|
37
22
|
}
|
|
23
|
+
let defaultLogger = null;
|
|
24
|
+
/**
|
|
25
|
+
* Surface the error a callback threw, so a 500 is never a mystery. Calls the
|
|
26
|
+
* app's `onError` when given, otherwise logs the error name and message
|
|
27
|
+
* (secrets redacted) to stderr. A throwing `onError` cannot change the
|
|
28
|
+
* response.
|
|
29
|
+
*/
|
|
30
|
+
export function reportDeliveryFailure(onError, error, failure) {
|
|
31
|
+
try {
|
|
32
|
+
if (onError) {
|
|
33
|
+
onError(error, failure);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
defaultLogger ??= createLogger({ service: "queek-app-sdk" });
|
|
37
|
+
defaultLogger.error("queek delivery callback failed; answering 500 so Queek retries", {
|
|
38
|
+
stage: failure.stage,
|
|
39
|
+
event_id: failure.eventId,
|
|
40
|
+
topic: failure.topic,
|
|
41
|
+
error: error instanceof Error ? `${error.name}: ${error.message}` : String(error),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Reporting must never change the HTTP answer.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
38
48
|
/** Build the layer-2 `Response` from a layer-1 result. */
|
|
39
49
|
export function toResponse(result) {
|
|
40
50
|
return Response.json(result.body, { status: result.status, headers: result.headers });
|
|
41
51
|
}
|
|
42
|
-
//# sourceMappingURL=delivery.js.map
|
package/dist/frame.d.ts
CHANGED
|
@@ -284,4 +284,3 @@ export declare function listenToDashboard(options: FrameBridgeOptions): () => vo
|
|
|
284
284
|
* blocked open target, non-product pick) — `sendBridgeMessage` skips null.
|
|
285
285
|
*/
|
|
286
286
|
export declare function clipOutbound(message: AppOutboundMessage): AppOutboundMessage | null;
|
|
287
|
-
//# sourceMappingURL=frame.d.ts.map
|
package/dist/frame.js
CHANGED
package/dist/handoff.d.ts
CHANGED
|
@@ -137,4 +137,3 @@ export interface ScopesUpdateData {
|
|
|
137
137
|
}
|
|
138
138
|
export type ScopesUpdateEnvelope = HandoffEnvelope<typeof SCOPES_UPDATE_EVENT, ScopesUpdateData>;
|
|
139
139
|
export type HandoffEnvelopeAny = InstallEnvelope | UninstallEnvelope | SettingsEnvelope | ResyncEnvelope | ScopesUpdateEnvelope;
|
|
140
|
-
//# sourceMappingURL=handoff.d.ts.map
|
package/dist/handoff.js
CHANGED
package/dist/hono.d.ts
CHANGED
package/dist/hono.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
export { API_KEY_EXPIRED_CODE, API_KEY_REVOKED_CODE, APP_INSTALLATION_GONE_CODE, APP_INSTALLATION_PENDING_CODE, APP_JWT_SKEW_SECONDS, APP_JWT_TTL_SECONDS, APP_PRIVATE_KEY_FORMS, APP_TOKEN_REVOKED_CODE, type AppCredential, AppMintHaltedError, decodePrivateKeyInput, INSUFFICIENT_SCOPE_CODE, INVALID_CLIENT_CODE, INVALID_CLIENT_KEY_CODE, InvalidAppCredentialError, isAppTokenRevoked, isInstallationGone, isInstallationPending, isInsufficientScope, isInvalidClient, isResyncCooldown, isTokenRefusal, loadAppCredential, MAX_MINT_ATTEMPTS, MINT_BACKOFF_BASE_MS, MINT_BACKOFF_MAX_MS, RESYNC_COOLDOWN_CODE, RETRY_JITTER_MAX_MS, type SignAppJwtOptions, signAppJwt, TOKEN_VALIDITY_SKEW_SECONDS, TOO_MANY_REQUESTS_CODE, UnknownInstallationError, } from "./app-auth.js";
|
|
12
12
|
export { type AuthFetchOptions, BRIDGE_TOKEN_TIMEOUT_MS, type InstalledAuth, installAuthFetch, LAUNCH_TOKEN_PARAM, readLaunchToken, stripLaunchToken, } from "./auth-fetch.js";
|
|
13
13
|
export { backgroundTaskCount, type CatchupFailure, type CatchupInstallation, type CatchupOptions, type CatchupResult, type DetachOptions, detach, runInstallationCatchup, } from "./background.js";
|
|
14
|
-
export { apiHostsFromEnv, createQueekClient, DEFAULT_API_HOSTS, devApiHostsFromEnv, InvalidApiBaseError, isWriteMethod, MERCHANT_API_PATH, type MerchantPaths, newIdempotencyKey, type OperationResponse, QueekApiError, type QueekClient, type QueekClientOptions, type QueekErrorDetails, queekApiErrorFromResponse, type RequestOptions, type ResolvedPaths, type RetryOptions, resolveApiBase, type StoreProfile, } from "./client.js";
|
|
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
17
|
export { type CoreDelivery, type CoreHeaders, type DeliveryResult, decodeBody, type InstallDelivery, readHeader, } from "./delivery.js";
|
|
@@ -27,4 +27,3 @@ export { createInstallationStore, createPostgresPool, defaultClearCachedTokenIfM
|
|
|
27
27
|
export { applyTheme, getThemeModeFromUrl, installThemeListener, rememberThemeMode, THEME_PARAM, THEME_STORAGE_KEY, type ThemeDocument, type ThemeDocumentElement, type ThemeListenerOptions, type ThemeStorage, themeBootstrapScript, } from "./theme.js";
|
|
28
28
|
export { APP_API_PATH, AppTokenProvider, type AppTokens, createAppTokenProvider, createInstallationClient, type InstallationClientOptions, mintPath, resolveAppApiBase, type TokenProviderOptions, } from "./tokens.js";
|
|
29
29
|
export { handleWebhookDelivery, handleWebhookRequest, QUEEK_TOPIC_HEADER, type QueekWebhookEnvelope, type SecretResolution, type WebhookHandlerContext, type WebhookHandlerFn, type WebhookHandlerOptions, } from "./webhooks.js";
|
|
30
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
export { API_KEY_EXPIRED_CODE, API_KEY_REVOKED_CODE, APP_INSTALLATION_GONE_CODE, APP_INSTALLATION_PENDING_CODE, APP_JWT_SKEW_SECONDS, APP_JWT_TTL_SECONDS, APP_PRIVATE_KEY_FORMS, APP_TOKEN_REVOKED_CODE, AppMintHaltedError, decodePrivateKeyInput, INSUFFICIENT_SCOPE_CODE, INVALID_CLIENT_CODE, INVALID_CLIENT_KEY_CODE, InvalidAppCredentialError, isAppTokenRevoked, isInstallationGone, isInstallationPending, isInsufficientScope, isInvalidClient, isResyncCooldown, isTokenRefusal, loadAppCredential, MAX_MINT_ATTEMPTS, MINT_BACKOFF_BASE_MS, MINT_BACKOFF_MAX_MS, RESYNC_COOLDOWN_CODE, RETRY_JITTER_MAX_MS, signAppJwt, TOKEN_VALIDITY_SKEW_SECONDS, TOO_MANY_REQUESTS_CODE, UnknownInstallationError, } from "./app-auth.js";
|
|
12
12
|
export { BRIDGE_TOKEN_TIMEOUT_MS, installAuthFetch, LAUNCH_TOKEN_PARAM, readLaunchToken, stripLaunchToken, } from "./auth-fetch.js";
|
|
13
13
|
export { backgroundTaskCount, detach, runInstallationCatchup, } from "./background.js";
|
|
14
|
-
export { apiHostsFromEnv, createQueekClient, DEFAULT_API_HOSTS, devApiHostsFromEnv, InvalidApiBaseError, isWriteMethod, MERCHANT_API_PATH, newIdempotencyKey, QueekApiError, queekApiErrorFromResponse, resolveApiBase, } from "./client.js";
|
|
14
|
+
export { apiHostsFromEnv, createQueekClient, DEFAULT_API_HOSTS, DEFAULT_MAX_RETRY_AFTER_MS, DEFAULT_REQUEST_TIMEOUT_MS, devApiHostsFromEnv, InvalidApiBaseError, isWriteMethod, MERCHANT_API_PATH, newIdempotencyKey, paginate, QueekApiError, queekApiErrorFromResponse, resolveApiBase, } from "./client.js";
|
|
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";
|
|
@@ -27,4 +27,3 @@ export { createInstallationStore, createPostgresPool, defaultClearCachedTokenIfM
|
|
|
27
27
|
export { applyTheme, getThemeModeFromUrl, installThemeListener, rememberThemeMode, THEME_PARAM, THEME_STORAGE_KEY, themeBootstrapScript, } from "./theme.js";
|
|
28
28
|
export { APP_API_PATH, AppTokenProvider, createAppTokenProvider, createInstallationClient, mintPath, resolveAppApiBase, } from "./tokens.js";
|
|
29
29
|
export { handleWebhookDelivery, handleWebhookRequest, QUEEK_TOPIC_HEADER, } from "./webhooks.js";
|
|
30
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DeliveryResult, type InstallDelivery } from "./delivery.js";
|
|
1
|
+
import { type DeliveryErrorHandler, type DeliveryResult, type InstallDelivery } from "./delivery.js";
|
|
2
2
|
import { type InstallData, type InstallEnvelope, type ScopesUpdateEnvelope, type SettingsEnvelope, type UninstallEnvelope } from "./handoff.js";
|
|
3
3
|
import type { InstallationRecord, InstallationStore } from "./store.js";
|
|
4
4
|
/**
|
|
@@ -49,12 +49,18 @@ export interface InstallCallbacks {
|
|
|
49
49
|
/** Defaults to refreshing the cached grant (dropping the cached token when it moved). */
|
|
50
50
|
onScopesUpdate?: (envelope: ScopesUpdateEnvelope) => Promise<void>;
|
|
51
51
|
}
|
|
52
|
+
/** Options for the install handoff handlers. `appSecret` and `store` are required; every callback has a safe default. */
|
|
52
53
|
export interface InstallHandlerOptions extends InstallCallbacks {
|
|
53
54
|
/** The app signing secret (`whsec_…`) from Queek registration. */
|
|
54
55
|
appSecret: string;
|
|
55
56
|
store: InstallationStore;
|
|
56
57
|
nowSeconds?: number;
|
|
57
58
|
maxSkewSeconds?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Called with the error a callback or store write threw, before the 500
|
|
61
|
+
* goes back. Without it the error is logged to stderr (secrets redacted).
|
|
62
|
+
*/
|
|
63
|
+
onError?: DeliveryErrorHandler;
|
|
58
64
|
}
|
|
59
65
|
/**
|
|
60
66
|
* Build the storable installation record from install `data`. Timestamps
|
|
@@ -93,12 +99,26 @@ export declare function saveResyncedInstallation(existing: InstallationRecord, d
|
|
|
93
99
|
* `uninstall`, or `settings`, taken from the input path's last segment)
|
|
94
100
|
* from plain data — no `Request`, no framework. Same options, same
|
|
95
101
|
* behaviour, same errors/status codes at every layer.
|
|
102
|
+
*
|
|
103
|
+
* Answers 401 for a missing header, stale timestamp or bad signature, 400
|
|
104
|
+
* for a body that is not the expected envelope, 409 for a repeated
|
|
105
|
+
* `webhook-id`, 404 for an unknown route, and 500 when your callback or the
|
|
106
|
+
* store write throws (Queek then retries).
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* const result = await handleInstallDelivery(
|
|
110
|
+
* { rawBody, headers, method: "POST", path: "/queek/install" },
|
|
111
|
+
* { appSecret: process.env.APP_SIGNING_SECRET ?? "", store },
|
|
112
|
+
* );
|
|
113
|
+
* // reply with result.status and result.body
|
|
96
114
|
*/
|
|
97
115
|
export declare function handleInstallDelivery(input: InstallDelivery, options: InstallHandlerOptions): Promise<DeliveryResult>;
|
|
98
116
|
/**
|
|
99
117
|
* Layer 2: the Web-standard wrapper, built ONLY on layer 1 — reads
|
|
100
118
|
* `await request.arrayBuffer()` + headers, calls the core, builds the
|
|
101
119
|
* `Response`.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* export const POST = (request: Request) => handleInstallRequest(request, { appSecret, store });
|
|
102
123
|
*/
|
|
103
124
|
export declare function handleInstallRequest(request: Request, options: InstallHandlerOptions): Promise<Response>;
|
|
104
|
-
//# sourceMappingURL=install-handlers.d.ts.map
|