@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/resync.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { isInstallationGone, isInstallationPending, isResyncCooldown, RETRY_JITTER_MAX_MS, } from "./app-auth.js";
|
|
2
|
+
import { QueekApiError, queekApiErrorFromResponse } from "./client.js";
|
|
3
|
+
import { createLogger } from "./logger.js";
|
|
4
|
+
import { resolveAppApiBase } from "./tokens.js";
|
|
5
|
+
const MAX_LIST_ATTEMPTS = 3;
|
|
6
|
+
const LIST_BACKOFF_BASE_MS = 250;
|
|
7
|
+
const LIST_BACKOFF_MAX_MS = 5_000;
|
|
8
|
+
/** Per-installation resync attempts before a retryable refusal is skipped or aborts the run. */
|
|
9
|
+
const MAX_RESYNC_ATTEMPTS = 3;
|
|
10
|
+
const RESYNC_BACKOFF_BASE_MS = 250;
|
|
11
|
+
const RESYNC_BACKOFF_MAX_MS = 5_000;
|
|
12
|
+
function defaultSleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
export async function resyncFromQueek(options) {
|
|
16
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
17
|
+
const userAgent = options.userAgent ?? "queek-app/1.0";
|
|
18
|
+
const logger = options.logger ?? createLogger({ service: "queek-app-resync" });
|
|
19
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
20
|
+
const random = options.random ?? Math.random;
|
|
21
|
+
const jittered = (baseMs) => baseMs + Math.floor(random() * RETRY_JITTER_MAX_MS);
|
|
22
|
+
const appBase = resolveAppApiBase(options.apiBase, options.allowedApiHosts ?? []);
|
|
23
|
+
async function appFetch(path, init) {
|
|
24
|
+
// Signed per attempt (never reused past its ≤10-min window, never logged).
|
|
25
|
+
const jwt = options.tokens.signJwt();
|
|
26
|
+
const headers = new Headers();
|
|
27
|
+
headers.set("Authorization", `Bearer ${jwt}`);
|
|
28
|
+
headers.set("Accept", "application/json");
|
|
29
|
+
headers.set("User-Agent", userAgent);
|
|
30
|
+
let response;
|
|
31
|
+
try {
|
|
32
|
+
response = await fetchImpl(`${appBase}${path}`, { method: init.method, headers });
|
|
33
|
+
}
|
|
34
|
+
catch (cause) {
|
|
35
|
+
throw new QueekApiError({
|
|
36
|
+
status: 0,
|
|
37
|
+
code: "network_error",
|
|
38
|
+
message: cause instanceof Error ? `Could not reach Queek: ${cause.message}` : "Could not reach Queek.",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return response;
|
|
42
|
+
}
|
|
43
|
+
/** Parse an app-endpoint failure without acting on it: the caller below
|
|
44
|
+
* decides by status + code (pending/cooldown/throttle/gone/halt). */
|
|
45
|
+
async function readAppError(response) {
|
|
46
|
+
const text = await response.text();
|
|
47
|
+
const parsed = text === "" ? null : tryParseJson(text);
|
|
48
|
+
return queekApiErrorFromResponse(response.status, parsed, response.headers);
|
|
49
|
+
}
|
|
50
|
+
// -- List (opaque keyset cursor, secret-free) ------------------------------
|
|
51
|
+
// The cursor is never interpreted — echoed back verbatim until
|
|
52
|
+
// `next_cursor` is null (rev 8: keyset on a stable unique key, scoped to
|
|
53
|
+
// the calling app; inserts/uninstalls mid-walk never skip or duplicate).
|
|
54
|
+
const listed = [];
|
|
55
|
+
let cursor = null;
|
|
56
|
+
let attempt = 0;
|
|
57
|
+
let listDone = false;
|
|
58
|
+
while (!listDone) {
|
|
59
|
+
const path = cursor === null ? `/installations` : `/installations?cursor=${encodeURIComponent(cursor)}`;
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
response = await appFetch(path, { method: "GET" });
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof QueekApiError &&
|
|
66
|
+
error.code === "network_error" &&
|
|
67
|
+
attempt < MAX_LIST_ATTEMPTS - 1) {
|
|
68
|
+
await sleep(jittered(Math.min(LIST_BACKOFF_MAX_MS, LIST_BACKOFF_BASE_MS * 2 ** attempt)));
|
|
69
|
+
attempt += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
if (response.status === 429) {
|
|
75
|
+
if (attempt < MAX_LIST_ATTEMPTS - 1) {
|
|
76
|
+
const retryAfter = queekApiErrorFromResponse(429, null, response.headers).retryAfterMs;
|
|
77
|
+
await sleep(jittered(retryAfter ?? LIST_BACKOFF_BASE_MS));
|
|
78
|
+
attempt += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
throw queekApiErrorFromResponse(response.status, null, response.headers);
|
|
82
|
+
}
|
|
83
|
+
if (!response.ok || response.status !== 200) {
|
|
84
|
+
// 401/403 halt inside (invalid_client / kill switch); 404-gone is a
|
|
85
|
+
// no-op against the empty id; anything else propagates.
|
|
86
|
+
await options.tokens.handleAppEndpointError(await readAppError(response), { id: "", pid: "" });
|
|
87
|
+
}
|
|
88
|
+
const page = (await response.json());
|
|
89
|
+
for (const item of page.data ?? [])
|
|
90
|
+
listed.push(item);
|
|
91
|
+
attempt = 0; // a good page resets the backoff budget for the next page
|
|
92
|
+
cursor = typeof page.next_cursor === "string" && page.next_cursor !== "" ? page.next_cursor : null;
|
|
93
|
+
listDone = cursor === null;
|
|
94
|
+
}
|
|
95
|
+
// -- Resync each, drop cached tokens, purge the absent ----------------------
|
|
96
|
+
const result = {
|
|
97
|
+
listed: [],
|
|
98
|
+
resyncRequested: [],
|
|
99
|
+
cooldownSkipped: [],
|
|
100
|
+
pendingSkipped: [],
|
|
101
|
+
purged: [],
|
|
102
|
+
};
|
|
103
|
+
const listedIds = new Set();
|
|
104
|
+
for (const item of listed) {
|
|
105
|
+
listedIds.add(item.id);
|
|
106
|
+
result.listed.push(item.id);
|
|
107
|
+
const resyncPath = `/installations/${encodeURIComponent(item.id)}/resync`;
|
|
108
|
+
for (let attempt = 0;; attempt++) {
|
|
109
|
+
const response = await appFetch(resyncPath, { method: "POST" });
|
|
110
|
+
if (response.status === 202) {
|
|
111
|
+
result.resyncRequested.push(item.id);
|
|
112
|
+
await options.tokens.clearInstallationPending(item.id);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
const error = await readAppError(response);
|
|
116
|
+
if (response.status === 429 && isResyncCooldown(error.code)) {
|
|
117
|
+
// Rotation cooldown (≤1/hour): skip this installation, no retry loop.
|
|
118
|
+
result.cooldownSkipped.push(item.id);
|
|
119
|
+
logger.warn("queek resync skipped: rotation cooldown", { installation: item.id });
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
if (response.status === 429) {
|
|
123
|
+
// Per-app bucket (rev 8): back off on `Retry-After` (jittered) and
|
|
124
|
+
// retry — never recorded as a cooldown skip. An exhausted budget
|
|
125
|
+
// aborts the run: the whole app is throttled, so pushing on would
|
|
126
|
+
// only deepen it; the caller retries later.
|
|
127
|
+
if (attempt >= MAX_RESYNC_ATTEMPTS - 1)
|
|
128
|
+
throw error;
|
|
129
|
+
await sleep(jittered(error.retryAfterMs ?? RESYNC_BACKOFF_BASE_MS));
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (response.status === 409 && isInstallationPending(error.code)) {
|
|
133
|
+
// Pending installation (rev 8, mark persisted per B2 review r2):
|
|
134
|
+
// retry later with backoff — NEVER purge. An exhausted budget
|
|
135
|
+
// skips (recorded); the persisted mark survives restarts, so the
|
|
136
|
+
// purge-absent step below (and after a restart) keeps the row.
|
|
137
|
+
await options.tokens.markInstallationPending(item.id);
|
|
138
|
+
if (attempt >= MAX_RESYNC_ATTEMPTS - 1) {
|
|
139
|
+
result.pendingSkipped.push(item.id);
|
|
140
|
+
logger.warn("queek resync skipped: installation pending", { installation: item.id });
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
const backoff = Math.min(RESYNC_BACKOFF_MAX_MS, RESYNC_BACKOFF_BASE_MS * 2 ** attempt);
|
|
144
|
+
await sleep(jittered(backoff));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
// 404-gone purges inside; 401/403-halt throws the halted error; a
|
|
149
|
+
// 409 with any OTHER code rethrows here untouched (never purged).
|
|
150
|
+
await options.tokens.handleAppEndpointError(error, { id: item.id, pid: item.id });
|
|
151
|
+
}
|
|
152
|
+
catch (endpointError) {
|
|
153
|
+
// Gone server-side: the row is already purged inside — record it
|
|
154
|
+
// and keep going. Anything else (halt et al.) aborts the run.
|
|
155
|
+
if (endpointError instanceof QueekApiError &&
|
|
156
|
+
endpointError.status === 404 &&
|
|
157
|
+
isInstallationGone(endpointError.code)) {
|
|
158
|
+
result.purged.push(item.id);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
throw endpointError;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Drop cached tokens for everything still listed (a post-outage token
|
|
166
|
+
// may be revoked server-side; the next call re-mints cleanly)…
|
|
167
|
+
for (const id of listedIds) {
|
|
168
|
+
await options.store.clearCachedToken(id);
|
|
169
|
+
}
|
|
170
|
+
// …and purge local rows Queek no longer lists (uninstall-while-down) —
|
|
171
|
+
// EXCEPT rows carrying the persisted pending mark. The list covers
|
|
172
|
+
// ACTIVE installations only (rev 8), so absence alone never purges a
|
|
173
|
+
// pending row — including after a process restart, because the mark
|
|
174
|
+
// lives in the store column, not in memory. The row stays for a later
|
|
175
|
+
// run, when it is active (listed + resynced), gone (404 → purged), or
|
|
176
|
+
// still pending (kept again).
|
|
177
|
+
const local = await options.store.listInstallations();
|
|
178
|
+
for (const row of local) {
|
|
179
|
+
if (!listedIds.has(row.installationId)) {
|
|
180
|
+
if (await options.tokens.isKnownPending(row.installationId)) {
|
|
181
|
+
logger.warn("queek resync kept a pending installation absent from Queek's list", {
|
|
182
|
+
installation: row.installationPid,
|
|
183
|
+
});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
await options.store.deleteInstallation(row.installationId);
|
|
187
|
+
result.purged.push(row.installationId);
|
|
188
|
+
logger.warn("queek resync purged an installation absent from Queek's list", {
|
|
189
|
+
installation: row.installationPid,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
function tryParseJson(text) {
|
|
196
|
+
try {
|
|
197
|
+
return JSON.parse(text);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return { message: text.slice(0, 500) };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=resync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resync.js","sourceRoot":"","sources":["../src/resync.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-only entry (S4 stage 2): session-token verification needs the
|
|
3
|
+
* per-installation `embsec_…` secret, which must never enter a browser
|
|
4
|
+
* bundle. Import the verifier from `@usequeek/app-sdk/server`, never from
|
|
5
|
+
* the main entry.
|
|
6
|
+
*/
|
|
7
|
+
export type { SessionTokenBinding, SessionTokenClaims, SessionTokenFailure, VerifySessionTokenOptions, } from "./session.js";
|
|
8
|
+
export { EMBED_SECRET_PREFIX, SESSION_CLOCK_TOLERANCE_SECONDS, sessionTokenInstallationId, verifySessionToken, verifySessionTokenDetailed, } from "./session.js";
|
|
9
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,mBAAmB,EACnB,yBAAyB,GAC1B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,mBAAmB,EACnB,+BAA+B,EAC/B,0BAA0B,EAC1B,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,cAAc,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-only entry (S4 stage 2): session-token verification needs the
|
|
3
|
+
* per-installation `embsec_…` secret, which must never enter a browser
|
|
4
|
+
* bundle. Import the verifier from `@usequeek/app-sdk/server`, never from
|
|
5
|
+
* the main entry.
|
|
6
|
+
*/
|
|
7
|
+
export { EMBED_SECRET_PREFIX, SESSION_CLOCK_TOLERANCE_SECONDS, sessionTokenInstallationId, verifySessionToken, verifySessionTokenDetailed, } from "./session.js";
|
|
8
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH,OAAO,EACL,mBAAmB,EACnB,+BAA+B,EAC/B,0BAA0B,EAC1B,kBAAkB,EAClB,0BAA0B,GAC3B,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard session tokens (S4 stage 2): short-lived HS256 JWTs the backend
|
|
3
|
+
* mints per installation for the framed merchant page. The app backend
|
|
4
|
+
* verifies them with the per-installation `embsec_…` secret before trusting
|
|
5
|
+
* any call that carries one.
|
|
6
|
+
*
|
|
7
|
+
* Server-only: the secret must never enter a browser bundle, so this module
|
|
8
|
+
* ships behind the `./server` export, not the main entry. Style mirrors
|
|
9
|
+
* `signatures.ts` — a boolean fast path plus a detailed variant returning
|
|
10
|
+
* `{ ok } | { ok: false, reason }`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const EMBED_SECRET_PREFIX = "embsec_";
|
|
13
|
+
/** The only JWS algorithm the dashboard mints (HS256 over the embsec_ secret). */
|
|
14
|
+
export declare const SESSION_TOKEN_ALG = "HS256";
|
|
15
|
+
/** Backend skew the verifier tolerates (matches the mint side). */
|
|
16
|
+
export declare const SESSION_CLOCK_TOLERANCE_SECONDS = 20;
|
|
17
|
+
export type SessionTokenFailure = "missing_token" | "missing_secret" | "wrong_algorithm" | "invalid_signature" | "expired" | "not_yet_valid" | "wrong_audience" | "wrong_issuer" | "binding_mismatch";
|
|
18
|
+
export interface SessionTokenClaims {
|
|
19
|
+
installationId: string;
|
|
20
|
+
vendorId: string;
|
|
21
|
+
appSlug: string;
|
|
22
|
+
appId: string;
|
|
23
|
+
subject: string;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
issuedAt: number;
|
|
26
|
+
expiresAt: number;
|
|
27
|
+
}
|
|
28
|
+
export interface SessionTokenBinding {
|
|
29
|
+
installationId: string;
|
|
30
|
+
vendorId: string;
|
|
31
|
+
appSlug: string;
|
|
32
|
+
appId: string;
|
|
33
|
+
}
|
|
34
|
+
export interface VerifySessionTokenOptions {
|
|
35
|
+
/** The per-installation `embsec_…` secret, raw UTF-8 bytes (never decoded). */
|
|
36
|
+
secret: string;
|
|
37
|
+
/** Single expected audience: the app slug. */
|
|
38
|
+
audience: string;
|
|
39
|
+
/** Expected issuer: the Queek api_base. */
|
|
40
|
+
issuer: string;
|
|
41
|
+
/**
|
|
42
|
+
* The installation row the token must belong to (the app backend's own
|
|
43
|
+
* record). Every field is compared — a token minted for another
|
|
44
|
+
* installation fails closed even when its signature and audience check
|
|
45
|
+
* out, mirroring the backend `verify()`.
|
|
46
|
+
*/
|
|
47
|
+
expected: SessionTokenBinding;
|
|
48
|
+
clockToleranceSeconds?: number;
|
|
49
|
+
}
|
|
50
|
+
export declare function verifySessionToken(token: string, options: VerifySessionTokenOptions): Promise<boolean>;
|
|
51
|
+
export declare function verifySessionTokenDetailed(token: string, options: VerifySessionTokenOptions): Promise<{
|
|
52
|
+
ok: true;
|
|
53
|
+
claims: SessionTokenClaims;
|
|
54
|
+
} | {
|
|
55
|
+
ok: false;
|
|
56
|
+
reason: SessionTokenFailure;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* ROUTING HINT ONLY — the `installation_id` claim read WITHOUT verifying
|
|
60
|
+
* anything, so a server can load the installation whose embed secret then
|
|
61
|
+
* verifies the token with `verifySessionToken`. Never trust it on its
|
|
62
|
+
* own: a caller that skips the verify has no authentication at all.
|
|
63
|
+
* Null for anything that is not a decodable JWT carrying the claim.
|
|
64
|
+
*/
|
|
65
|
+
export declare function sessionTokenInstallationId(token: string): string | null;
|
|
66
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C,kFAAkF;AAClF,eAAO,MAAM,iBAAiB,UAAU,CAAC;AAEzC,mEAAmE;AACnE,eAAO,MAAM,+BAA+B,KAAK,CAAC;AAElD,MAAM,MAAM,mBAAmB,GAC3B,eAAe,GACf,gBAAgB,GAChB,iBAAiB,GACjB,mBAAmB,GACnB,SAAS,GACT,eAAe,GACf,gBAAgB,GAChB,cAAc,GACd,kBAAkB,CAAC;AAEvB,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,yBAAyB;IACxC,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,OAAO,CAAC,CAGlB;AAED,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,mBAAmB,CAAA;CAAE,CAAC,CA2ChG;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOvE"}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { decodeJwt, decodeProtectedHeader, jwtVerify } from "jose";
|
|
2
|
+
/**
|
|
3
|
+
* Dashboard session tokens (S4 stage 2): short-lived HS256 JWTs the backend
|
|
4
|
+
* mints per installation for the framed merchant page. The app backend
|
|
5
|
+
* verifies them with the per-installation `embsec_…` secret before trusting
|
|
6
|
+
* any call that carries one.
|
|
7
|
+
*
|
|
8
|
+
* Server-only: the secret must never enter a browser bundle, so this module
|
|
9
|
+
* ships behind the `./server` export, not the main entry. Style mirrors
|
|
10
|
+
* `signatures.ts` — a boolean fast path plus a detailed variant returning
|
|
11
|
+
* `{ ok } | { ok: false, reason }`.
|
|
12
|
+
*/
|
|
13
|
+
export const EMBED_SECRET_PREFIX = "embsec_";
|
|
14
|
+
/** The only JWS algorithm the dashboard mints (HS256 over the embsec_ secret). */
|
|
15
|
+
export const SESSION_TOKEN_ALG = "HS256";
|
|
16
|
+
/** Backend skew the verifier tolerates (matches the mint side). */
|
|
17
|
+
export const SESSION_CLOCK_TOLERANCE_SECONDS = 20;
|
|
18
|
+
export async function verifySessionToken(token, options) {
|
|
19
|
+
const result = await verifySessionTokenDetailed(token, options);
|
|
20
|
+
return result.ok;
|
|
21
|
+
}
|
|
22
|
+
export async function verifySessionTokenDetailed(token, options) {
|
|
23
|
+
if (!token) {
|
|
24
|
+
return { ok: false, reason: "missing_token" };
|
|
25
|
+
}
|
|
26
|
+
const { secret, audience, issuer } = options;
|
|
27
|
+
if (!secret?.startsWith(EMBED_SECRET_PREFIX)) {
|
|
28
|
+
return { ok: false, reason: "missing_secret" };
|
|
29
|
+
}
|
|
30
|
+
// Fail closed on the algorithm before jose sees the token: a foreign alg
|
|
31
|
+
// (none, RS256, …) is rejected here with wrong_algorithm regardless of
|
|
32
|
+
// how jose shapes the downstream error (key-type TypeError vs
|
|
33
|
+
// JOSEAlgNotAllowed). An undecodable header falls through to jwtVerify,
|
|
34
|
+
// which rejects it as invalid_signature.
|
|
35
|
+
const headerAlg = readTokenAlg(token);
|
|
36
|
+
if (headerAlg !== null && headerAlg !== SESSION_TOKEN_ALG) {
|
|
37
|
+
return { ok: false, reason: "wrong_algorithm" };
|
|
38
|
+
}
|
|
39
|
+
let payload;
|
|
40
|
+
try {
|
|
41
|
+
const verified = await jwtVerify(token, new TextEncoder().encode(secret), {
|
|
42
|
+
algorithms: ["HS256"],
|
|
43
|
+
audience,
|
|
44
|
+
issuer,
|
|
45
|
+
clockTolerance: options.clockToleranceSeconds ?? SESSION_CLOCK_TOLERANCE_SECONDS,
|
|
46
|
+
});
|
|
47
|
+
payload = verified.payload;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return { ok: false, reason: classifyJoseError(error) };
|
|
51
|
+
}
|
|
52
|
+
const claims = readSessionClaims(payload);
|
|
53
|
+
const { expected } = options;
|
|
54
|
+
if (!claims ||
|
|
55
|
+
claims.installationId !== expected.installationId ||
|
|
56
|
+
claims.vendorId !== expected.vendorId ||
|
|
57
|
+
claims.appSlug !== expected.appSlug ||
|
|
58
|
+
claims.appId !== expected.appId) {
|
|
59
|
+
return { ok: false, reason: "binding_mismatch" };
|
|
60
|
+
}
|
|
61
|
+
return { ok: true, claims };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* ROUTING HINT ONLY — the `installation_id` claim read WITHOUT verifying
|
|
65
|
+
* anything, so a server can load the installation whose embed secret then
|
|
66
|
+
* verifies the token with `verifySessionToken`. Never trust it on its
|
|
67
|
+
* own: a caller that skips the verify has no authentication at all.
|
|
68
|
+
* Null for anything that is not a decodable JWT carrying the claim.
|
|
69
|
+
*/
|
|
70
|
+
export function sessionTokenInstallationId(token) {
|
|
71
|
+
try {
|
|
72
|
+
const claim = decodeJwt(token).installation_id;
|
|
73
|
+
return typeof claim === "string" && claim.length > 0 ? claim : null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The token's `alg` without verifying anything. Returns null when the
|
|
81
|
+
* header is missing or undecodable, so malformed tokens keep flowing to
|
|
82
|
+
* jwtVerify (invalid_signature) instead of being mislabelled.
|
|
83
|
+
*/
|
|
84
|
+
function readTokenAlg(token) {
|
|
85
|
+
try {
|
|
86
|
+
const alg = decodeProtectedHeader(token).alg;
|
|
87
|
+
return typeof alg === "string" ? alg : null;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function readSessionClaims(payload) {
|
|
94
|
+
const pick = (key) => typeof payload[key] === "string" && payload[key].length > 0 ? payload[key] : null;
|
|
95
|
+
const installationId = pick("installation_id");
|
|
96
|
+
const vendorId = pick("vendor_id");
|
|
97
|
+
const appSlug = pick("app_slug");
|
|
98
|
+
const appId = pick("app_id");
|
|
99
|
+
const subject = typeof payload.sub === "string" ? payload.sub : null;
|
|
100
|
+
const sessionId = pick("sid");
|
|
101
|
+
const issuedAt = typeof payload.iat === "number" ? payload.iat : null;
|
|
102
|
+
const expiresAt = typeof payload.exp === "number" ? payload.exp : null;
|
|
103
|
+
if (!installationId ||
|
|
104
|
+
!vendorId ||
|
|
105
|
+
!appSlug ||
|
|
106
|
+
!appId ||
|
|
107
|
+
!subject ||
|
|
108
|
+
!sessionId ||
|
|
109
|
+
issuedAt === null ||
|
|
110
|
+
expiresAt === null) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return { installationId, vendorId, appSlug, appId, subject, sessionId, issuedAt, expiresAt };
|
|
114
|
+
}
|
|
115
|
+
function classifyJoseError(error) {
|
|
116
|
+
// Exact jose v6 pins (verified against jose@6.2.12 — codes, names, and
|
|
117
|
+
// the `claim` field probed, not guessed): claim failures carry code
|
|
118
|
+
// ERR_JWT_CLAIM_VALIDATION_FAILED and name the claim; expiry also
|
|
119
|
+
// surfaces as JWTExpired / ERR_JWT_EXPIRED; algorithm refusal is
|
|
120
|
+
// JOSEAlgNotAllowed / ERR_JOSE_ALG_NOT_ALLOWED (unreachable in practice —
|
|
121
|
+
// the header pre-check fails closed first). Anything else — JWSInvalid,
|
|
122
|
+
// JWSSignatureVerificationFailed, key TypeErrors, garbage input — is an
|
|
123
|
+
// invalid signature. No fuzzy matching: a renamed jose error must fall
|
|
124
|
+
// through loudly, not hide behind a regex.
|
|
125
|
+
const shaped = error;
|
|
126
|
+
const claim = typeof shaped?.claim === "string" ? shaped.claim : "";
|
|
127
|
+
if (claim === "exp") {
|
|
128
|
+
return "expired";
|
|
129
|
+
}
|
|
130
|
+
if (claim === "nbf") {
|
|
131
|
+
return "not_yet_valid";
|
|
132
|
+
}
|
|
133
|
+
if (claim === "aud") {
|
|
134
|
+
return "wrong_audience";
|
|
135
|
+
}
|
|
136
|
+
if (claim === "iss") {
|
|
137
|
+
return "wrong_issuer";
|
|
138
|
+
}
|
|
139
|
+
const code = typeof shaped?.code === "string" ? shaped.code : "";
|
|
140
|
+
if (code === "ERR_JWT_EXPIRED") {
|
|
141
|
+
return "expired";
|
|
142
|
+
}
|
|
143
|
+
if (code === "ERR_JOSE_ALG_NOT_ALLOWED") {
|
|
144
|
+
return "wrong_algorithm";
|
|
145
|
+
}
|
|
146
|
+
const name = typeof shaped?.name === "string" ? shaped.name : "";
|
|
147
|
+
if (name === "JWTExpired") {
|
|
148
|
+
return "expired";
|
|
149
|
+
}
|
|
150
|
+
if (name === "JOSEAlgNotAllowed") {
|
|
151
|
+
return "wrong_algorithm";
|
|
152
|
+
}
|
|
153
|
+
return "invalid_signature";
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard Webhooks verification, byte-for-byte compatible with Queek's
|
|
3
|
+
* `App\Services\Webhooks\WebhookSigner` (queek_backend).
|
|
4
|
+
*
|
|
5
|
+
* Signed content: `{id}.{timestamp}.{body}` where `body` is the RAW request
|
|
6
|
+
* bytes. Re-serialising JSON before verifying breaks the MAC — verify first,
|
|
7
|
+
* parse second.
|
|
8
|
+
*
|
|
9
|
+
* The MAC key is the DECODED bytes of the `whsec_<base64>` secret, not the
|
|
10
|
+
* printable string. A secret without the prefix (or with undecodable base64)
|
|
11
|
+
* falls back to the raw string, exactly like the backend's `keyFor()`.
|
|
12
|
+
*
|
|
13
|
+
* Header names: `webhook-id`, `webhook-timestamp`, `webhook-signature`.
|
|
14
|
+
* The signature header may carry several space-delimited `v1,<base64>`
|
|
15
|
+
* signatures (rotation grace window): ANY match verifies.
|
|
16
|
+
*/
|
|
17
|
+
export declare const WEBHOOK_ID_HEADER = "webhook-id";
|
|
18
|
+
export declare const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
|
|
19
|
+
export declare const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
|
|
20
|
+
export declare const SECRET_PREFIX = "whsec_";
|
|
21
|
+
/** Maximum age of a delivery before it is rejected as stale (5 minutes, the Standard Webhooks default). */
|
|
22
|
+
export declare const MAX_TIMESTAMP_SKEW_SECONDS: number;
|
|
23
|
+
export interface SignatureInput {
|
|
24
|
+
/** Value of the `webhook-id` header. */
|
|
25
|
+
id: string;
|
|
26
|
+
/** Value of the `webhook-timestamp` header (unix seconds, as sent). */
|
|
27
|
+
timestamp: string;
|
|
28
|
+
/** Raw request body bytes, untouched. */
|
|
29
|
+
body: string;
|
|
30
|
+
/** Value of the `webhook-signature` header (one or more space-delimited signatures). */
|
|
31
|
+
signatureHeader: string;
|
|
32
|
+
/** The endpoint/app `whsec_…` secret. */
|
|
33
|
+
secret: string;
|
|
34
|
+
}
|
|
35
|
+
export interface VerifyOptions {
|
|
36
|
+
/** Unix seconds to judge freshness against (defaults to now). Exposed for tests. */
|
|
37
|
+
nowSeconds?: number;
|
|
38
|
+
/** Allowed clock skew in seconds (defaults to MAX_TIMESTAMP_SKEW_SECONDS). */
|
|
39
|
+
maxSkewSeconds?: number;
|
|
40
|
+
/** Skip the freshness check (NOT recommended; exposed for replaying stored fixtures). */
|
|
41
|
+
skipFreshnessCheck?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare function secretKeyBytes(secret: string): Buffer;
|
|
44
|
+
/** One `v1,<base64>` signature for one secret — mirrors `WebhookSigner::sign()`. */
|
|
45
|
+
export declare function signQueekPayload(eventId: string, timestamp: number | string, body: string, secret: string): string;
|
|
46
|
+
/** Failure reason when verification rejects a delivery. */
|
|
47
|
+
export type SignatureFailure = "missing_headers" | "stale_timestamp" | "signature_mismatch";
|
|
48
|
+
export declare function verifyQueekSignature(input: SignatureInput, options?: VerifyOptions): boolean;
|
|
49
|
+
export declare function verifyQueekSignatureDetailed(input: SignatureInput, options?: VerifyOptions): {
|
|
50
|
+
ok: true;
|
|
51
|
+
} | {
|
|
52
|
+
ok: false;
|
|
53
|
+
reason: SignatureFailure;
|
|
54
|
+
};
|
|
55
|
+
//# sourceMappingURL=signatures.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signatures.d.ts","sourceRoot":"","sources":["../src/signatures.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAC9C,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,eAAO,MAAM,wBAAwB,sBAAsB,CAAC;AAC5D,eAAO,MAAM,aAAa,WAAW,CAAC;AAEtC,2GAA2G;AAC3G,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD,MAAM,WAAW,cAAc;IAC7B,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,wFAAwF;IACxF,eAAe,EAAE,MAAM,CAAC;IACxB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yFAAyF;IACzF,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAarD;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAAG,MAAM,EAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,MAAM,CAKR;AAED,2DAA2D;AAC3D,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAE5F,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAEhG;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,cAAc,EACrB,OAAO,GAAE,aAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAyBxD"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Standard Webhooks verification, byte-for-byte compatible with Queek's
|
|
4
|
+
* `App\Services\Webhooks\WebhookSigner` (queek_backend).
|
|
5
|
+
*
|
|
6
|
+
* Signed content: `{id}.{timestamp}.{body}` where `body` is the RAW request
|
|
7
|
+
* bytes. Re-serialising JSON before verifying breaks the MAC — verify first,
|
|
8
|
+
* parse second.
|
|
9
|
+
*
|
|
10
|
+
* The MAC key is the DECODED bytes of the `whsec_<base64>` secret, not the
|
|
11
|
+
* printable string. A secret without the prefix (or with undecodable base64)
|
|
12
|
+
* falls back to the raw string, exactly like the backend's `keyFor()`.
|
|
13
|
+
*
|
|
14
|
+
* Header names: `webhook-id`, `webhook-timestamp`, `webhook-signature`.
|
|
15
|
+
* The signature header may carry several space-delimited `v1,<base64>`
|
|
16
|
+
* signatures (rotation grace window): ANY match verifies.
|
|
17
|
+
*/
|
|
18
|
+
export const WEBHOOK_ID_HEADER = "webhook-id";
|
|
19
|
+
export const WEBHOOK_TIMESTAMP_HEADER = "webhook-timestamp";
|
|
20
|
+
export const WEBHOOK_SIGNATURE_HEADER = "webhook-signature";
|
|
21
|
+
export const SECRET_PREFIX = "whsec_";
|
|
22
|
+
/** Maximum age of a delivery before it is rejected as stale (5 minutes, the Standard Webhooks default). */
|
|
23
|
+
export const MAX_TIMESTAMP_SKEW_SECONDS = 5 * 60;
|
|
24
|
+
export function secretKeyBytes(secret) {
|
|
25
|
+
const encoded = secret.startsWith(SECRET_PREFIX) ? secret.slice(SECRET_PREFIX.length) : secret;
|
|
26
|
+
try {
|
|
27
|
+
const decoded = Buffer.from(encoded, "base64");
|
|
28
|
+
// Round-trip guard: the backend uses `base64_decode($encoded, true) ?: $secret`,
|
|
29
|
+
// i.e. undecodable input falls back to the raw secret string.
|
|
30
|
+
if (decoded.length > 0 && decoded.toString("base64").replace(/=+$/, "") === encoded.replace(/=+$/, "")) {
|
|
31
|
+
return decoded;
|
|
32
|
+
}
|
|
33
|
+
return Buffer.from(secret, "utf8");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return Buffer.from(secret, "utf8");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** One `v1,<base64>` signature for one secret — mirrors `WebhookSigner::sign()`. */
|
|
40
|
+
export function signQueekPayload(eventId, timestamp, body, secret) {
|
|
41
|
+
const mac = createHmac("sha256", secretKeyBytes(secret))
|
|
42
|
+
.update(`${eventId}.${timestamp}.${body}`, "utf8")
|
|
43
|
+
.digest();
|
|
44
|
+
return `v1,${mac.toString("base64")}`;
|
|
45
|
+
}
|
|
46
|
+
export function verifyQueekSignature(input, options = {}) {
|
|
47
|
+
return verifyQueekSignatureDetailed(input, options).ok;
|
|
48
|
+
}
|
|
49
|
+
export function verifyQueekSignatureDetailed(input, options = {}) {
|
|
50
|
+
const { id, timestamp, body, signatureHeader, secret } = input;
|
|
51
|
+
if (!id || !timestamp || !signatureHeader || !secret) {
|
|
52
|
+
return { ok: false, reason: "missing_headers" };
|
|
53
|
+
}
|
|
54
|
+
if (!options.skipFreshnessCheck) {
|
|
55
|
+
const ts = Number(timestamp);
|
|
56
|
+
const now = options.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
57
|
+
const maxSkew = options.maxSkewSeconds ?? MAX_TIMESTAMP_SKEW_SECONDS;
|
|
58
|
+
if (!Number.isFinite(ts) || Math.abs(now - ts) > maxSkew) {
|
|
59
|
+
return { ok: false, reason: "stale_timestamp" };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const expected = signQueekPayload(id, timestamp, body, secret);
|
|
63
|
+
const expectedBuf = Buffer.from(expected, "utf8");
|
|
64
|
+
for (const candidate of signatureHeader.trim().split(/\s+/)) {
|
|
65
|
+
if (candidate === "")
|
|
66
|
+
continue;
|
|
67
|
+
const candidateBuf = Buffer.from(candidate, "utf8");
|
|
68
|
+
if (candidateBuf.length === expectedBuf.length && timingSafeEqual(candidateBuf, expectedBuf)) {
|
|
69
|
+
return { ok: true };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { ok: false, reason: "signature_mismatch" };
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=signatures.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signatures.js","sourceRoot":"","sources":["../src/signatures.ts"],"names":[],"mappings":"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"}
|