@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/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { type AppCredential } from "./app-auth.js";
|
|
2
|
+
import { QueekApiError, type QueekClient } from "./client.js";
|
|
3
|
+
import { type Logger } from "./logger.js";
|
|
4
|
+
import type { InstallationStore } from "./store.js";
|
|
5
|
+
/**
|
|
6
|
+
* Installation tokens (S1, SDK 0.2.0): the app holds ONE asymmetric app
|
|
7
|
+
* credential and mints short-lived installation tokens from Queek on
|
|
8
|
+
* demand, cached (encrypted) in its own database to near-expiry.
|
|
9
|
+
*
|
|
10
|
+
* - `acquireToken(installationId)`: cached-valid token (expiry more than 5
|
|
11
|
+
* minutes away) → else sign an app JWT → `POST access_tokens` → persist
|
|
12
|
+
* (encrypted, expires_at, kid) → return. Every Queek API call in the
|
|
13
|
+
* installation client goes through it.
|
|
14
|
+
* - A merchant-API token refusal → drop it, re-mint once, retry once; a
|
|
15
|
+
* second refusal propagates. Token refusal = any 401, or a 403 with
|
|
16
|
+
* `api_key_revoked` / `api_key_expired` / `invalid_client_key` (rev 7).
|
|
17
|
+
* A merchant 403 `app_token_revoked` is the app-wide kill switch (drop
|
|
18
|
+
* all + halt, no mint); any other merchant 403 (scope, plan, mode)
|
|
19
|
+
* propagates untouched. (If the re-mint itself hits `invalid_client` /
|
|
20
|
+
* kill-switch / gone, the mint path below applies those rules.)
|
|
21
|
+
* - Concurrent callers in one process share ONE in-flight mint per
|
|
22
|
+
* installation (single-flight). Across containers two mints are harmless
|
|
23
|
+
* by design: Queek keeps coexisting tokens valid (K=2 slots) and the
|
|
24
|
+
* last write wins the shared cache row; a residual race self-heals via
|
|
25
|
+
* re-mint on 401.
|
|
26
|
+
* - Error handling exactly per the wire contract: 401 `invalid_client`
|
|
27
|
+
* halts minting app-wide (loud log, no retry loop); 403 kill-switch
|
|
28
|
+
* drops ALL cached tokens and halts; 404 `app_installation_gone` purges
|
|
29
|
+
* the installation; 409 `app_installation_pending` retries later with
|
|
30
|
+
* backoff — NEVER purges, NEVER halts (the persisted pending mark keeps
|
|
31
|
+
* the row through restarts and resync's purge-absent step); 429 honors
|
|
32
|
+
* `Retry-After` +
|
|
33
|
+
* jitter with bounded retries; 5xx/network bounded exponential backoff.
|
|
34
|
+
*/
|
|
35
|
+
/** Path of the app-credential API below the store host. */
|
|
36
|
+
export declare const APP_API_PATH = "/api/v1/apps";
|
|
37
|
+
/** Minimal surface the installation client needs (stubs stay one-liners). */
|
|
38
|
+
export interface AppTokens {
|
|
39
|
+
acquireToken(installationId: string): Promise<string>;
|
|
40
|
+
dropCachedToken(installationId: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Kill-switch path: forget EVERY cached token for the app and halt
|
|
43
|
+
* minting (loud log inside). Called on a merchant 403 `app_token_revoked`.
|
|
44
|
+
*/
|
|
45
|
+
revokeAppAccess(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export interface TokenProviderOptions {
|
|
48
|
+
credential: AppCredential;
|
|
49
|
+
store: InstallationStore;
|
|
50
|
+
fetchImpl?: typeof fetch;
|
|
51
|
+
/** Sent as User-Agent on app-API calls. Defaults to `queek-app/1.0`. */
|
|
52
|
+
userAgent?: string;
|
|
53
|
+
/** Extra allowed `apiBase` hosts (test/local backends). */
|
|
54
|
+
allowedApiHosts?: string[];
|
|
55
|
+
/** Loud halt/drop logs go here. Defaults to a `queek-app-tokens` logger (stderr on warn+). */
|
|
56
|
+
logger?: Logger;
|
|
57
|
+
/** Injectable clock (ms since epoch). Defaults to `Date.now`. */
|
|
58
|
+
nowMs?: () => number;
|
|
59
|
+
/** Injectable sleep for tests. Defaults to a real setTimeout sleep. */
|
|
60
|
+
sleep?: (ms: number) => Promise<void>;
|
|
61
|
+
/** Injectable uniform [0,1) for jitter. Defaults to `Math.random`. */
|
|
62
|
+
random?: () => number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Validate the handoff `apiBase` (https + allowlist, same rules as the
|
|
66
|
+
* merchant client) and normalize it to the app-API base
|
|
67
|
+
* (`<origin>/api/v1/apps`). Throws `InvalidApiBaseError` before any fetch.
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveAppApiBase(raw: string, allowedApiHosts?: string[]): string;
|
|
70
|
+
export declare function mintPath(installationId: string): string;
|
|
71
|
+
export declare class AppTokenProvider implements AppTokens {
|
|
72
|
+
private readonly credential;
|
|
73
|
+
private readonly store;
|
|
74
|
+
private readonly fetchImpl;
|
|
75
|
+
private readonly userAgent;
|
|
76
|
+
private readonly allowedApiHosts;
|
|
77
|
+
private readonly logger;
|
|
78
|
+
private readonly nowMs;
|
|
79
|
+
private readonly sleep;
|
|
80
|
+
private readonly random;
|
|
81
|
+
private readonly inflight;
|
|
82
|
+
private halted;
|
|
83
|
+
constructor(options: TokenProviderOptions);
|
|
84
|
+
/** True while minting is halted app-wide (`invalid_client` or kill switch). */
|
|
85
|
+
isHalted(): boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Clear a halt (e.g. the key was fixed / the app re-enabled). A process
|
|
88
|
+
* restart clears it too — the halt is in-process only.
|
|
89
|
+
*/
|
|
90
|
+
resumeMinting(): void;
|
|
91
|
+
acquireToken(installationId: string): Promise<string>;
|
|
92
|
+
private acquireUnshared;
|
|
93
|
+
private isFresh;
|
|
94
|
+
dropCachedToken(installationId: string): Promise<void>;
|
|
95
|
+
/** Kill-switch path (also used by ops): forget every cached token, keep the rows. */
|
|
96
|
+
dropAllCachedTokens(): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Persist the 409 `app_installation_pending` mark for this installation
|
|
99
|
+
* (store column — survives restarts, B2 review r2). Resync's purge-absent
|
|
100
|
+
* step keeps marked rows (the list covers active installations only).
|
|
101
|
+
* Called by the mint path and by `resyncFromQueek`; idempotent, and a
|
|
102
|
+
* no-op when the row is already gone.
|
|
103
|
+
*/
|
|
104
|
+
markInstallationPending(installationId: string): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Clear the persisted pending mark: the installation answered active
|
|
107
|
+
* again (mint success, resync 202/redelivery).
|
|
108
|
+
*/
|
|
109
|
+
clearInstallationPending(installationId: string): Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* True when the row carries the persisted pending mark. Consulted by
|
|
112
|
+
* resync's purge-absent step; false when the row is missing.
|
|
113
|
+
*/
|
|
114
|
+
isKnownPending(installationId: string): Promise<boolean>;
|
|
115
|
+
/**
|
|
116
|
+
* Kill-switch path from any surface (mint 403, merchant 403, ops):
|
|
117
|
+
* forget EVERY cached token for the app and halt minting, loudly.
|
|
118
|
+
* Clears on process restart or `resumeMinting()`.
|
|
119
|
+
*/
|
|
120
|
+
revokeAppAccess(): Promise<void>;
|
|
121
|
+
/** Sign a fresh app JWT (exposed for the app-API calls in `resync.ts`). */
|
|
122
|
+
signJwt(nowSeconds?: number): string;
|
|
123
|
+
/**
|
|
124
|
+
* Map an app-endpoint failure to its contract behavior. ALWAYS throws:
|
|
125
|
+
* the halted error, or the original `QueekApiError` after performing the
|
|
126
|
+
* side effect (drop-all / purge). `installationPid` is log-only.
|
|
127
|
+
*/
|
|
128
|
+
handleAppEndpointError(error: QueekApiError, installation: {
|
|
129
|
+
id: string;
|
|
130
|
+
pid: string;
|
|
131
|
+
}): Promise<never>;
|
|
132
|
+
private mintWithRetry;
|
|
133
|
+
private jittered;
|
|
134
|
+
private mintOnce;
|
|
135
|
+
}
|
|
136
|
+
/** Create a provider in one call (thin wrapper over `new AppTokenProvider`). */
|
|
137
|
+
export declare function createAppTokenProvider(options: TokenProviderOptions): AppTokenProvider;
|
|
138
|
+
export interface InstallationClientOptions {
|
|
139
|
+
installationId: string;
|
|
140
|
+
/** Handoff `api_base` (bare store host or full merchant base). Validated at construction. */
|
|
141
|
+
apiBase: string;
|
|
142
|
+
tokens: AppTokens;
|
|
143
|
+
fetchImpl?: typeof fetch;
|
|
144
|
+
/** Sent as User-Agent. Defaults to `queek-app/1.0`. */
|
|
145
|
+
userAgent?: string;
|
|
146
|
+
/** Extra allowed `apiBase` hosts (test/local backends). */
|
|
147
|
+
allowedApiHosts?: string[];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A `QueekClient` whose every call goes through `acquireToken()`: the
|
|
151
|
+
* installation token is resolved per request (usually a cache hit — the
|
|
152
|
+
* store IS the cache, one shared row per installation), sent as
|
|
153
|
+
* `X-Client-Key`, and on a merchant-API token refusal (any 401, or 403
|
|
154
|
+
* `api_key_revoked` / `api_key_expired` / `invalid_client_key`) it is
|
|
155
|
+
* dropped, re-minted once, and the call retried once; a second refusal
|
|
156
|
+
* propagates to the caller. A merchant 403 `app_token_revoked` drops all
|
|
157
|
+
* cached tokens and halts minting; any other 403 propagates without a
|
|
158
|
+
* mint. 429/network retries (`requestWithRetry`) reuse one idempotency key
|
|
159
|
+
* exactly like the static client.
|
|
160
|
+
*/
|
|
161
|
+
export declare function createInstallationClient(options: InstallationClientOptions): QueekClient;
|
|
162
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { APP_TOKEN_REVOKED_CODE, AppMintHaltedError, INVALID_CLIENT_CODE, isAppTokenRevoked, isInstallationGone, isInstallationPending, isTokenRefusal, MAX_MINT_ATTEMPTS, MINT_BACKOFF_BASE_MS, MINT_BACKOFF_MAX_MS, RETRY_JITTER_MAX_MS, signAppJwt, TOKEN_VALIDITY_SKEW_SECONDS, UnknownInstallationError, } from "./app-auth.js";
|
|
2
|
+
import { createQueekClient, MERCHANT_API_PATH, QueekApiError, queekApiErrorFromResponse, resolveApiBase, } from "./client.js";
|
|
3
|
+
import { createLogger } from "./logger.js";
|
|
4
|
+
/**
|
|
5
|
+
* Installation tokens (S1, SDK 0.2.0): the app holds ONE asymmetric app
|
|
6
|
+
* credential and mints short-lived installation tokens from Queek on
|
|
7
|
+
* demand, cached (encrypted) in its own database to near-expiry.
|
|
8
|
+
*
|
|
9
|
+
* - `acquireToken(installationId)`: cached-valid token (expiry more than 5
|
|
10
|
+
* minutes away) → else sign an app JWT → `POST access_tokens` → persist
|
|
11
|
+
* (encrypted, expires_at, kid) → return. Every Queek API call in the
|
|
12
|
+
* installation client goes through it.
|
|
13
|
+
* - A merchant-API token refusal → drop it, re-mint once, retry once; a
|
|
14
|
+
* second refusal propagates. Token refusal = any 401, or a 403 with
|
|
15
|
+
* `api_key_revoked` / `api_key_expired` / `invalid_client_key` (rev 7).
|
|
16
|
+
* A merchant 403 `app_token_revoked` is the app-wide kill switch (drop
|
|
17
|
+
* all + halt, no mint); any other merchant 403 (scope, plan, mode)
|
|
18
|
+
* propagates untouched. (If the re-mint itself hits `invalid_client` /
|
|
19
|
+
* kill-switch / gone, the mint path below applies those rules.)
|
|
20
|
+
* - Concurrent callers in one process share ONE in-flight mint per
|
|
21
|
+
* installation (single-flight). Across containers two mints are harmless
|
|
22
|
+
* by design: Queek keeps coexisting tokens valid (K=2 slots) and the
|
|
23
|
+
* last write wins the shared cache row; a residual race self-heals via
|
|
24
|
+
* re-mint on 401.
|
|
25
|
+
* - Error handling exactly per the wire contract: 401 `invalid_client`
|
|
26
|
+
* halts minting app-wide (loud log, no retry loop); 403 kill-switch
|
|
27
|
+
* drops ALL cached tokens and halts; 404 `app_installation_gone` purges
|
|
28
|
+
* the installation; 409 `app_installation_pending` retries later with
|
|
29
|
+
* backoff — NEVER purges, NEVER halts (the persisted pending mark keeps
|
|
30
|
+
* the row through restarts and resync's purge-absent step); 429 honors
|
|
31
|
+
* `Retry-After` +
|
|
32
|
+
* jitter with bounded retries; 5xx/network bounded exponential backoff.
|
|
33
|
+
*/
|
|
34
|
+
/** Path of the app-credential API below the store host. */
|
|
35
|
+
export const APP_API_PATH = "/api/v1/apps";
|
|
36
|
+
function defaultSleep(ms) {
|
|
37
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate the handoff `apiBase` (https + allowlist, same rules as the
|
|
41
|
+
* merchant client) and normalize it to the app-API base
|
|
42
|
+
* (`<origin>/api/v1/apps`). Throws `InvalidApiBaseError` before any fetch.
|
|
43
|
+
*/
|
|
44
|
+
export function resolveAppApiBase(raw, allowedApiHosts = []) {
|
|
45
|
+
const merchant = resolveApiBase(raw, allowedApiHosts);
|
|
46
|
+
return `${merchant.slice(0, merchant.length - MERCHANT_API_PATH.length)}${APP_API_PATH}`;
|
|
47
|
+
}
|
|
48
|
+
export function mintPath(installationId) {
|
|
49
|
+
return `/installations/${encodeURIComponent(installationId)}/access_tokens`;
|
|
50
|
+
}
|
|
51
|
+
export class AppTokenProvider {
|
|
52
|
+
credential;
|
|
53
|
+
store;
|
|
54
|
+
fetchImpl;
|
|
55
|
+
userAgent;
|
|
56
|
+
allowedApiHosts;
|
|
57
|
+
logger;
|
|
58
|
+
nowMs;
|
|
59
|
+
sleep;
|
|
60
|
+
random;
|
|
61
|
+
inflight = new Map();
|
|
62
|
+
halted = null;
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.credential = options.credential;
|
|
65
|
+
this.store = options.store;
|
|
66
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
67
|
+
this.userAgent = options.userAgent ?? "queek-app/1.0";
|
|
68
|
+
this.allowedApiHosts = options.allowedApiHosts ?? [];
|
|
69
|
+
this.logger = options.logger ?? createLogger({ service: "queek-app-tokens" });
|
|
70
|
+
this.nowMs = options.nowMs ?? Date.now;
|
|
71
|
+
this.sleep = options.sleep ?? defaultSleep;
|
|
72
|
+
this.random = options.random ?? Math.random;
|
|
73
|
+
}
|
|
74
|
+
/** True while minting is halted app-wide (`invalid_client` or kill switch). */
|
|
75
|
+
isHalted() {
|
|
76
|
+
return this.halted !== null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Clear a halt (e.g. the key was fixed / the app re-enabled). A process
|
|
80
|
+
* restart clears it too — the halt is in-process only.
|
|
81
|
+
*/
|
|
82
|
+
resumeMinting() {
|
|
83
|
+
this.halted = null;
|
|
84
|
+
}
|
|
85
|
+
async acquireToken(installationId) {
|
|
86
|
+
const shared = this.inflight.get(installationId);
|
|
87
|
+
if (shared)
|
|
88
|
+
return shared;
|
|
89
|
+
const attempt = this.acquireUnshared(installationId);
|
|
90
|
+
this.inflight.set(installationId, attempt);
|
|
91
|
+
try {
|
|
92
|
+
return await attempt;
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
this.inflight.delete(installationId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async acquireUnshared(installationId) {
|
|
99
|
+
const row = await this.store.getInstallation(installationId);
|
|
100
|
+
if (!row)
|
|
101
|
+
throw new UnknownInstallationError(installationId);
|
|
102
|
+
// Cache serves even while halted: a bad app key (or a kill switch)
|
|
103
|
+
// stops MINTING, but tokens minted before it stay usable on the
|
|
104
|
+
// merchant API until they expire.
|
|
105
|
+
if (row.token !== null && row.tokenExpiresAt !== null && this.isFresh(row.tokenExpiresAt)) {
|
|
106
|
+
return row.token;
|
|
107
|
+
}
|
|
108
|
+
if (this.halted)
|
|
109
|
+
throw this.halted;
|
|
110
|
+
const minted = await this.mintWithRetry(row.apiBase, installationId, row.installationPid);
|
|
111
|
+
// A successful mint proves the installation is active again: persist
|
|
112
|
+
// the fresh token and clear the pending mark in the same write.
|
|
113
|
+
await this.store.saveInstallation({
|
|
114
|
+
...row,
|
|
115
|
+
token: minted.token,
|
|
116
|
+
tokenExpiresAt: minted.expiresAt,
|
|
117
|
+
tokenKid: minted.kid,
|
|
118
|
+
pending: false,
|
|
119
|
+
updatedAt: new Date(this.nowMs()).toISOString(),
|
|
120
|
+
});
|
|
121
|
+
return minted.token;
|
|
122
|
+
}
|
|
123
|
+
isFresh(expiresAtIso) {
|
|
124
|
+
const expiresMs = Date.parse(expiresAtIso);
|
|
125
|
+
if (Number.isNaN(expiresMs))
|
|
126
|
+
return false;
|
|
127
|
+
return expiresMs - this.nowMs() > TOKEN_VALIDITY_SKEW_SECONDS * 1000;
|
|
128
|
+
}
|
|
129
|
+
async dropCachedToken(installationId) {
|
|
130
|
+
await this.store.clearCachedToken(installationId);
|
|
131
|
+
}
|
|
132
|
+
/** Kill-switch path (also used by ops): forget every cached token, keep the rows. */
|
|
133
|
+
async dropAllCachedTokens() {
|
|
134
|
+
await this.store.clearAllCachedTokens();
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Persist the 409 `app_installation_pending` mark for this installation
|
|
138
|
+
* (store column — survives restarts, B2 review r2). Resync's purge-absent
|
|
139
|
+
* step keeps marked rows (the list covers active installations only).
|
|
140
|
+
* Called by the mint path and by `resyncFromQueek`; idempotent, and a
|
|
141
|
+
* no-op when the row is already gone.
|
|
142
|
+
*/
|
|
143
|
+
async markInstallationPending(installationId) {
|
|
144
|
+
await this.store.markInstallationPending(installationId);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Clear the persisted pending mark: the installation answered active
|
|
148
|
+
* again (mint success, resync 202/redelivery).
|
|
149
|
+
*/
|
|
150
|
+
async clearInstallationPending(installationId) {
|
|
151
|
+
await this.store.clearInstallationPending(installationId);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* True when the row carries the persisted pending mark. Consulted by
|
|
155
|
+
* resync's purge-absent step; false when the row is missing.
|
|
156
|
+
*/
|
|
157
|
+
async isKnownPending(installationId) {
|
|
158
|
+
return this.store.isKnownPending(installationId);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Kill-switch path from any surface (mint 403, merchant 403, ops):
|
|
162
|
+
* forget EVERY cached token for the app and halt minting, loudly.
|
|
163
|
+
* Clears on process restart or `resumeMinting()`.
|
|
164
|
+
*/
|
|
165
|
+
async revokeAppAccess() {
|
|
166
|
+
await this.store.clearAllCachedTokens();
|
|
167
|
+
this.halted = new AppMintHaltedError(APP_TOKEN_REVOKED_CODE, "Queek revoked this app's access (kill switch or disabled app). " +
|
|
168
|
+
"All cached tokens dropped. Re-enable the app, then restart (or resumeMinting).");
|
|
169
|
+
this.logger.error("queek app minting halted: access revoked (kill switch / disabled)", {
|
|
170
|
+
code: APP_TOKEN_REVOKED_CODE,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Sign a fresh app JWT (exposed for the app-API calls in `resync.ts`). */
|
|
174
|
+
signJwt(nowSeconds) {
|
|
175
|
+
return signAppJwt({ credential: this.credential, nowSeconds });
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Map an app-endpoint failure to its contract behavior. ALWAYS throws:
|
|
179
|
+
* the halted error, or the original `QueekApiError` after performing the
|
|
180
|
+
* side effect (drop-all / purge). `installationPid` is log-only.
|
|
181
|
+
*/
|
|
182
|
+
async handleAppEndpointError(error, installation) {
|
|
183
|
+
if (error.status === 401) {
|
|
184
|
+
// The wire contract defines 401 on app endpoints as `invalid_client`
|
|
185
|
+
// (bad/expired JWT, unknown kid): fatal for the app regardless of the
|
|
186
|
+
// exact code string — retry-looping a refused identity is never right.
|
|
187
|
+
this.halted = new AppMintHaltedError(INVALID_CLIENT_CODE, `Queek refused the app credential (status 401, code ${JSON.stringify(error.code)}). ` +
|
|
188
|
+
`Fix APP_PRIVATE_KEY/APP_KEY_ID or re-register, then restart (or resumeMinting).`);
|
|
189
|
+
this.logger.error("queek app minting halted: invalid_client", {
|
|
190
|
+
installation: installation.pid,
|
|
191
|
+
code: error.code,
|
|
192
|
+
});
|
|
193
|
+
throw this.halted;
|
|
194
|
+
}
|
|
195
|
+
if (error.status === 403 && isAppTokenRevoked(error.code)) {
|
|
196
|
+
await this.revokeAppAccess();
|
|
197
|
+
throw this.halted;
|
|
198
|
+
}
|
|
199
|
+
if (error.status === 404 && isInstallationGone(error.code)) {
|
|
200
|
+
// The row's deletion drops its pending mark with it — a gone
|
|
201
|
+
// installation is never "pending".
|
|
202
|
+
await this.store.deleteInstallation(installation.id);
|
|
203
|
+
this.logger.warn("queek installation gone server-side; purged locally", {
|
|
204
|
+
installation: installation.pid,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// NOTE: 409 `app_installation_pending` never reaches here as a purge
|
|
208
|
+
// or halt — the mint loop retries it with backoff below, and resync
|
|
209
|
+
// handles it per installation. A pending row is marked, never deleted.
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
async mintWithRetry(apiBase, installationId, installationPid) {
|
|
213
|
+
const appBase = resolveAppApiBase(apiBase, this.allowedApiHosts);
|
|
214
|
+
let lastError;
|
|
215
|
+
for (let attempt = 0; attempt < MAX_MINT_ATTEMPTS; attempt++) {
|
|
216
|
+
try {
|
|
217
|
+
return await this.mintOnce(appBase, installationId, installationPid);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
lastError = error;
|
|
221
|
+
if (!(error instanceof QueekApiError))
|
|
222
|
+
throw error;
|
|
223
|
+
// Contract-mapped failures throw their final error from inside
|
|
224
|
+
// `handleAppEndpointError` (halt/drop/purge already performed).
|
|
225
|
+
if (error.status === 401 || error.status === 404 || error.status === 403) {
|
|
226
|
+
await this.handleAppEndpointError(error, { id: installationId, pid: installationPid });
|
|
227
|
+
}
|
|
228
|
+
if (error.status === 409 && isInstallationPending(error.code)) {
|
|
229
|
+
// Pending installation (rev 8, mark persisted per B2 review
|
|
230
|
+
// r2): retry later with backoff — NEVER purge, NEVER halt. The
|
|
231
|
+
// persisted mark keeps the row through restarts and resync's
|
|
232
|
+
// purge-absent step (the list covers active rows only). After
|
|
233
|
+
// the bounded budget the 409 propagates to the caller, which
|
|
234
|
+
// retries later; the mark clears on mint success (same write)
|
|
235
|
+
// or with the row on 404-gone purge.
|
|
236
|
+
await this.markInstallationPending(installationId);
|
|
237
|
+
this.logger.warn("queek installation pending; mint retry later with backoff", {
|
|
238
|
+
installation: installationPid,
|
|
239
|
+
code: error.code,
|
|
240
|
+
});
|
|
241
|
+
if (attempt === MAX_MINT_ATTEMPTS - 1)
|
|
242
|
+
throw error;
|
|
243
|
+
const backoff = Math.min(MINT_BACKOFF_MAX_MS, MINT_BACKOFF_BASE_MS * 2 ** attempt);
|
|
244
|
+
await this.sleep(this.jittered(backoff));
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (attempt === MAX_MINT_ATTEMPTS - 1)
|
|
248
|
+
throw error;
|
|
249
|
+
if (error.status === 429) {
|
|
250
|
+
await this.sleep(this.jittered(error.retryAfterMs ?? MINT_BACKOFF_BASE_MS));
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (error.status >= 500 || error.code === "network_error") {
|
|
254
|
+
const backoff = Math.min(MINT_BACKOFF_MAX_MS, MINT_BACKOFF_BASE_MS * 2 ** attempt);
|
|
255
|
+
await this.sleep(this.jittered(backoff));
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
throw lastError;
|
|
262
|
+
}
|
|
263
|
+
jittered(baseMs) {
|
|
264
|
+
return baseMs + Math.floor(this.random() * RETRY_JITTER_MAX_MS);
|
|
265
|
+
}
|
|
266
|
+
async mintOnce(appBase, installationId, installationPid) {
|
|
267
|
+
// Signed per attempt (never reused past its ≤10-min window, never logged).
|
|
268
|
+
const jwt = signAppJwt({ credential: this.credential });
|
|
269
|
+
const url = `${appBase}${mintPath(installationId)}`;
|
|
270
|
+
const headers = new Headers();
|
|
271
|
+
headers.set("Authorization", `Bearer ${jwt}`);
|
|
272
|
+
headers.set("Accept", "application/json");
|
|
273
|
+
headers.set("User-Agent", this.userAgent);
|
|
274
|
+
let response;
|
|
275
|
+
try {
|
|
276
|
+
response = await this.fetchImpl(url, { method: "POST", headers });
|
|
277
|
+
}
|
|
278
|
+
catch (cause) {
|
|
279
|
+
throw new QueekApiError({
|
|
280
|
+
status: 0,
|
|
281
|
+
code: "network_error",
|
|
282
|
+
message: cause instanceof Error ? `Could not reach Queek: ${cause.message}` : "Could not reach Queek.",
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
const text = await response.text();
|
|
286
|
+
const parsed = text === "" ? null : tryParseJson(text);
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
throw queekApiErrorFromResponse(response.status, parsed, response.headers);
|
|
289
|
+
}
|
|
290
|
+
if (response.status !== 201) {
|
|
291
|
+
throw new Error(`Unexpected mint status ${response.status} for installation ${installationPid} (expected 201).`);
|
|
292
|
+
}
|
|
293
|
+
return parseMintBody(parsed, installationPid, this.credential.keyId);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function parseMintBody(body, installationPid, kid) {
|
|
297
|
+
const record = (typeof body === "object" && body !== null ? body : {});
|
|
298
|
+
const token = typeof record.token === "string" && record.token !== "" ? record.token : null;
|
|
299
|
+
const expiresAt = typeof record.expires_at === "string" ? record.expires_at : null;
|
|
300
|
+
if (token === null || expiresAt === null || Number.isNaN(Date.parse(expiresAt))) {
|
|
301
|
+
throw new Error(`Malformed mint response for installation ${installationPid}: expected {token, expires_at}.`);
|
|
302
|
+
}
|
|
303
|
+
return { token, expiresAt, kid };
|
|
304
|
+
}
|
|
305
|
+
/** Create a provider in one call (thin wrapper over `new AppTokenProvider`). */
|
|
306
|
+
export function createAppTokenProvider(options) {
|
|
307
|
+
return new AppTokenProvider(options);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* A `QueekClient` whose every call goes through `acquireToken()`: the
|
|
311
|
+
* installation token is resolved per request (usually a cache hit — the
|
|
312
|
+
* store IS the cache, one shared row per installation), sent as
|
|
313
|
+
* `X-Client-Key`, and on a merchant-API token refusal (any 401, or 403
|
|
314
|
+
* `api_key_revoked` / `api_key_expired` / `invalid_client_key`) it is
|
|
315
|
+
* dropped, re-minted once, and the call retried once; a second refusal
|
|
316
|
+
* propagates to the caller. A merchant 403 `app_token_revoked` drops all
|
|
317
|
+
* cached tokens and halts minting; any other 403 propagates without a
|
|
318
|
+
* mint. 429/network retries (`requestWithRetry`) reuse one idempotency key
|
|
319
|
+
* exactly like the static client.
|
|
320
|
+
*/
|
|
321
|
+
export function createInstallationClient(options) {
|
|
322
|
+
// Validated here, once, before any fetch — same rules as the static client.
|
|
323
|
+
const probe = resolveApiBase(options.apiBase, options.allowedApiHosts ?? []);
|
|
324
|
+
void probe;
|
|
325
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
326
|
+
const userAgent = options.userAgent ?? "queek-app/1.0";
|
|
327
|
+
const allowedApiHosts = options.allowedApiHosts ?? [];
|
|
328
|
+
let cached = null;
|
|
329
|
+
async function clientForToken(token) {
|
|
330
|
+
if (!cached || cached.token !== token) {
|
|
331
|
+
cached = {
|
|
332
|
+
token,
|
|
333
|
+
client: createQueekClient({
|
|
334
|
+
apiBase: options.apiBase,
|
|
335
|
+
apiKey: token,
|
|
336
|
+
fetchImpl,
|
|
337
|
+
userAgent,
|
|
338
|
+
allowedApiHosts,
|
|
339
|
+
}),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
return cached.client;
|
|
343
|
+
}
|
|
344
|
+
async function request(method, path, requestOptions = {}) {
|
|
345
|
+
const token = await options.tokens.acquireToken(options.installationId);
|
|
346
|
+
try {
|
|
347
|
+
return await (await clientForToken(token)).request(method, path, requestOptions);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (!(error instanceof QueekApiError))
|
|
351
|
+
throw error;
|
|
352
|
+
// Kill switch / disabled app, observed on the merchant path: drop
|
|
353
|
+
// every cached token and halt minting, then propagate this refusal
|
|
354
|
+
// (no mint — the app is revoked, not the token).
|
|
355
|
+
if (error.status === 403 && isAppTokenRevoked(error.code)) {
|
|
356
|
+
await options.tokens.revokeAppAccess();
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
// Token refusals ONLY (rev 7): any 401, or a 403 carrying a
|
|
360
|
+
// revoked/expired-key code. Every other 403 (scope, plan, mode)
|
|
361
|
+
// propagates to the caller without burning a mint.
|
|
362
|
+
if (!isTokenRefusal(error.status, error.code))
|
|
363
|
+
throw error;
|
|
364
|
+
// Dead token: drop it, re-mint once, retry once. The re-mint runs
|
|
365
|
+
// the full contract error mapping (halt/purge on
|
|
366
|
+
// invalid_client/kill-switch/gone); a second merchant refusal
|
|
367
|
+
// propagates to the caller.
|
|
368
|
+
await options.tokens.dropCachedToken(options.installationId);
|
|
369
|
+
const fresh = await options.tokens.acquireToken(options.installationId);
|
|
370
|
+
return (await clientForToken(fresh)).request(method, path, requestOptions);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
async function requestWithRetry(method, path, retryOptions = {}) {
|
|
374
|
+
const maxAttempts = Math.max(1, Math.floor(retryOptions.maxAttempts ?? 3));
|
|
375
|
+
const baseDelayMs = retryOptions.baseDelayMs ?? 250;
|
|
376
|
+
const maxDelayMs = retryOptions.maxDelayMs ?? 5000;
|
|
377
|
+
const sleep = retryOptions.sleep ?? defaultSleep;
|
|
378
|
+
const idempotencyKey = retryOptions.idempotencyKey;
|
|
379
|
+
let lastError;
|
|
380
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
381
|
+
try {
|
|
382
|
+
return await request(method, path, { ...retryOptions, idempotencyKey });
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
lastError = error;
|
|
386
|
+
const retryable = error instanceof QueekApiError && (error.isRateLimited || error.code === "network_error");
|
|
387
|
+
if (!retryable || attempt === maxAttempts - 1)
|
|
388
|
+
throw error;
|
|
389
|
+
const delay = error instanceof QueekApiError && error.isRateLimited && error.retryAfterMs !== undefined
|
|
390
|
+
? error.retryAfterMs
|
|
391
|
+
: Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
|
|
392
|
+
await sleep(delay);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
throw lastError;
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
getStore: (signal) => request("GET", "/store", { signal }),
|
|
399
|
+
request,
|
|
400
|
+
requestWithRetry,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function tryParseJson(text) {
|
|
404
|
+
try {
|
|
405
|
+
return JSON.parse(text);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
return { message: text.slice(0, 500) };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { InstallationRecord, InstallationStore } from "./store.js";
|
|
3
|
+
/**
|
|
4
|
+
* Receiver for Queek topic deliveries (`DeliverWebhookJob` in
|
|
5
|
+
* queek_backend, one signed POST per installation endpoint).
|
|
6
|
+
*
|
|
7
|
+
* Delivery envelope: `{ id, topic, api_version: "v1", created_at, data }`
|
|
8
|
+
* with `X-Queek-Topic` echoing `topic`. Verification uses the
|
|
9
|
+
* PER-INSTALLATION endpoint secret handed over once inside the install
|
|
10
|
+
* payload (`data.webhook_secret`) — never the app signing secret.
|
|
11
|
+
*
|
|
12
|
+
* Secret lookup: topic payloads are resource renders with no guaranteed
|
|
13
|
+
* installation/vendor id, so the default resolver identifies the
|
|
14
|
+
* installation by trying each stored installation secret against the
|
|
15
|
+
* signature (the signature itself names the sender; HMACs are microseconds,
|
|
16
|
+
* installations per app are few). Apps that carry their own routing hint
|
|
17
|
+
* may pass `resolveSecret` instead.
|
|
18
|
+
*
|
|
19
|
+
* Dedupe: Queek retries a delivery for ~4h until it sees 2xx, so every
|
|
20
|
+
* `webhook-id` is processed AT MOST once — the header id is claimed
|
|
21
|
+
* atomically up front (exactly one concurrent same-id delivery runs the
|
|
22
|
+
* handler) and a repeat answers 200 WITHOUT re-running it. A throwing
|
|
23
|
+
* handler releases the claim and answers 500 so the retry can land.
|
|
24
|
+
* Unknown topics answer 200 too (a topic the app no longer handles must
|
|
25
|
+
* not wedge Queek's retry queue); only auth and malformed bodies are
|
|
26
|
+
* non-2xx.
|
|
27
|
+
*/
|
|
28
|
+
export declare const QUEEK_TOPIC_HEADER = "X-Queek-Topic";
|
|
29
|
+
export interface QueekWebhookEnvelope<TData = unknown> {
|
|
30
|
+
id: string;
|
|
31
|
+
topic: string;
|
|
32
|
+
api_version: "v1";
|
|
33
|
+
created_at: string;
|
|
34
|
+
data: TData;
|
|
35
|
+
}
|
|
36
|
+
export interface WebhookHandlerContext {
|
|
37
|
+
installation: InstallationRecord;
|
|
38
|
+
topic: string;
|
|
39
|
+
eventId: string;
|
|
40
|
+
}
|
|
41
|
+
export type WebhookHandlerFn<TData = unknown> = (envelope: QueekWebhookEnvelope<TData>, context: WebhookHandlerContext) => Promise<void>;
|
|
42
|
+
export interface SecretResolution {
|
|
43
|
+
installationId: string;
|
|
44
|
+
secret: string;
|
|
45
|
+
}
|
|
46
|
+
export interface WebhookHandlerOptions {
|
|
47
|
+
store: InstallationStore;
|
|
48
|
+
/** `topic → handler`, e.g. `{ "orders/updated": onOrderUpdated }`. */
|
|
49
|
+
handlers: Record<string, WebhookHandlerFn>;
|
|
50
|
+
/** Override the default try-each-secret resolution. */
|
|
51
|
+
resolveSecret?: (envelope: QueekWebhookEnvelope | null, rawBody: string, headers: {
|
|
52
|
+
id: string;
|
|
53
|
+
timestamp: string;
|
|
54
|
+
signatureHeader: string;
|
|
55
|
+
}) => Promise<SecretResolution | null>;
|
|
56
|
+
nowSeconds?: number;
|
|
57
|
+
maxSkewSeconds?: number;
|
|
58
|
+
}
|
|
59
|
+
export declare function createWebhookHandler(options: WebhookHandlerOptions): Hono;
|
|
60
|
+
//# sourceMappingURL=webhooks.d.ts.map
|