@usequeek/app-sdk 0.4.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/CHANGELOG.md +120 -0
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/app-auth.d.ts +139 -0
- package/dist/app-auth.d.ts.map +1 -0
- package/dist/app-auth.js +205 -0
- package/dist/app-auth.js.map +1 -0
- package/dist/background.d.ts +68 -0
- package/dist/background.d.ts.map +1 -0
- package/dist/background.js +78 -0
- package/dist/background.js.map +1 -0
- package/dist/client.d.ts +148 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +269 -0
- package/dist/client.js.map +1 -0
- package/dist/collected.d.ts +103 -0
- package/dist/collected.d.ts.map +1 -0
- package/dist/collected.js +59 -0
- package/dist/collected.js.map +1 -0
- package/dist/crypto.d.ts +6 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +76 -0
- package/dist/crypto.js.map +1 -0
- package/dist/frame.d.ts +58 -0
- package/dist/frame.d.ts.map +1 -0
- package/dist/frame.js +56 -0
- package/dist/frame.js.map +1 -0
- package/dist/handoff.d.ts +113 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +29 -0
- package/dist/handoff.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/install-handlers.d.ts +62 -0
- package/dist/install-handlers.d.ts.map +1 -0
- package/dist/install-handlers.js +238 -0
- package/dist/install-handlers.js.map +1 -0
- package/dist/logger.d.ts +27 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +75 -0
- package/dist/logger.js.map +1 -0
- package/dist/merchant-schema.d.ts +13963 -0
- package/dist/merchant-schema.d.ts.map +1 -0
- package/dist/merchant-schema.js +11 -0
- package/dist/merchant-schema.js.map +1 -0
- package/dist/resync.d.ts +80 -0
- package/dist/resync.d.ts.map +1 -0
- package/dist/resync.js +203 -0
- package/dist/resync.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +8 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +66 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +155 -0
- package/dist/session.js.map +1 -0
- package/dist/signatures.d.ts +55 -0
- package/dist/signatures.d.ts.map +1 -0
- package/dist/signatures.js +74 -0
- package/dist/signatures.js.map +1 -0
- package/dist/store.d.ts +278 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +610 -0
- package/dist/store.js.map +1 -0
- package/dist/tokens.d.ts +162 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +411 -0
- package/dist/tokens.js.map +1 -0
- package/dist/webhooks.d.ts +60 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +125 -0
- package/dist/webhooks.js.map +1 -0
- package/openapi/merchant.json +21452 -0
- package/package.json +63 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@usequeek/app-sdk` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
4
|
+
|
|
5
|
+
## [0.2.0] - 2026-09-25
|
|
6
|
+
|
|
7
|
+
GitHub-style app credentials (slice S1 of `app-credentials-and-databases`, built against the
|
|
8
|
+
`/api/v1/apps` wire contract in parallel with the backend B1/B2 — the backend is not deployed yet).
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **BREAKING:** the install handoff no longer carries a store-callable `api_key`, and the store
|
|
13
|
+
no longer has an `api_key_enc` column. Installations cache one minted token instead
|
|
14
|
+
(`token_enc` + `token_expires_at` + `token_kid`); opening a 0.1.x SQLite database migrates it
|
|
15
|
+
(legacy key ciphertext is discarded — minted tokens replace it).
|
|
16
|
+
- `InstallationRecord` carries the cached token (`token`/`tokenExpiresAt`/`tokenKid`, memory-only
|
|
17
|
+
plaintext) plus the persisted 409-pending mark (`pending`); `InstallationStore` gains
|
|
18
|
+
`listInstallations()`, `clearCachedToken()`, `clearAllCachedTokens()`,
|
|
19
|
+
`markInstallationPending()` / `clearInstallationPending()` / `isKnownPending()`.
|
|
20
|
+
Opening a v1 database migrates it (schema v2: `pending` column, backfilled false;
|
|
21
|
+
Postgres `schema_version` guard converges to exactly one row at version 2).
|
|
22
|
+
- The default install handler merges a redelivered install envelope for an existing installation
|
|
23
|
+
idempotently (resync path): secret + settings refresh, `installedAt` and the cached token kept.
|
|
24
|
+
- The logger additionally redacts bare RS256 JWTs and PEM private-key blocks.
|
|
25
|
+
- `.env.example` files document `APP_KEY_ID` + `APP_PRIVATE_KEY`.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `app-auth.ts`: `loadAppCredential` (`APP_SLUG`/`APP_KEY_ID`/`APP_PRIVATE_KEY`, RSA PEM validated
|
|
30
|
+
at boot), `signAppJwt` (RS256 via `node:crypto` only, `iat` now − 60 s, `exp` window 540 s,
|
|
31
|
+
`kid` header), and the wire-contract error codes in exactly one place (`INVALID_CLIENT_CODE`,
|
|
32
|
+
`APP_TOKEN_REVOKED_CODE` (confirmed: `ApiError::APP_TOKEN_REVOKED`, also returned when the app is
|
|
33
|
+
disabled), `APP_INSTALLATION_GONE_CODE`, `APP_INSTALLATION_PENDING_CODE`, `RESYNC_COOLDOWN_CODE`,
|
|
34
|
+
`TOO_MANY_REQUESTS_CODE`).
|
|
35
|
+
- `tokens.ts`: `createAppTokenProvider` (`acquireToken` with 5-minute validity skew, in-process
|
|
36
|
+
single-flight per installation, exact contract error mapping: `invalid_client` halts, kill-switch
|
|
37
|
+
403 drops all tokens and halts, `app_installation_gone` purges, 429 + jitter, bounded 5xx
|
|
38
|
+
backoff) and `createInstallationClient` (every call through `acquireToken`; merchant re-mint
|
|
39
|
+
only on token refusals — any 401 or 403 `api_key_revoked`/`api_key_expired`/`invalid_client_key`
|
|
40
|
+
→ drop, re-mint once, retry once; merchant 403 `app_token_revoked` → drop all + halt; every
|
|
41
|
+
other 403 propagates without a mint).
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- Merchant 403s that are NOT token refusals (e.g. `insufficient_scope`, plan, mode) no longer
|
|
46
|
+
burn a mint per call: they propagate to the caller untouched (rev 7 review). `AppTokens` gains
|
|
47
|
+
`revokeAppAccess()` for the merchant-observed kill switch.
|
|
48
|
+
- Mint/resync 409 `app_installation_pending` (rev 8 review): retry later with backoff
|
|
49
|
+
(bounded), NEVER purge, NEVER halt. The installation carries a persisted pending mark
|
|
50
|
+
(B2 review r2: `pending` column on the installation row, schema v2 — survives restarts;
|
|
51
|
+
`markInstallationPending` / `isKnownPending` / `clearInstallationPending` on both the
|
|
52
|
+
stores and `AppTokenProvider`, which delegates); a 409 with any other code propagates
|
|
53
|
+
untouched.
|
|
54
|
+
- Resync 404 `app_installation_gone` purges just that installation and the run continues
|
|
55
|
+
(backend re-checks status on resync per B2 review r2 — an uninstall landing between
|
|
56
|
+
authorize and resync 404s here); the row's deletion drops its pending mark with it.
|
|
57
|
+
- Resync 429s are distinguished by code (rev 8): 429 `resync_cooldown` is the ≤1/hour
|
|
58
|
+
rotation cooldown (skipped + recorded in `cooldownSkipped`, never retried); any other 429
|
|
59
|
+
is the per-app bucket (`too_many_requests`: `Retry-After` + jitter, bounded retries,
|
|
60
|
+
then the run aborts — never recorded as a cooldown skip). `ResyncResult` gains
|
|
61
|
+
`pendingSkipped` for installations still pending after the retry budget.
|
|
62
|
+
- Resync purge-absent keeps locally pending rows (rev 8): `GET installations` lists active
|
|
63
|
+
installations only, so absence never purges an installation the app knows (via a 409 on
|
|
64
|
+
mint or resync) is pending. The list cursor is treated as opaque keyset throughout —
|
|
65
|
+
followed until `next_cursor` is null, never interpreted.
|
|
66
|
+
- `resync.ts`: `resyncFromQueek` (active-only list over an opaque keyset cursor →
|
|
67
|
+
per-installation resync with pending-retry/skip + cooldown-skip vs throttle-retry →
|
|
68
|
+
drop cached tokens → purge absent except known-pending; connectivity scope only).
|
|
69
|
+
- `store.ts`: `PostgresInstallationStore` (`pg`, pool max 2, advisory-locked schema +
|
|
70
|
+
`schema_version` row), `createInstallationStore` (`DATABASE_URL` → Postgres, else SQLite —
|
|
71
|
+
refused in production), AES-GCM envelope unchanged.
|
|
72
|
+
- `background.ts`: `runInstallationCatchup` (uniform 0–600 s start jitter, per-install error
|
|
73
|
+
isolation, concurrency ≤ pool size, honors 429 once per installation).
|
|
74
|
+
- CI runs the Postgres suite via a `postgres:16` service container (`DATABASE_URL` always set);
|
|
75
|
+
without `DATABASE_URL` those tests skip cleanly.
|
|
76
|
+
- Vitest suites against a local fake Queek server (JWT-verifying mint, opaque-cursor list, resync,
|
|
77
|
+
merchant refusals): cache-hit/expiry/single-flight/two-container minting, every contract error,
|
|
78
|
+
wipe → resync → connectivity, cooldown skip vs throttle retry, pending mint/resync retry + skip,
|
|
79
|
+
purge-absent (including pending-kept), resync-404 purges one installation without aborting,
|
|
80
|
+
persisted pending marks (v1→v2 migration + restart simulation on both stores),
|
|
81
|
+
production-refuses-SQLite.
|
|
82
|
+
|
|
83
|
+
## [0.3.0] - 2026-09-25
|
|
84
|
+
|
|
85
|
+
One shared Postgres pool per app process (the apps' own tables now live beside the SDK's tables in the same per-app database).
|
|
86
|
+
|
|
87
|
+
### Added
|
|
88
|
+
|
|
89
|
+
- `store.ts`: `createPostgresPool(connectionString, max)` — builds one `pg` Pool with the idle-client error guard for a process to share between its installation store and its app store. `PostgresInstallationStore` and `createInstallationStore` accept an optional shared `pool` (a provided pool wins over `DATABASE_URL`; `close()` never ends a pool the store does not own). `PostgresStoreOptions.connectionString` is now optional when `pool` is given.
|
|
90
|
+
|
|
91
|
+
## [0.4.0] - 2026-09-28
|
|
92
|
+
|
|
93
|
+
Standalone release: `@usequeek/app-sdk` ships from its own repo (`usequeek/app-sdk`).
|
|
94
|
+
|
|
95
|
+
### Added
|
|
96
|
+
|
|
97
|
+
- Session verifier as a server-only export (`@usequeek/app-sdk/server`): `verifySessionToken` — HS256 dashboard session tokens minted per installation (`embsec_…` secret, 20 s clock tolerance, slug audience + api_base issuer, full installation binding). The secret never enters a browser bundle: the main entry does not export the verifier. `sessionTokenInstallationId` reads the token's `installation_id` as an unverified routing hint.
|
|
98
|
+
- Frame bridge (`frame.ts`, browser-safe, no secret): `listenToDashboard` (accepts only the exact dashboard origin) and `sendReady` (exact target origin, never `"*"`).
|
|
99
|
+
- Resync handoff accepted on both `/install` and `/settings` (`type: app/resync`): the install-shaped data plus the resync-only `secret_rotated` flag merges via `saveResyncedInstallation` (secrets/settings/scopes refresh, `installedAt` and the cached token kept).
|
|
100
|
+
- Install handoff carries `app_id` + `embed_secret`: the app's own id (exactly as the session token signs it) and the installation's `embsec_…` embed secret, handed over on install/resync and kept on the installation record (`embedSecret` encrypted, `appId`).
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
|
|
104
|
+
- Merchant snapshot refreshed from production (line-properties PATCH, `storefront.domains`, `app_id`, no list `meta.currency`): regenerated types from the live Merchant API contract (decision 2026-09-24-one-public-product-object) — the product list + retrieve now return the one public `ProductResource`, the same object as `products/*` webhooks (`id` = int p_id, `uid` = UUID, plus `url`, `storefront_price`, `storefront_compare_at_price`, `primary_image_url`, `variants[]` with integer `id` = variant p_id, `images[]`; no cost/wholesale/discount internals).
|
|
105
|
+
- Installation-bound app writes for the S3a contract (queek_backend@c1fa1c31; snapshot intentionally ahead of production — see `openapi/merchant.drift.json`, re-check after the S3a deploy): `setSetupNotice` (PUT app/setup, full-sheet write), `sendAlert` (POST app/alerts, severity/title/message plus optional dedupe key), `collectedDefinitions.list/create/update` (the calling installation's own collected types), and `createRecord` (POST records with a fresh Idempotency-Key per submit; `values` is a `{field_key: value}` object per the backend's FormsAppCollectedTest).
|
|
106
|
+
|
|
107
|
+
## [0.1.0] - 2026-09-24
|
|
108
|
+
|
|
109
|
+
First release: the kit every Queek app is built from.
|
|
110
|
+
|
|
111
|
+
### Added
|
|
112
|
+
|
|
113
|
+
- Standard Webhooks signature verification (`verifyQueekSignature`) with timestamp-skew enforcement.
|
|
114
|
+
- Hono install/uninstall/settings handlers (`createInstallHandlers`) with encrypted persistence defaults.
|
|
115
|
+
- Typed Merchant API client (`createQueekClient`): `X-Client-Key` auth, `Idempotency-Key` on writes, typed errors, 429 retry helpers. Types generated from the committed `openapi/merchant.json` snapshot.
|
|
116
|
+
- Topic webhook dispatch (`createWebhookHandler`) with per-installation secrets and `webhook-id` dedupe.
|
|
117
|
+
- Encrypted SQLite installation store (`SqliteInstallationStore`, AES-GCM).
|
|
118
|
+
- Detached background-task helper (`detach`) with delivery-result tracking.
|
|
119
|
+
- Redacting JSON logger (`createLogger`).
|
|
120
|
+
- Dev-only `QUEEK_DEV_API_HOSTS` API-host override (refused when `NODE_ENV=production`).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Queek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# @usequeek/app-sdk
|
|
2
|
+
|
|
3
|
+
The SDK for building a Queek app on the public Merchant API and signed webhooks. Hono route helpers for the install handoff and topic webhooks, 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).
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm i @usequeek/app-sdk hono pg
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Requires Node `>=22.14`. `hono` is a peer dependency so your app never carries two copies. `pg` is a regular dependency (the production store).
|
|
10
|
+
|
|
11
|
+
## Example
|
|
12
|
+
|
|
13
|
+
A minimal app: health check, install/uninstall/settings handlers, and one webhook topic:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
createAppTokenProvider,
|
|
18
|
+
createInstallHandlers,
|
|
19
|
+
createWebhookHandler,
|
|
20
|
+
loadAppCredential,
|
|
21
|
+
SqliteInstallationStore,
|
|
22
|
+
} from "@usequeek/app-sdk";
|
|
23
|
+
import { Hono } from "hono";
|
|
24
|
+
|
|
25
|
+
const store = new SqliteInstallationStore({
|
|
26
|
+
path: "./data/installations.db",
|
|
27
|
+
storeKey: process.env.APP_ENCRYPTION_KEY!,
|
|
28
|
+
});
|
|
29
|
+
const tokens = createAppTokenProvider({
|
|
30
|
+
credential: loadAppCredential({ appSlug: "hello" }), // APP_SLUG/APP_KEY_ID/APP_PRIVATE_KEY
|
|
31
|
+
store,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const app = new Hono();
|
|
35
|
+
app.get("/health", (c) => c.json({ ok: true }));
|
|
36
|
+
app.route("/", createInstallHandlers({ appSecret: process.env.QUEEK_APP_SECRET!, store }));
|
|
37
|
+
app.route(
|
|
38
|
+
"/webhooks",
|
|
39
|
+
createWebhookHandler({
|
|
40
|
+
store,
|
|
41
|
+
handlers: {
|
|
42
|
+
"orders/updated": async (envelope, context) => {
|
|
43
|
+
console.log("order update:", context.installation.storePid, envelope.data);
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
export default app;
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Every Merchant API call goes through `createInstallationClient({ installationId, apiBase, tokens })`,
|
|
53
|
+
which resolves the installation's token via `acquireToken()` and sends it as `X-Client-Key`.
|
|
54
|
+
|
|
55
|
+
## Credential lifecycle (S1: GitHub-style installation tokens)
|
|
56
|
+
|
|
57
|
+
One asymmetric credential per app — no per-installation secrets cross the handoff any more.
|
|
58
|
+
|
|
59
|
+
1. **Generate a keypair** (once, offline) and keep the private key in the founder's password
|
|
60
|
+
manager beside `APP_ENCRYPTION_KEY` (key + DB loss = working data lost — see below):
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
openssl genrsa -out app-private.pem 2048
|
|
64
|
+
openssl rsa -in app-private.pem -pubout -out app-public.pem
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
2. **Register the public key** (Queek side; the backend stores it under a `kid`):
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
php artisan app:register --public-key=./app-public.pem
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Queek prints the app secret (`QUEEK_APP_SECRET`, verifies the handoff) and the key id
|
|
74
|
+
(`APP_KEY_ID`, rides the JWT `kid` header). Configure the app with `APP_SLUG` (= `iss`),
|
|
75
|
+
`APP_KEY_ID`, and `APP_PRIVATE_KEY` (the PEM — never logged, never shipped to clients).
|
|
76
|
+
|
|
77
|
+
3. **Acquire / cache / re-mint.** `acquireToken(installationId)` serves the cached token while
|
|
78
|
+
its expiry is more than 5 minutes away; otherwise it signs an RS256 app JWT
|
|
79
|
+
(`iss` = slug, `iat` = now − 60 s, `exp` = `iat` + 540 s, header `kid` — `node:crypto`
|
|
80
|
+
only) and `POST`s `access_tokens`, then persists the token encrypted with its expiry and
|
|
81
|
+
`kid` (one shared cache row per installation — restarts never burst). Concurrent callers
|
|
82
|
+
in one process share one in-flight mint; two containers minting at once is harmless by
|
|
83
|
+
design (Queek keeps coexisting tokens valid; a residual race self-heals via re-mint).
|
|
84
|
+
Merchant refusal table (rev 7): any 401, or 403 `api_key_revoked` / `api_key_expired` /
|
|
85
|
+
`invalid_client_key` → drop the token, re-mint once, retry once (a second refusal
|
|
86
|
+
propagates); 403 `app_token_revoked` → drop ALL cached tokens and halt minting (kill
|
|
87
|
+
switch / disabled app, no mint); every other 403 (scope, plan, mode) propagates to the
|
|
88
|
+
caller without a mint.
|
|
89
|
+
|
|
90
|
+
4. **Failures follow the wire contract exactly** (`app-auth.ts` holds each code in one
|
|
91
|
+
constant, confirmed against the backend build):
|
|
92
|
+
- `401 invalid_client` — fatal for the app: loud log, minting stops, no retry loop.
|
|
93
|
+
- `403 app_token_revoked` (kill switch / disabled app) — drops ALL cached tokens, stops
|
|
94
|
+
minting, loud log. Minting resumes after the app is re-enabled + process restart
|
|
95
|
+
(or `resumeMinting()`).
|
|
96
|
+
- `404 app_installation_gone` — purges that installation locally.
|
|
97
|
+
- `409 app_installation_pending` — the installation is not active yet: retry later
|
|
98
|
+
with backoff, NEVER purge, NEVER halt. The row carries a persisted pending mark
|
|
99
|
+
(store column, schema v2 — survives restarts, cleared when the installation is
|
|
100
|
+
seen active or its mint succeeds).
|
|
101
|
+
- `429 resync_cooldown` (per-installation resync only) — the ≤1/hour rotation
|
|
102
|
+
cooldown: skipped + recorded, never retried. Any OTHER 429 is the per-app bucket
|
|
103
|
+
(`too_many_requests`): `Retry-After` honored with jitter, bounded retries.
|
|
104
|
+
- `5xx` — bounded exponential backoff.
|
|
105
|
+
|
|
106
|
+
5. **Resync after DB loss or a long outage.** `resyncFromQueek({ apiBase, tokens, store })`
|
|
107
|
+
lists ACTIVE installations only (opaque keyset cursor — followed until `next_cursor`
|
|
108
|
+
is null, never interpreted) → requests a resync per installation (409 pending →
|
|
109
|
+
backoff + bounded retry, then skipped + recorded, never purged; 429 `resync_cooldown`
|
|
110
|
+
→ skipped + recorded, never retried; any other 429 → `Retry-After` + jitter, bounded
|
|
111
|
+
retry; 404 `app_installation_gone` purges just that row and the run continues) → drops
|
|
112
|
+
cached tokens → purges local rows absent from Queek's list, EXCEPT rows carrying the
|
|
113
|
+
persisted pending mark — absence never purges a pending row, even across a restart.
|
|
114
|
+
The fresh `webhook_secret` + non-secret settings arrive over the
|
|
115
|
+
existing signed install channel — the install handler merges a redelivery for an
|
|
116
|
+
existing installation idempotently (secret + settings refresh, `installedAt` and the
|
|
117
|
+
cached token kept).
|
|
118
|
+
|
|
119
|
+
6. **Kill switch behaviour.** One backend operation revokes every token of an app across
|
|
120
|
+
every store on the next request. The SDK side: cached tokens are dropped, minting halts
|
|
121
|
+
with a loud log, and every call fails closed until the app is re-enabled and the
|
|
122
|
+
process restarts (or resumes). Rotation without drama: add a `kid` (both verify) →
|
|
123
|
+
switch the app to it → wait one token TTL → remove the old `kid`.
|
|
124
|
+
|
|
125
|
+
## Data rule + backups
|
|
126
|
+
|
|
127
|
+
The app database holds INSTALLATIONS ONLY (refs, token cache, webhook secrets, settings).
|
|
128
|
+
Business data lives in Queek. `resyncFromQueek` restores CONNECTIVITY with zero merchant
|
|
129
|
+
action — but app WORKING data (inbound tokens, order links, form tokens, app-side-only
|
|
130
|
+
settings) does NOT come back from resync: it needs the per-app `pg_dump` backup
|
|
131
|
+
(RPO ≤ 24 h) plus the `APP_ENCRYPTION_KEY` backup. Events missed beyond Queek's webhook
|
|
132
|
+
retries (~4 h) are gone; resync cannot backfill them. Full runbook: `docs/deploy.md` + O1.
|
|
133
|
+
|
|
134
|
+
## API surface
|
|
135
|
+
|
|
136
|
+
- **app-auth** (`app-auth.ts`): `loadAppCredential` (`APP_SLUG`/`APP_KEY_ID`/`APP_PRIVATE_KEY`,
|
|
137
|
+
PEM validated at boot), `signAppJwt` (RS256, `iat` now − 60 s, `exp` window 540 s ≤ 600 s,
|
|
138
|
+
`kid` header), the wire-contract error codes in one place (`INVALID_CLIENT_CODE`,
|
|
139
|
+
`APP_TOKEN_REVOKED_CODE`, `APP_INSTALLATION_GONE_CODE`, `APP_INSTALLATION_PENDING_CODE`,
|
|
140
|
+
`RESYNC_COOLDOWN_CODE`, `TOO_MANY_REQUESTS_CODE`), `AppMintHaltedError`.
|
|
141
|
+
- **tokens** (`tokens.ts`): `createAppTokenProvider({ credential, store, … })` —
|
|
142
|
+
`acquireToken` (cache → sign → mint → persist), single-flight per installation, the exact
|
|
143
|
+
contract error mapping (409 pending → backoff + bounded retry, marked, never purged);
|
|
144
|
+
`createInstallationClient({ installationId, apiBase, tokens })` — the `QueekClient`
|
|
145
|
+
every app call uses (re-mint once + retry once on token refusals only).
|
|
146
|
+
- **resync** (`resync.ts`): `resyncFromQueek({ apiBase, tokens, store })` — list active
|
|
147
|
+
only (opaque keyset cursor) → resync each (409 pending → retry then skip + record;
|
|
148
|
+
429 `resync_cooldown` → skip + record; other 429 → backoff + retry) → drop tokens →
|
|
149
|
+
purge absent except known-pending. Connectivity scope only.
|
|
150
|
+
- **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.
|
|
151
|
+
- **install handlers** (`install-handlers.ts`): `createInstallHandlers({ appSecret, store, onInstall?, onUninstall?, onSettings? })` — serves the signed install/uninstall/settings handoff. Defaults persist the installation (encrypted) in the store; a redelivered install for an existing installation merges idempotently (`saveResyncedInstallation`).
|
|
152
|
+
- **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. Types come from `openapi/merchant.json`, the committed snapshot of the live contract. Prefer `createInstallationClient` in apps.
|
|
153
|
+
- **webhooks** (`webhooks.ts`): `createWebhookHandler({ store, handlers })` — verifies each delivery against the installation's endpoint secret, dedupes on `webhook-id`, and dispatches `topic → handler` at most once.
|
|
154
|
+
- **store** (`store.ts`): `SqliteInstallationStore` (local/dev/test) and `PostgresInstallationStore`
|
|
155
|
+
(`pg`, pool max 2, advisory-locked schema + `schema_version` row so two containers boot
|
|
156
|
+
safely) — installations encrypted at rest (AES-GCM via `APP_ENCRYPTION_KEY`), plus the
|
|
157
|
+
persisted 409-pending mark (`pending` column, schema v2, migrated in place) and the
|
|
158
|
+
seen-webhook-id claim table behind dedupe. Pick with `createInstallationStore()`
|
|
159
|
+
(`DATABASE_URL` set → Postgres, else SQLite — which production REFUSES with a clear
|
|
160
|
+
message). Set once when the app is deployed; installs never change env: each install adds
|
|
161
|
+
a row to the app's database, with that store's token + webhook secret encrypted using this key.
|
|
162
|
+
- **background** (`background.ts`): `detach` plus `runInstallationCatchup` — jittered
|
|
163
|
+
per-installation cron (uniform 0–600 s start jitter, per-install error isolation,
|
|
164
|
+
concurrency ≤ pool size, honors 429 once per installation).
|
|
165
|
+
- **logger** (`logger.ts`): redacts `sk_`/`pk_`/`whsec_`/`Bearer` values, bare RS256 JWTs, and
|
|
166
|
+
PEM private-key blocks — the app JWT and private key can never reach logs.
|
|
167
|
+
- **session** (`session.ts`, server-only via `@usequeek/app-sdk/server`):
|
|
168
|
+
`verifySessionToken` — HS256 dashboard session tokens minted per installation
|
|
169
|
+
(`embsec_…` secret, raw UTF-8 key bytes, 20 s clock tolerance, slug audience
|
|
170
|
+
+ api_base issuer, full installation binding). The secret never enters a
|
|
171
|
+
browser bundle: the main entry does not export the verifier. The install
|
|
172
|
+
and resync handoffs deliver `embed_secret` + `app_id`; the store keeps
|
|
173
|
+
them on the installation (`embedSecret` encrypted, `appId`), and
|
|
174
|
+
`sessionTokenInstallationId` reads the token's `installation_id` as an
|
|
175
|
+
unverified routing hint so a server can load that row before verifying.
|
|
176
|
+
- **frame** (`frame.ts`, browser-safe, no secret): `listenToDashboard`
|
|
177
|
+
(accepts only the exact dashboard origin) and `sendReady` (exact target
|
|
178
|
+
origin, never `"*"`).
|
|
179
|
+
|
|
180
|
+
## Embedded merchant page (S4 stage 2)
|
|
181
|
+
|
|
182
|
+
The dashboard frames your app's granted merchant page in a
|
|
183
|
+
`sandbox="allow-scripts allow-forms"` iframe and delivers the session token
|
|
184
|
+
by postMessage. Serve the page with a `frame-ancestors` policy naming ONLY
|
|
185
|
+
your dashboard origin, for example:
|
|
186
|
+
|
|
187
|
+
```http
|
|
188
|
+
Content-Security-Policy: frame-ancestors https://merchant.queek.com
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Replace the host with the dashboard origin you registered. `frame-ancestors`
|
|
192
|
+
is a docs-only control the dashboard cannot enforce for you: without it any
|
|
193
|
+
site may frame the page, and the token handshake (origin-bound) is your only
|
|
194
|
+
remaining gate. Verify every token server-side with `verifySessionToken`
|
|
195
|
+
before trusting calls that carry one.
|
|
196
|
+
|
|
197
|
+
## Local development
|
|
198
|
+
|
|
199
|
+
Point the client at a local backend with `QUEEK_DEV_API_HOSTS` (comma-separated hosts). It is refused when `NODE_ENV=production`, so it can never weaken a live app. See `devApiHostsFromEnv` in `client.ts`.
|
|
200
|
+
|
|
201
|
+
Postgres locally: any Postgres works — point `DATABASE_URL` at it and the pg suite runs
|
|
202
|
+
(CI always runs it via a service container); without `DATABASE_URL` those tests skip cleanly.
|
|
203
|
+
|
|
204
|
+
## Registration
|
|
205
|
+
|
|
206
|
+
App registration is by the Queek team today: you ship Queek your manifest URL and receive the app secret + store key. There is no public self-serve registration yet.
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app credential (S1, SDK 0.2.0): ONE asymmetric RS256 key per app.
|
|
3
|
+
* Queek holds the PUBLIC keys (`apps.public_keys`, kid-capped); the app
|
|
4
|
+
* holds the PRIVATE key in `APP_PRIVATE_KEY` and signs short-lived app
|
|
5
|
+
* JWTs that mint per-installation tokens (`acquireToken()`).
|
|
6
|
+
*
|
|
7
|
+
* Wire contract (BINDING for B2 and S1): every call under `/api/v1/apps`
|
|
8
|
+
* carries `Authorization: Bearer <app JWT>` (RS256, header `kid`, claims
|
|
9
|
+
* `iss` = app slug, `iat`, `exp`, `exp − iat` ≤ 600 s). `node:crypto` only
|
|
10
|
+
* — no new dependency. The JWT and the private key are NEVER logged (the
|
|
11
|
+
* logger redacts `Bearer …` values; signers below never emit them).
|
|
12
|
+
*/
|
|
13
|
+
/** 401 on an app-credential call: bad/expired JWT or unknown kid. Fatal for the app. */
|
|
14
|
+
export declare const INVALID_CLIENT_CODE = "invalid_client";
|
|
15
|
+
/**
|
|
16
|
+
* 403 on an app-credential call: the app is disabled or its token epoch was
|
|
17
|
+
* bumped by `app:revoke-tokens` (kill switch). Confirmed against B1
|
|
18
|
+
* (`ApiError::APP_TOKEN_REVOKED`, returned in both cases); matched in
|
|
19
|
+
* exactly one place (`isAppTokenRevoked`).
|
|
20
|
+
*/
|
|
21
|
+
export declare const APP_TOKEN_REVOKED_CODE = "app_token_revoked";
|
|
22
|
+
/** 404 on an installation-scoped app call: purge that installation locally. */
|
|
23
|
+
export declare const APP_INSTALLATION_GONE_CODE = "app_installation_gone";
|
|
24
|
+
/**
|
|
25
|
+
* 409 on an installation-scoped app call: the installation is PENDING
|
|
26
|
+
* (not yet active). Retry later with backoff — NEVER purge, NEVER halt.
|
|
27
|
+
* `GET installations` lists active installations only, so a pending row is
|
|
28
|
+
* absent from the list: resync's purge-absent step must keep local rows
|
|
29
|
+
* the app knows (via a 409) are pending.
|
|
30
|
+
*/
|
|
31
|
+
export declare const APP_INSTALLATION_PENDING_CODE = "app_installation_pending";
|
|
32
|
+
/**
|
|
33
|
+
* 429 on a per-installation resync: the ≤1/hour rotation COOLDOWN. Skip
|
|
34
|
+
* that installation (recorded, no retry loop). Any OTHER resync 429 is the
|
|
35
|
+
* per-app bucket (`too_many_requests`): back off on `Retry-After`
|
|
36
|
+
* (jittered) and retry — never recorded as a cooldown skip.
|
|
37
|
+
*/
|
|
38
|
+
export declare const RESYNC_COOLDOWN_CODE = "resync_cooldown";
|
|
39
|
+
/** 429 everywhere: honor `Retry-After`, jittered. */
|
|
40
|
+
export declare const TOO_MANY_REQUESTS_CODE = "too_many_requests";
|
|
41
|
+
/** True when a Queek error code is the kill-switch/disabled refusal. */
|
|
42
|
+
export declare function isAppTokenRevoked(code: string | undefined): boolean;
|
|
43
|
+
/** True when a Queek error code is the fatal app-credential refusal. */
|
|
44
|
+
export declare function isInvalidClient(code: string | undefined): boolean;
|
|
45
|
+
/** True when a Queek error code means the installation is gone server-side. */
|
|
46
|
+
export declare function isInstallationGone(code: string | undefined): boolean;
|
|
47
|
+
/** True when a Queek error code means the installation is pending (409: retry later, never purge). */
|
|
48
|
+
export declare function isInstallationPending(code: string | undefined): boolean;
|
|
49
|
+
/** True when a resync 429 code is the per-installation rotation cooldown (skip, recorded). */
|
|
50
|
+
export declare function isResyncCooldown(code: string | undefined): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Merchant-API token-refusal codes (rev 7): a token in one of these states
|
|
53
|
+
* is dead — drop it, re-mint once, retry once. `api_key_revoked` covers the
|
|
54
|
+
* uninstalled store, the removed kid, and the replaced K-slot; the re-mint
|
|
55
|
+
* then answers 404 `app_installation_gone` (purge) or 401 `invalid_client`
|
|
56
|
+
* (halt) where applicable. Every OTHER merchant 403 (scope, plan, mode)
|
|
57
|
+
* propagates to the caller without a mint.
|
|
58
|
+
*/
|
|
59
|
+
export declare const API_KEY_REVOKED_CODE = "api_key_revoked";
|
|
60
|
+
export declare const API_KEY_EXPIRED_CODE = "api_key_expired";
|
|
61
|
+
export declare const INVALID_CLIENT_KEY_CODE = "invalid_client_key";
|
|
62
|
+
/**
|
|
63
|
+
* True when a merchant-API refusal means the presented token is dead: any
|
|
64
|
+
* 401, or a 403 with a revoked/expired-key code. `app_token_revoked` is
|
|
65
|
+
* deliberately NOT here — that is the app-wide kill switch (drop all +
|
|
66
|
+
* halt), not a single dead token.
|
|
67
|
+
*/
|
|
68
|
+
export declare function isTokenRefusal(status: number, code: string | undefined): boolean;
|
|
69
|
+
/** App-JWT `iat` is backdated this far (clock skew, GitHub's recommendation). */
|
|
70
|
+
export declare const APP_JWT_SKEW_SECONDS = 60;
|
|
71
|
+
/** App-JWT lifetime: `exp = iat + 540`, so `exp − iat` (540 s) stays ≤ the 600 s wire bound. */
|
|
72
|
+
export declare const APP_JWT_TTL_SECONDS = 540;
|
|
73
|
+
/** A cached installation token counts as valid only while its expiry is more than this far away. */
|
|
74
|
+
export declare const TOKEN_VALIDITY_SKEW_SECONDS = 300;
|
|
75
|
+
/** Mint attempts per `acquireToken` before surfacing a 429/5xx (1 initial + retries). */
|
|
76
|
+
export declare const MAX_MINT_ATTEMPTS = 3;
|
|
77
|
+
/** 5xx backoff base (doubles per attempt); every sleep also carries uniform jitter (see below). */
|
|
78
|
+
export declare const MINT_BACKOFF_BASE_MS = 250;
|
|
79
|
+
/** 5xx backoff ceiling (a 429 `Retry-After` may exceed it — the server's word wins). */
|
|
80
|
+
export declare const MINT_BACKOFF_MAX_MS = 5000;
|
|
81
|
+
/** Uniform jitter added to every mint/resync backoff sleep (0–1000 ms). */
|
|
82
|
+
export declare const RETRY_JITTER_MAX_MS = 1000;
|
|
83
|
+
/** Thrown when `APP_PRIVATE_KEY`/`APP_KEY_ID`/slug fail validation — before any fetch. */
|
|
84
|
+
export declare class InvalidAppCredentialError extends Error {
|
|
85
|
+
readonly code = "invalid_app_credential";
|
|
86
|
+
constructor(message: string);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Thrown (and cached in-process) when Queek refuses the app itself:
|
|
90
|
+
* `invalid_client` (bad/unknown key — fatal until the key is fixed) or the
|
|
91
|
+
* kill-switch 403 (disabled/epoch-revoked — fatal until the app is
|
|
92
|
+
* re-enabled). Minting STOPS: no retry loop. Clears on process restart;
|
|
93
|
+
* call `resumeMinting()` to clear it sooner (tests, or an operator who
|
|
94
|
+
* just re-enabled the app).
|
|
95
|
+
*/
|
|
96
|
+
export declare class AppMintHaltedError extends Error {
|
|
97
|
+
readonly code = "app_mint_halted";
|
|
98
|
+
readonly reason: typeof INVALID_CLIENT_CODE | typeof APP_TOKEN_REVOKED_CODE;
|
|
99
|
+
constructor(reason: AppMintHaltedError["reason"], detail: string);
|
|
100
|
+
}
|
|
101
|
+
/** Thrown when no installation row exists locally for an id. */
|
|
102
|
+
export declare class UnknownInstallationError extends Error {
|
|
103
|
+
readonly code = "unknown_installation";
|
|
104
|
+
constructor(installationId: string);
|
|
105
|
+
}
|
|
106
|
+
export interface AppCredential {
|
|
107
|
+
/** `iss`: the app slug (as registered with `app:register`). */
|
|
108
|
+
appSlug: string;
|
|
109
|
+
/** `kid`: which of the app's public keys Queek verifies against (`APP_KEY_ID`). */
|
|
110
|
+
keyId: string;
|
|
111
|
+
/** PEM-encoded PKCS#8 / PKCS#1 RSA private key (`APP_PRIVATE_KEY`). Memory only, never logged. */
|
|
112
|
+
privateKeyPem: string;
|
|
113
|
+
}
|
|
114
|
+
export interface AppCredentialOptions {
|
|
115
|
+
appSlug?: string;
|
|
116
|
+
keyId?: string;
|
|
117
|
+
privateKeyPem?: string;
|
|
118
|
+
env?: NodeJS.ProcessEnv;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Load the app credential: explicit options win, otherwise `APP_SLUG`,
|
|
122
|
+
* `APP_KEY_ID`, `APP_PRIVATE_KEY` from env. The PEM must parse as an RSA
|
|
123
|
+
* private key here, once, so a broken key fails at boot — never mid-mint.
|
|
124
|
+
* Throws `InvalidAppCredentialError` before any fetch.
|
|
125
|
+
*/
|
|
126
|
+
export declare function loadAppCredential(options?: AppCredentialOptions): AppCredential;
|
|
127
|
+
export interface SignAppJwtOptions {
|
|
128
|
+
credential: AppCredential;
|
|
129
|
+
/** Seconds since epoch. Defaults to `Date.now() / 1000` (injectable in tests). */
|
|
130
|
+
nowSeconds?: number;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Sign an app JWT (RS256, `node:crypto` only): header `{alg RS256, kid}`,
|
|
134
|
+
* claims `{iss = app slug, iat = now − 60 s, exp = iat + 540 s}` — the
|
|
135
|
+
* `exp − iat` window (540 s) stays under the 600 s wire bound. Returns the
|
|
136
|
+
* compact JWT; NEVER logs it or the private key.
|
|
137
|
+
*/
|
|
138
|
+
export declare function signAppJwt(options: SignAppJwtOptions): string;
|
|
139
|
+
//# sourceMappingURL=app-auth.d.ts.map
|
|
@@ -0,0 +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;AAE1D,wEAAwE;AACxE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEnE;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEjE;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEpE;AAED,sGAAsG;AACtG,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAEvE;AAED,8FAA8F;AAC9F,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAElE;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,eAAO,MAAM,oBAAoB,oBAAoB,CAAC;AACtD,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMhF;AAMD,iFAAiF;AACjF,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,gGAAgG;AAChG,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,oGAAoG;AACpG,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAC/C,yFAAyF;AACzF,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,mGAAmG;AACnG,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AACzC,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAMzC,0FAA0F;AAC1F,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,QAAQ,CAAC,IAAI,4BAA4B;gBAE7B,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,IAAI,qBAAqB;IAClC,QAAQ,CAAC,MAAM,EAAE,OAAO,mBAAmB,GAAG,OAAO,sBAAsB,CAAC;gBAEhE,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM;CAKjE;AAED,gEAAgE;AAChE,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,0BAA0B;gBAE3B,cAAc,EAAE,MAAM;CAInC;AAED,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,OAAO,EAAE,MAAM,CAAC;IAChB,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd,kGAAkG;IAClG,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAoCnF;AAMD,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,aAAa,CAAC;IAC1B,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAkB7D"}
|