@usequeek/app-sdk 0.5.1 → 0.6.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 +179 -12
- package/dist/app-auth.d.ts +9 -1
- package/dist/app-auth.d.ts.map +1 -1
- package/dist/app-auth.js +11 -1
- package/dist/app-auth.js.map +1 -1
- package/dist/auth-fetch.d.ts +91 -0
- package/dist/auth-fetch.d.ts.map +1 -0
- package/dist/auth-fetch.js +263 -0
- package/dist/auth-fetch.js.map +1 -0
- package/dist/browser.d.ts +25 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +27 -0
- package/dist/browser.js.map +1 -0
- package/dist/client.d.ts +32 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +6 -2
- package/dist/client.js.map +1 -1
- package/dist/frame.d.ts +233 -4
- package/dist/frame.d.ts.map +1 -1
- package/dist/frame.js +694 -19
- package/dist/frame.js.map +1 -1
- package/dist/handoff.d.ts +32 -2
- package/dist/handoff.d.ts.map +1 -1
- package/dist/handoff.js +2 -1
- package/dist/handoff.js.map +1 -1
- package/dist/hono.d.ts +13 -0
- package/dist/hono.d.ts.map +1 -1
- package/dist/hono.js +15 -0
- package/dist/hono.js.map +1 -1
- package/dist/index.d.ts +11 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/install-handlers.d.ts +33 -8
- package/dist/install-handlers.d.ts.map +1 -1
- package/dist/install-handlers.js +133 -15
- package/dist/install-handlers.js.map +1 -1
- package/dist/merchant-schema.d.ts +1550 -68
- package/dist/merchant-schema.d.ts.map +1 -1
- package/dist/merchant-schema.js +1 -1
- package/dist/proxy.d.ts +122 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +204 -0
- package/dist/proxy.js.map +1 -0
- package/dist/react.d.ts +78 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +262 -0
- package/dist/react.js.map +1 -0
- package/dist/scopes.d.ts +174 -0
- package/dist/scopes.d.ts.map +1 -0
- package/dist/scopes.js +297 -0
- package/dist/scopes.js.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/session.d.ts +17 -5
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +35 -16
- package/dist/session.js.map +1 -1
- package/dist/store.d.ts +33 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +114 -4
- package/dist/store.js.map +1 -1
- package/dist/theme.d.ts +100 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +162 -0
- package/dist/theme.js.map +1 -0
- package/dist/tokens.d.ts +30 -15
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +57 -19
- package/dist/tokens.js.map +1 -1
- package/dist/webhooks.d.ts.map +1 -1
- package/dist/webhooks.js +1 -1
- package/dist/webhooks.js.map +1 -1
- package/package.json +21 -5
- package/CHANGELOG.md +0 -162
- package/openapi/merchant.json +0 -21452
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @usequeek/app-sdk
|
|
2
2
|
|
|
3
|
+
> In development — not yet released. APIs may change before 1.0.
|
|
4
|
+
|
|
3
5
|
The SDK for building a Queek app on the public Merchant API and signed webhooks. Framework-agnostic Web-standard handlers for the install handoff and topic webhooks (`Request` in, `Response` out — use them from Next.js route handlers, Express, or any runtime), a typed Merchant API client (types generated from the live contract), GitHub-style app credentials (one asymmetric key per app, short-lived per-installation tokens minted on demand), resync recovery, and an encrypted installation store (SQLite for local/test, Postgres in production). Optional Hono wrappers live under `@usequeek/app-sdk/hono`.
|
|
4
6
|
|
|
5
7
|
```sh
|
|
@@ -8,6 +10,18 @@ npm i @usequeek/app-sdk pg
|
|
|
8
10
|
|
|
9
11
|
Requires Node `>=22.14`. `pg` is a regular dependency (the production store). `hono` is an optional peer — install it (`npm i hono`) only if you use the Hono wrappers.
|
|
10
12
|
|
|
13
|
+
## Which entry do I import?
|
|
14
|
+
|
|
15
|
+
| Import | For | Pulls in |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `@usequeek/app-sdk` (main) | Server / universal code: handlers, client, stores, verifiers | `node:crypto`, `pg` — NOT browser-bundlable |
|
|
18
|
+
| `@usequeek/app-sdk/server` | Session-token verifier (`verifySessionToken`) | Server-only (`jose`) — never import in a browser bundle |
|
|
19
|
+
| `@usequeek/app-sdk/hono` | Thin Hono wrappers (`hono` optional peer) | Server-only |
|
|
20
|
+
| `@usequeek/app-sdk/react` | React apps (`<QueekProvider>`, `useQueek()`; `react` optional peer) | Browser-safe (built on the same bridge modules as `/browser`) |
|
|
21
|
+
| `@usequeek/app-sdk/browser` | Plain-browser code: `installAuthFetch` + bridge/theme helpers, no framework | Browser-safe ONLY — importing these helpers from the main entry drags in the whole barrel and breaks Vite/Rollup builds |
|
|
22
|
+
|
|
23
|
+
Browser rule of thumb: if the code ships to the browser and does not need React, import it from `@usequeek/app-sdk/browser`. `installAuthFetch` is a browser-safe MODULE, but it is not browser-safe FROM THE MAIN ENTRY — the main barrel re-exports server modules (`app-auth` → `node:crypto`, `store` → `pg`), so a bundler resolving `installAuthFetch` from `@usequeek/app-sdk` still parses those Node-only files and fails.
|
|
24
|
+
|
|
11
25
|
The shape follows [`@shopify/shopify-api`](https://github.com/Shopify/shopify-app-js/blob/main/packages/apps/shopify-api/README.md): the core "doesn't rely on any specific framework, so you can include it alongside your preferred stack" (runtime differences are covered by adapters such as `@shopify/shopify-api/adapters/node`), and framework integrations are separate packages in the [shopify-app-js monorepo](https://github.com/Shopify/shopify-app-js) (e.g. `@shopify/shopify-app-express` and `@shopify/shopify-app-remix` build on `@shopify/shopify-api`).
|
|
12
26
|
|
|
13
27
|
## Example (any framework)
|
|
@@ -126,7 +140,7 @@ Signature verification covers the exact bytes Queek sent (`{id}.{timestamp}.{bod
|
|
|
126
140
|
|
|
127
141
|
### Body-size posture
|
|
128
142
|
|
|
129
|
-
The SDK itself imposes no maximum body size (`arrayBuffer` / `TextDecoder` / `JSON.parse` are unbounded
|
|
143
|
+
The SDK itself imposes no maximum body size (`arrayBuffer` / `TextDecoder` / `JSON.parse` are unbounded) — the cap belongs to the framework parser in front of it, which already rejects oversized bodies before the SDK ever sees them. Queek deliveries are small JSON payloads, so the framework defaults are plenty; if you set them explicitly, `express.raw({ type: "application/json", limit: "1mb" })` and Fastify's default `bodyLimit` of 1 MiB are the recommended ceilings. No SDK option is needed unless Queek ever ships large deliveries.
|
|
130
144
|
|
|
131
145
|
(If you already run Hono on Node, `@hono/node-server`'s `getRequestListener` bridges serving for you — but the adapters above need no extra dependency.)
|
|
132
146
|
|
|
@@ -162,8 +176,6 @@ app.route(
|
|
|
162
176
|
export default app;
|
|
163
177
|
```
|
|
164
178
|
|
|
165
|
-
> Migrating from 0.4.x: `import { createInstallHandlers } from "@usequeek/app-sdk/hono"` — the creators moved out of the root entry so non-Hono apps never install `hono`. The options objects are unchanged.
|
|
166
|
-
|
|
167
179
|
Every Merchant API call goes through `createInstallationClient({ installationId, apiBase, tokens })`,
|
|
168
180
|
which resolves the installation's token via `acquireToken()` and sends it as `X-Client-Key`.
|
|
169
181
|
|
|
@@ -247,6 +259,44 @@ settings) does NOT come back from resync: it needs the per-app `pg_dump` backup
|
|
|
247
259
|
(RPO ≤ 24 h) plus the `APP_ENCRYPTION_KEY` backup. Events missed beyond Queek's webhook
|
|
248
260
|
retries (~4 h) are gone; resync cannot backfill them. Full runbook: `docs/deploy.md` + O1.
|
|
249
261
|
|
|
262
|
+
## Data deletion
|
|
263
|
+
|
|
264
|
+
Treat the uninstall handoff (`app/uninstalled`) as the deletion trigger: the default
|
|
265
|
+
`onUninstall` already deletes the installation row (secrets, token cache, settings) —
|
|
266
|
+
keep that delete when you override it, and purge any app-side working data keyed by the
|
|
267
|
+
installation there too.
|
|
268
|
+
|
|
269
|
+
Unknown topics answer 200 without running a handler (forward compatibility, not consent):
|
|
270
|
+
that 200 must not swallow a future privacy topic. If Queek ships mandatory privacy
|
|
271
|
+
topics, register explicit handlers for them — an unhandled topic only reports
|
|
272
|
+
`unhandled: true`, it deletes nothing.
|
|
273
|
+
|
|
274
|
+
## Storefront app-proxy (signed reads)
|
|
275
|
+
|
|
276
|
+
Queek signs each storefront proxy fetch with the installation's `proxy_secret`
|
|
277
|
+
(`AppProxyService::signQuery`); verify before answering the shopper:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { handleProxyRequest } from "@usequeek/app-sdk";
|
|
281
|
+
|
|
282
|
+
export async function GET(request: Request) {
|
|
283
|
+
return handleProxyRequest(
|
|
284
|
+
request,
|
|
285
|
+
{ store, path: "/apps/booking/availability" }, // Queek-side canonical path, not the local route
|
|
286
|
+
({ installation, params }) =>
|
|
287
|
+
Response.json({ slots: slotsFor(params.date, installation.storePid) }),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The canonical string is `path + "\n" + shop + "\n" + ts + "\n" + sorted(k=v&...)`
|
|
293
|
+
(`sig` excluded), hex HMAC-SHA256 over the FULL `whsec_…` string — no base64 decode
|
|
294
|
+
step. `kid` routes to the installation whose `proxy_secret` verifies (previous-secret
|
|
295
|
+
grace: pass every active secret to `verifyProxyQuery`); timestamps skew at most 5
|
|
296
|
+
minutes, each `jti` is single-use, and only `GET` is served (phase 1 is read-only).
|
|
297
|
+
Hono: mount `createProxyHandler({ store, path, onVerified })` from
|
|
298
|
+
`@usequeek/app-sdk/hono`.
|
|
299
|
+
|
|
250
300
|
## API surface
|
|
251
301
|
|
|
252
302
|
- **app-auth** (`app-auth.ts`): `loadAppCredential` (`APP_SLUG`/`APP_KEY_ID`/`APP_PRIVATE_KEY`
|
|
@@ -267,8 +317,9 @@ retries (~4 h) are gone; resync cannot backfill them. Full runbook: `docs/deploy
|
|
|
267
317
|
- **verify** (`signatures.ts`): `verifyQueekSignature` — Standard Webhooks verification (`webhook-id`, `webhook-timestamp`, `webhook-signature` over `{id}.{timestamp}.{body}`, keyed by the decoded `whsec_…` bytes), with timestamp-skew enforcement.
|
|
268
318
|
- **delivery core** (`delivery.ts`): `CoreDelivery` (`rawBody` + `headers`) / `InstallDelivery` (+ `method`/`path`) / `DeliveryResult` (`{ status, body }`) / `CoreHeaders`, plus `readHeader` (case-insensitive, array-tolerant), `decodeBody`, and `toResponse`. Zero request/response types.
|
|
269
319
|
- **install handlers** (`install-handlers.ts`): layer 1 `handleInstallDelivery(input, { appSecret, store, onInstall?, onUninstall?, onSettings? })` serves the signed install/uninstall/settings handoff from raw bytes + headers (routes on the path's trailing segment); layer 2 `handleInstallRequest(request, …)` adapts `Request` → `Response` onto it. Defaults persist the installation (encrypted) in the store; a redelivered install for an existing installation merges idempotently (`saveResyncedInstallation`). The Hono wrapper `createInstallHandlers` lives under `@usequeek/app-sdk/hono` (`hono.ts`).
|
|
270
|
-
- **client** (`client.ts`): `createQueekClient({ apiBase, apiKey })` — the low-level typed fetch client over the Merchant API (`X-Client-Key`), with `Idempotency-Key` on writes, typed `QueekApiError`s, and 429 retry helpers.
|
|
271
|
-
- **webhooks** (`webhooks.ts`): layer 1 `handleWebhookDelivery(input, { store, handlers })` verifies each delivery against the installation's endpoint secret, dedupes on `webhook-id`, and dispatches `topic → handler` at most once; layer 2 `handleWebhookRequest(request, …)` adapts `Request` → `Response` onto it. The Hono wrapper `createWebhookHandler` lives under `@usequeek/app-sdk/hono` (`hono.ts`).
|
|
320
|
+
- **client** (`client.ts`): `createQueekClient({ apiBase, apiKey })` — the low-level typed fetch client over the Merchant API (`X-Client-Key`), with `Idempotency-Key` on writes, typed `QueekApiError`s, and 429 retry helpers. Both it and `createInstallationClient({ installationId, apiBase, tokens })` are generic (`<AppPaths>`, default: the bundled `merchant-schema.ts` compat shim): pass the app's own codegen output for current types with zero SDK publish. The default does NOT auto-update (re-run codegen in the app; sunset signal at 1.0). `openapi/merchant.json` stays in-repo as `gen:merchant`'s reference input and is not shipped in the published package. Prefer `createInstallationClient` in apps.
|
|
321
|
+
- **webhooks** (`webhooks.ts`): layer 1 `handleWebhookDelivery(input, { store, handlers })` verifies each delivery against the installation's endpoint secret, dedupes on `webhook-id`, and dispatches `topic → handler` at most once; layer 2 `handleWebhookRequest(request, …)` adapts `Request` → `Response` onto it. The Hono wrapper `createWebhookHandler` lives under `@usequeek/app-sdk/hono` (`hono.ts`). Unknown topics answer 200 `unhandled` — see Data deletion before relying on that.
|
|
322
|
+
- **proxy** (`proxy.ts`): `verifyProxyQuery` / `verifyProxyQueryDetailed` — app-proxy query verification byte-exact with the backend (`path\nshop\nts\nsorted(k=v&...)`, hex HMAC-SHA256 over the FULL `whsec_…` string, 5-minute skew floored at 60 s, `timingSafeEqual`, previous-secret grace over the secrets list); layer 1 `verifyProxyDelivery(input, { store, … })` resolves the installation from the store by `kid` and claims single-use `jti`; layer 2 `handleProxyRequest(request, { store, path, … }, onVerified)` serves GET only. The Hono wrapper `createProxyHandler` lives under `@usequeek/app-sdk/hono` (`hono.ts`).
|
|
272
323
|
- **store** (`store.ts`): `SqliteInstallationStore` (local/dev/test) and `PostgresInstallationStore`
|
|
273
324
|
(`pg`, pool max 2, advisory-locked schema + `schema_version` row so two containers boot
|
|
274
325
|
safely) — installations encrypted at rest (AES-GCM via `APP_ENCRYPTION_KEY`), plus the
|
|
@@ -284,16 +335,130 @@ retries (~4 h) are gone; resync cannot backfill them. Full runbook: `docs/deploy
|
|
|
284
335
|
PEM private-key blocks — the app JWT and private key can never reach logs.
|
|
285
336
|
- **session** (`session.ts`, server-only via `@usequeek/app-sdk/server`):
|
|
286
337
|
`verifySessionToken` — HS256 dashboard session tokens minted per installation
|
|
287
|
-
(`embsec_…` secret, raw UTF-8 key bytes, 20 s clock tolerance, slug audience
|
|
288
|
-
|
|
338
|
+
(`embsec_…` secret, raw UTF-8 key bytes, 20 s clock tolerance, slug audience,
|
|
339
|
+
issuer = the handoff `apiBase` verbatim (`installation.apiBase` — it equals the
|
|
340
|
+
bare `app.url` the backend signs as `iss`), full installation binding). One token
|
|
341
|
+
type, exactly Shopify's `id_token`: the dashboard puts the same token in the
|
|
342
|
+
first-load URL param (`queek_token`, stripped on arrival) and answers bridge
|
|
343
|
+
`ready` requests with it, so the app's exchange endpoint verifies first-load and
|
|
344
|
+
refresh tokens with this one verifier — no purpose split, extra claims are
|
|
345
|
+
ignored. The secret never enters a
|
|
289
346
|
browser bundle: the main entry does not export the verifier. The install
|
|
290
347
|
and resync handoffs deliver `embed_secret` + `app_id`; the store keeps
|
|
291
348
|
them on the installation (`embedSecret` encrypted, `appId`), and
|
|
292
349
|
`sessionTokenInstallationId` reads the token's `installation_id` as an
|
|
293
350
|
unverified routing hint so a server can load that row before verifying.
|
|
294
|
-
- **frame** (`frame.ts`, browser-safe, no secret):
|
|
295
|
-
|
|
296
|
-
|
|
351
|
+
- **frame** (`frame.ts`, browser-safe, no secret): the embedded-app bridge
|
|
352
|
+
v1 — typed unions both directions (`ready{capabilities,sdkVersion}`,
|
|
353
|
+
`theme`, `title`/`title-action`, `toast`, `save-bar`/`save-bar-action`,
|
|
354
|
+
`navigate`/`navigated`, `open`, `pick-resource`/`resource-picked`/
|
|
355
|
+
`resource-pick-cancelled`), unknown types ignored, every outbound string
|
|
356
|
+
length-capped client-side. `listenToDashboard` (accepts only the exact
|
|
357
|
+
dashboard origin, plus an `expectSource` window check) and typed senders
|
|
358
|
+
(`sendReady`, `sendToast`, `sendSaveBar`, `sendTitle`, `sendNavigated`,
|
|
359
|
+
`sendOpen`, `sendPickResource`, …) to the exact target origin, never `"*"`.
|
|
360
|
+
`parseOutboundMessage` runs the same per-type validation the dashboard
|
|
361
|
+
runs — the dashboard re-validates everything before acting (open targets,
|
|
362
|
+
title/toast/pick content), so a compromised frame cannot smuggle
|
|
363
|
+
`javascript:` URLs or uncapped strings through the bridge. `open` targets
|
|
364
|
+
are a dashboard-relative path or an absolute `https:` URL; backslashes,
|
|
365
|
+
control characters, protocol-relative URLs and every other scheme are
|
|
366
|
+
refused (by `clipOutbound` as well as `isAllowedOpenTarget`).
|
|
367
|
+
|
|
368
|
+
**Bridge handshake.** The app announces `ready{capabilities,sdkVersion}`;
|
|
369
|
+
the dashboard answers with its `theme` message, which also declares the
|
|
370
|
+
DASHBOARD's capabilities: `{ type: "theme", mode, locale?, capabilities?:
|
|
371
|
+
string[], bridge?: "1" }` (same caps as outbound: at most 32 entries of 64
|
|
372
|
+
characters). `listenToDashboard` exposes both on the `BridgeTheme` passed to
|
|
373
|
+
`onTheme`. A dashboard that omits `capabilities` is legacy (ready/token/
|
|
374
|
+
resize only). `<QueekProvider>` stores the declared set: `pickResource`
|
|
375
|
+
rejects at once when it lacks `pick-resource`, waits up to 1.5 s after
|
|
376
|
+
`ready` when the set is still unknown, and rejects if no handshake arrives.
|
|
377
|
+
Each pick carries a `requestId` (on `pick-resource`); a dashboard that
|
|
378
|
+
declares `pick-resource` MUST echo it on `resource-picked` / `resource-pick-cancelled` — answers with a
|
|
379
|
+
different or missing id are dropped as stale.
|
|
380
|
+
- **auth** (`auth-fetch.ts`, no secret — browser-safe as a module, but import it
|
|
381
|
+
from `@usequeek/app-sdk/browser` in browser bundles, never from the main
|
|
382
|
+
entry): `installAuthFetch({ exchange })`
|
|
383
|
+
— reads the dashboard token (`queek_token` query param) from the first
|
|
384
|
+
load, strips it with `history.replaceState`, exchanges it once for the
|
|
385
|
+
app's own session, attaches `Authorization: Bearer <session>` to
|
|
386
|
+
same-origin fetch (cookies stay out), and re-establishes via the bridge
|
|
387
|
+
`ready→token` flow + one retry on 401. Session recovery runs only when framed (a post target other than the
|
|
388
|
+
window itself) and is not retried per request after a failed attempt — only
|
|
389
|
+
after a later dashboard token arrives. Pass the same `capabilities`/
|
|
390
|
+
`sdkVersion` as the provider so its refresh `ready` announces one consistent set. The app's
|
|
391
|
+
token-exchange endpoint receives the first-load token and every 401-refresh
|
|
392
|
+
token through the same `exchange` callback — one token type, so it verifies
|
|
393
|
+
both with the single `verifySessionToken` verifier:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
import { verifySessionTokenDetailed } from "@usequeek/app-sdk/server";
|
|
397
|
+
|
|
398
|
+
async function exchange(token: string): Promise<string> {
|
|
399
|
+
const options = { secret: installation.embedSecret!, audience: "my-app", issuer: installation.apiBase, expected };
|
|
400
|
+
const checked = await verifySessionTokenDetailed(token, options);
|
|
401
|
+
if (!checked.ok) throw Object.assign(new Error("unauthorized"), { status: 401 });
|
|
402
|
+
return mintAppSession(checked.claims);
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
- **theme** (`theme.ts`, browser-safe, no secret): `applyTheme(mode)` toggles
|
|
406
|
+
`.dark` on `<html>` + `color-scheme` (shadcn's dark-mode mechanism);
|
|
407
|
+
`themeBootstrapScript()` returns the inline `<head>` snippet that reads the
|
|
408
|
+
unsigned `theme` URL param before first paint (no flash — a first-paint hint
|
|
409
|
+
only, validated to `light`|`dark`, overwritten by the live bridge message);
|
|
410
|
+
`installThemeListener` follows live bridge `theme{mode}` messages (only from
|
|
411
|
+
`window.parent` by default; pass `expectSource` to override). The live mode is
|
|
412
|
+
remembered in `sessionStorage`, and the bootstrap script falls back to it when
|
|
413
|
+
the URL carries no `theme` param, so an in-frame reload never flashes light. Every live `theme` message also
|
|
414
|
+
rewrites the `theme` URL param (other params kept) so a stale first-load param
|
|
415
|
+
never beats the current mode on reload, and the first-load mode is remembered
|
|
416
|
+
too (a legacy dashboard that never sends `theme` still reloads flash-free).
|
|
417
|
+
- **react** (`react.ts`, via `@usequeek/app-sdk/react` — `react` is an
|
|
418
|
+
optional peer, same pattern as `./hono`): `<QueekProvider>` owns one
|
|
419
|
+
bridge subscription; `useQueek()` returns `{ toast, saveBar, title,
|
|
420
|
+
navigate, pickResource, theme }` over the framework-free core.
|
|
421
|
+
- **scopes** (`scopes.ts`): `createInstallationScopesClient({ installationId, store, signJwt, … })`
|
|
422
|
+
— the optional-scopes session, wired to the same store + token provider as the
|
|
423
|
+
installation client. `queryScopes` reads the cached grant (no network);
|
|
424
|
+
`requestScopes` builds the dashboard consent link (pure — the SDK never
|
|
425
|
+
renders consent); `revokeScopes` posts the app-authenticated revoke and
|
|
426
|
+
refreshes the cache. The install/settings handlers accept the signed
|
|
427
|
+
`app/scopes_update` handoff on both routes and refresh the cached grant
|
|
428
|
+
(dropping the cached token when it moved).
|
|
429
|
+
|
|
430
|
+
## Scopes
|
|
431
|
+
|
|
432
|
+
Apps declare required scopes (granted at install) plus optional scopes
|
|
433
|
+
(requested later, revocable). The per-installation session covers the app side:
|
|
434
|
+
|
|
435
|
+
```ts
|
|
436
|
+
import { createInstallationScopesClient } from "@usequeek/app-sdk";
|
|
437
|
+
|
|
438
|
+
const scopes = createInstallationScopesClient({
|
|
439
|
+
installationId,
|
|
440
|
+
store,
|
|
441
|
+
signJwt: () => provider.signJwt(),
|
|
442
|
+
appSlug: "my-app",
|
|
443
|
+
optionalScopes: ["merchant-orders-read"], // the app's own manifest knowledge
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
await scopes.queryScopes(); // { granted, optional } — cached, no network
|
|
447
|
+
scopes.requestScopes(["merchant-orders-read"]); // dashboard consent link (open via sendOpen)
|
|
448
|
+
await scopes.revokeScopes(["merchant-orders-read"]); // 422 app_scope_required → AppScopeRequiredError
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
`queryScopes` splits the cached effective grant against the declared-optional
|
|
452
|
+
list you pass (the app surface exposes the grant but no declared list, so the
|
|
453
|
+
split needs your manifest knowledge). `requestScopes` returns
|
|
454
|
+
`/apps?app={slug}&view=scopes&scopes={a},{b}` (absolute under
|
|
455
|
+
`dashboardOrigin` when configured) — open it via `sendOpen` or a redirect: it
|
|
456
|
+
opens the merchant's consent screen in the dashboard (shipped with dashboard
|
|
457
|
+
item 8). The link carries the slug + the scope list only; the dashboard
|
|
458
|
+
resolves the installation from the signed-in store. The grant reaches you as a
|
|
459
|
+
signed `app/scopes_update` handoff, which the install/settings handlers apply
|
|
460
|
+
like a resync (same verifier, same replay claim): cached grant refreshed,
|
|
461
|
+
cached token dropped only when the grant moved.
|
|
297
462
|
|
|
298
463
|
## Embedded merchant page (S4 stage 2)
|
|
299
464
|
|
|
@@ -309,8 +474,10 @@ Content-Security-Policy: frame-ancestors https://merchant.queek.com
|
|
|
309
474
|
Replace the host with the dashboard origin you registered. `frame-ancestors`
|
|
310
475
|
is a docs-only control the dashboard cannot enforce for you: without it any
|
|
311
476
|
site may frame the page, and the token handshake (origin-bound) is your only
|
|
312
|
-
remaining gate. Verify every token server-side
|
|
313
|
-
|
|
477
|
+
remaining gate. Verify every token server-side before trusting calls that carry one:
|
|
478
|
+
the token-exchange endpoint receives the first-load token and every 401-refresh
|
|
479
|
+
token through the same `installAuthFetch({ exchange })` callback — one token type,
|
|
480
|
+
verified with the single `verifySessionToken` verifier (see the `auth` entry above).
|
|
314
481
|
|
|
315
482
|
## Local development
|
|
316
483
|
|
package/dist/app-auth.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export declare const APP_INSTALLATION_PENDING_CODE = "app_installation_pending";
|
|
|
38
38
|
export declare const RESYNC_COOLDOWN_CODE = "resync_cooldown";
|
|
39
39
|
/** 429 everywhere: honor `Retry-After`, jittered. */
|
|
40
40
|
export declare const TOO_MANY_REQUESTS_CODE = "too_many_requests";
|
|
41
|
+
/**
|
|
42
|
+
* 403 on a merchant call: the cached installation token was minted for an
|
|
43
|
+
* older grant (missing scope). The grant changed server-side — drop the
|
|
44
|
+
* cached token, re-mint once, retry once.
|
|
45
|
+
*/
|
|
46
|
+
export declare const INSUFFICIENT_SCOPE_CODE = "insufficient_scope";
|
|
41
47
|
/** True when a Queek error code is the kill-switch/disabled refusal. */
|
|
42
48
|
export declare function isAppTokenRevoked(code: string | undefined): boolean;
|
|
43
49
|
/** True when a Queek error code is the fatal app-credential refusal. */
|
|
@@ -53,7 +59,7 @@ export declare function isResyncCooldown(code: string | undefined): boolean;
|
|
|
53
59
|
* is dead — drop it, re-mint once, retry once. `api_key_revoked` covers the
|
|
54
60
|
* uninstalled store, the removed kid, and the replaced K-slot; the re-mint
|
|
55
61
|
* then answers 404 `app_installation_gone` (purge) or 401 `invalid_client`
|
|
56
|
-
* (halt) where applicable. Every OTHER merchant 403 (
|
|
62
|
+
* (halt) where applicable. Every OTHER merchant 403 (plan, mode)
|
|
57
63
|
* propagates to the caller without a mint.
|
|
58
64
|
*/
|
|
59
65
|
export declare const API_KEY_REVOKED_CODE = "api_key_revoked";
|
|
@@ -66,6 +72,8 @@ export declare const INVALID_CLIENT_KEY_CODE = "invalid_client_key";
|
|
|
66
72
|
* halt), not a single dead token.
|
|
67
73
|
*/
|
|
68
74
|
export declare function isTokenRefusal(status: number, code: string | undefined): boolean;
|
|
75
|
+
/** True when a merchant-API 403 means the cached token predates a grant change. */
|
|
76
|
+
export declare function isInsufficientScope(status: number, code: string | undefined): boolean;
|
|
69
77
|
/** App-JWT `iat` is backdated this far (clock skew, GitHub's recommendation). */
|
|
70
78
|
export declare const APP_JWT_SKEW_SECONDS = 60;
|
|
71
79
|
/** App-JWT lifetime: `exp = iat + 540`, so `exp − iat` (540 s) stays ≤ the 600 s wire bound. */
|
package/dist/app-auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-auth.d.ts","sourceRoot":"","sources":["../src/app-auth.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AAQH,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAC1D,+EAA+E;AAC/E,eAAO,MAAM,0BAA0B,0BAA0B,CAAC;AAClE;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,6BAA6B,CAAC;AACxE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,qDAAqD;AACrD,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"app-auth.d.ts","sourceRoot":"","sources":["../src/app-auth.ts"],"names":[],"mappings":"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"}
|
package/dist/app-auth.js
CHANGED
|
@@ -44,6 +44,12 @@ export const APP_INSTALLATION_PENDING_CODE = "app_installation_pending";
|
|
|
44
44
|
export const RESYNC_COOLDOWN_CODE = "resync_cooldown";
|
|
45
45
|
/** 429 everywhere: honor `Retry-After`, jittered. */
|
|
46
46
|
export const TOO_MANY_REQUESTS_CODE = "too_many_requests";
|
|
47
|
+
/**
|
|
48
|
+
* 403 on a merchant call: the cached installation token was minted for an
|
|
49
|
+
* older grant (missing scope). The grant changed server-side — drop the
|
|
50
|
+
* cached token, re-mint once, retry once.
|
|
51
|
+
*/
|
|
52
|
+
export const INSUFFICIENT_SCOPE_CODE = "insufficient_scope";
|
|
47
53
|
/** True when a Queek error code is the kill-switch/disabled refusal. */
|
|
48
54
|
export function isAppTokenRevoked(code) {
|
|
49
55
|
return code === APP_TOKEN_REVOKED_CODE;
|
|
@@ -69,7 +75,7 @@ export function isResyncCooldown(code) {
|
|
|
69
75
|
* is dead — drop it, re-mint once, retry once. `api_key_revoked` covers the
|
|
70
76
|
* uninstalled store, the removed kid, and the replaced K-slot; the re-mint
|
|
71
77
|
* then answers 404 `app_installation_gone` (purge) or 401 `invalid_client`
|
|
72
|
-
* (halt) where applicable. Every OTHER merchant 403 (
|
|
78
|
+
* (halt) where applicable. Every OTHER merchant 403 (plan, mode)
|
|
73
79
|
* propagates to the caller without a mint.
|
|
74
80
|
*/
|
|
75
81
|
export const API_KEY_REVOKED_CODE = "api_key_revoked";
|
|
@@ -87,6 +93,10 @@ export function isTokenRefusal(status, code) {
|
|
|
87
93
|
return (status === 403 &&
|
|
88
94
|
(code === API_KEY_REVOKED_CODE || code === API_KEY_EXPIRED_CODE || code === INVALID_CLIENT_KEY_CODE));
|
|
89
95
|
}
|
|
96
|
+
/** True when a merchant-API 403 means the cached token predates a grant change. */
|
|
97
|
+
export function isInsufficientScope(status, code) {
|
|
98
|
+
return status === 403 && code === INSUFFICIENT_SCOPE_CODE;
|
|
99
|
+
}
|
|
90
100
|
// ---------------------------------------------------------------------------
|
|
91
101
|
// Timing bounds (all in the open, all injectable in tests).
|
|
92
102
|
// ---------------------------------------------------------------------------
|
package/dist/app-auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-auth.js","sourceRoot":"","sources":["../src/app-auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE3D;;;;;;;;;;;GAWG;AAEH,8EAA8E;AAC9E,0EAA0E;AAC1E,2EAA2E;AAC3E,4BAA4B;AAC5B,8EAA8E;AAE9E,wFAAwF;AACxF,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AACpD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,mBAAmB,CAAC;AAC1D,+EAA+E;AAC/E,MAAM,CAAC,MAAM,0BAA0B,GAAG,uBAAuB,CAAC;AAClE;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,0BAA0B,CAAC;AACxE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,iBAAiB,CAAC;AACtD,qDAAqD;AACrD,MAAM,CAAC,MAAM,sBAAsB,GAAG,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"app-auth.js","sourceRoot":"","sources":["../src/app-auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-origin session carriage for embedded apps (U-validation "Auth
|
|
3
|
+
* carriage"): `installAuthFetch()` reads the dashboard token from the
|
|
4
|
+
* first-load URL (`?queek_token=`, stripped immediately), exchanges it
|
|
5
|
+
* ONCE for the app's own session, and attaches
|
|
6
|
+
* `Authorization: Bearer <session>` to every same-origin fetch. A 401
|
|
7
|
+
* re-establishes the session via the bridge `ready→token` flow and retries
|
|
8
|
+
* once. Cookies stay out (`credentials: "omit"` on handled requests —
|
|
9
|
+
* the cross-site iframe cannot rely on them anyway).
|
|
10
|
+
*
|
|
11
|
+
* The caller supplies `exchange(token) => session`: the app's own endpoint
|
|
12
|
+
* that verifies the dashboard token server-side with the single
|
|
13
|
+
* `verifySessionToken` verifier and mints the app session — the exchange
|
|
14
|
+
* runs once on the first load so the first paint needs no bridge
|
|
15
|
+
* round-trip, and again (same callback) with a fresh token of the same
|
|
16
|
+
* type after every 401 refresh. This module never sees secrets — it only
|
|
17
|
+
* carries opaque tokens.
|
|
18
|
+
*
|
|
19
|
+
* DOM-free by design (like `frame.ts`): location/history access is
|
|
20
|
+
* injectable and defaults to the global window when present.
|
|
21
|
+
*/
|
|
22
|
+
import { type EmbedEventTarget, type EmbedPostTarget } from "./frame.js";
|
|
23
|
+
/**
|
|
24
|
+
* Dashboard token query param on the first load. Matches the dashboard
|
|
25
|
+
* frame src contract (the ONLY token that may ride a URL, namespaced so it
|
|
26
|
+
* cannot collide with an app's own `token` param). The token is the same
|
|
27
|
+
* type the bridge `ready→token` flow returns on refresh — first load and
|
|
28
|
+
* refresh differ only in transport (URL param vs postMessage), exactly
|
|
29
|
+
* Shopify's `id_token`.
|
|
30
|
+
*/
|
|
31
|
+
export declare const LAUNCH_TOKEN_PARAM = "queek_token";
|
|
32
|
+
/** How long to wait for the dashboard's token reply after a `ready`. */
|
|
33
|
+
export declare const BRIDGE_TOKEN_TIMEOUT_MS = 8000;
|
|
34
|
+
export interface AuthFetchOptions {
|
|
35
|
+
/**
|
|
36
|
+
* Exchange a dashboard token for the app's own session (caller's endpoint).
|
|
37
|
+
* The endpoint receives the first-load token and every 401-refresh token
|
|
38
|
+
* through this same callback — one token type, so it verifies both with
|
|
39
|
+
* the single `verifySessionToken` verifier.
|
|
40
|
+
*/
|
|
41
|
+
exchange: (token: string) => Promise<string>;
|
|
42
|
+
/** Exact dashboard origin for the `ready→token` refresh flow. */
|
|
43
|
+
dashboardOrigin: string;
|
|
44
|
+
/** First-load token query param (default `queek_token`). */
|
|
45
|
+
param?: string;
|
|
46
|
+
/** postMessage target for `ready` (defaults to the global window's parent). */
|
|
47
|
+
postTarget?: EmbedPostTarget;
|
|
48
|
+
/**
|
|
49
|
+
* Capabilities announced in the refresh `ready` — pass the SAME list as the
|
|
50
|
+
* React provider / theme listener so the dashboard sees one consistent set.
|
|
51
|
+
*/
|
|
52
|
+
capabilities?: string[];
|
|
53
|
+
/** SDK version announced in the refresh `ready`. */
|
|
54
|
+
sdkVersion?: string;
|
|
55
|
+
/** message-event target for the token reply (defaults to the global window). */
|
|
56
|
+
listenTarget?: EmbedEventTarget;
|
|
57
|
+
/**
|
|
58
|
+
* Required `event.source` for bridge messages. Defaults to the global
|
|
59
|
+
* window's parent when present (the embedding dashboard); pass `null` to
|
|
60
|
+
* disable the source check.
|
|
61
|
+
*/
|
|
62
|
+
expectSource?: unknown;
|
|
63
|
+
/** fetch implementation (defaults to the global fetch). */
|
|
64
|
+
fetchImpl?: typeof fetch;
|
|
65
|
+
/** Current href (defaults to the global window location). */
|
|
66
|
+
getHref?: () => string;
|
|
67
|
+
/** Replace the URL without navigating (defaults to history.replaceState). */
|
|
68
|
+
replaceUrl?: (url: string) => void;
|
|
69
|
+
}
|
|
70
|
+
export interface InstalledAuth {
|
|
71
|
+
/** Session-carrying fetch: same-origin requests gain the session as a Bearer token, and a 401 retries once after a refresh. */
|
|
72
|
+
fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
73
|
+
/** Current app session, or null before the exchange settles / without a token. */
|
|
74
|
+
getSession: () => string | null;
|
|
75
|
+
/** Settles to the session, or null when there is no launch token to exchange. */
|
|
76
|
+
ready: Promise<string | null>;
|
|
77
|
+
/** Re-establish the session via the bridge `ready→token` flow. */
|
|
78
|
+
refresh: () => Promise<string>;
|
|
79
|
+
/** Unsubscribe the bridge token listener. */
|
|
80
|
+
dispose: () => void;
|
|
81
|
+
}
|
|
82
|
+
/** Read the first-load token from a URL without touching it. */
|
|
83
|
+
export declare function readLaunchToken(href: string, param?: string): string | null;
|
|
84
|
+
/**
|
|
85
|
+
* Strip the first-load token from a URL (every other param, plus the hash,
|
|
86
|
+
* is preserved). Returns the stripped href, or null when no token is
|
|
87
|
+
* present.
|
|
88
|
+
*/
|
|
89
|
+
export declare function stripLaunchToken(href: string, param?: string): string | null;
|
|
90
|
+
export declare function installAuthFetch(options: AuthFetchOptions): InstalledAuth;
|
|
91
|
+
//# sourceMappingURL=auth-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-fetch.d.ts","sourceRoot":"","sources":["../src/auth-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,eAAe,EAAgC,MAAM,YAAY,CAAC;AAEvG;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAEhD,wEAAwE;AACxE,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7C,iEAAiE;IACjE,eAAe,EAAE,MAAM,CAAC;IACxB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,UAAU,CAAC,EAAE,eAAe,CAAC;IAC7B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;IACvB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,+HAA+H;IAC/H,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,GAAG,OAAO,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IAChF,kFAAkF;IAClF,UAAU,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAChC,iFAAiF;IACjF,KAAK,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9B,kEAAkE;IAClE,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/B,6CAA6C;IAC7C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAeD,gEAAgE;AAChE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAA2B,GAAG,MAAM,GAAG,IAAI,CAO/F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAA2B,GAAG,MAAM,GAAG,IAAI,CAWhG;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,aAAa,CAqNzE"}
|