@cstar.help/js 0.13.0 → 0.15.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/dist/auth/index.cjs +73 -11
- package/dist/auth/index.d.cts +121 -1
- package/dist/auth/index.d.ts +121 -1
- package/dist/auth/index.js +59 -10
- package/dist/chat/index.cjs +64 -7
- package/dist/chat/index.d.cts +48 -4
- package/dist/chat/index.d.ts +48 -4
- package/dist/chat/index.js +64 -7
- package/dist/community/index.cjs +73 -11
- package/dist/community/index.d.cts +121 -1
- package/dist/community/index.d.ts +121 -1
- package/dist/community/index.js +59 -10
- package/dist/index.cjs +75 -11
- package/dist/index.d.cts +86 -26
- package/dist/index.d.ts +86 -26
- package/dist/index.js +75 -11
- package/dist/library/index.cjs +73 -11
- package/dist/library/index.d.cts +121 -1
- package/dist/library/index.d.ts +121 -1
- package/dist/library/index.js +59 -10
- package/dist/quickhelp/index.cjs +179 -17
- package/dist/quickhelp/index.d.cts +147 -5
- package/dist/quickhelp/index.d.ts +147 -5
- package/dist/quickhelp/index.js +165 -16
- package/dist/webhook/index.cjs +64 -8
- package/dist/webhook/index.d.cts +27 -3
- package/dist/webhook/index.d.ts +27 -3
- package/dist/webhook/index.js +62 -8
- package/dist/webhook/node.cjs +53 -5
- package/dist/webhook/node.d.cts +27 -4
- package/dist/webhook/node.d.ts +27 -4
- package/dist/webhook/node.js +51 -5
- package/package.json +1 -1
package/dist/quickhelp/index.js
CHANGED
|
@@ -35,7 +35,7 @@ function normalizeArgs(messageOrStatus, maybeBody, defaults) {
|
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
-
var
|
|
38
|
+
var CStarErrorImpl = class extends Error {
|
|
39
39
|
constructor(messageOrStatus, body) {
|
|
40
40
|
const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body);
|
|
41
41
|
super(resolved.message);
|
|
@@ -48,7 +48,7 @@ var CStarError = class extends Error {
|
|
|
48
48
|
this.statusCode = statusCode;
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
-
var
|
|
51
|
+
var CStarAuthenticationErrorImpl = class extends CStarErrorImpl {
|
|
52
52
|
constructor(messageOrStatus, body) {
|
|
53
53
|
const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
|
|
54
54
|
type: ErrorType.AUTHENTICATION_ERROR,
|
|
@@ -59,7 +59,7 @@ var CStarAuthenticationError = class extends CStarError {
|
|
|
59
59
|
this.name = "CStarAuthenticationError";
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
|
-
var
|
|
62
|
+
var CStarRateLimitErrorImpl = class extends CStarErrorImpl {
|
|
63
63
|
constructor(messageOrStatus, body, retryAfter) {
|
|
64
64
|
const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
|
|
65
65
|
type: ErrorType.RATE_LIMIT_ERROR,
|
|
@@ -71,7 +71,7 @@ var CStarRateLimitError = class extends CStarError {
|
|
|
71
71
|
this.retryAfter = retryAfter ?? 60;
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
|
-
var
|
|
74
|
+
var CStarNotFoundErrorImpl = class extends CStarErrorImpl {
|
|
75
75
|
constructor(messageOrStatus, body) {
|
|
76
76
|
const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
|
|
77
77
|
type: ErrorType.NOT_FOUND_ERROR,
|
|
@@ -82,7 +82,7 @@ var CStarNotFoundError = class extends CStarError {
|
|
|
82
82
|
this.name = "CStarNotFoundError";
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
|
-
var
|
|
85
|
+
var CStarValidationErrorImpl = class extends CStarErrorImpl {
|
|
86
86
|
constructor(messageOrStatus, body) {
|
|
87
87
|
const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
|
|
88
88
|
type: ErrorType.INVALID_REQUEST_ERROR,
|
|
@@ -93,7 +93,7 @@ var CStarValidationError = class extends CStarError {
|
|
|
93
93
|
this.name = "CStarValidationError";
|
|
94
94
|
}
|
|
95
95
|
};
|
|
96
|
-
var
|
|
96
|
+
var CStarPermissionErrorImpl = class extends CStarErrorImpl {
|
|
97
97
|
constructor(messageOrStatus, body) {
|
|
98
98
|
const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
|
|
99
99
|
type: ErrorType.PERMISSION_ERROR,
|
|
@@ -104,7 +104,7 @@ var CStarPermissionError = class extends CStarError {
|
|
|
104
104
|
this.name = "CStarPermissionError";
|
|
105
105
|
}
|
|
106
106
|
};
|
|
107
|
-
var
|
|
107
|
+
var CStarConflictErrorImpl = class extends CStarErrorImpl {
|
|
108
108
|
constructor(messageOrStatus, body) {
|
|
109
109
|
const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
|
|
110
110
|
type: ErrorType.CONFLICT_ERROR,
|
|
@@ -115,7 +115,7 @@ var CStarConflictError = class extends CStarError {
|
|
|
115
115
|
this.name = "CStarConflictError";
|
|
116
116
|
}
|
|
117
117
|
};
|
|
118
|
-
var
|
|
118
|
+
var CStarServerErrorImpl = class extends CStarErrorImpl {
|
|
119
119
|
constructor(messageOrStatus, body) {
|
|
120
120
|
const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
|
|
121
121
|
type: ErrorType.SERVER_ERROR,
|
|
@@ -126,7 +126,7 @@ var CStarServerError = class extends CStarError {
|
|
|
126
126
|
this.name = "CStarServerError";
|
|
127
127
|
}
|
|
128
128
|
};
|
|
129
|
-
var
|
|
129
|
+
var CStarFeatureDisabledErrorImpl = class extends CStarErrorImpl {
|
|
130
130
|
constructor(messageOrStatus, body) {
|
|
131
131
|
const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
|
|
132
132
|
type: ErrorType.FEATURE_NOT_CONFIGURED,
|
|
@@ -139,6 +139,82 @@ var CStarFeatureDisabledError = class extends CStarError {
|
|
|
139
139
|
this.enableUrl = resolved.enable_url ?? "";
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
|
+
var CStarRealtimeUnavailableErrorImpl = class extends CStarErrorImpl {
|
|
143
|
+
constructor(message) {
|
|
144
|
+
const resolved = message ?? "EventSource is not available in this environment. Real-time subscriptions only work in the browser. Move the call to a client-only path (`useEffect` / `onMount`), or pass an `eventSource` polyfill via the realtime options. See https://cstar.help/developers/sdk/realtime#ssr";
|
|
145
|
+
super(0, {
|
|
146
|
+
type: ErrorType.API_ERROR,
|
|
147
|
+
code: "realtime_unavailable",
|
|
148
|
+
message: resolved,
|
|
149
|
+
doc_url: "https://cstar.help/developers/sdk/realtime#ssr"
|
|
150
|
+
});
|
|
151
|
+
this.name = "CStarRealtimeUnavailableError";
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@cstar.help/js/errors/v1");
|
|
155
|
+
var globalSlot = globalThis;
|
|
156
|
+
var registry = globalSlot[REGISTRY_KEY] ?? (globalSlot[REGISTRY_KEY] = {
|
|
157
|
+
CStarError: CStarErrorImpl,
|
|
158
|
+
CStarAuthenticationError: CStarAuthenticationErrorImpl,
|
|
159
|
+
CStarRateLimitError: CStarRateLimitErrorImpl,
|
|
160
|
+
CStarNotFoundError: CStarNotFoundErrorImpl,
|
|
161
|
+
CStarValidationError: CStarValidationErrorImpl,
|
|
162
|
+
CStarPermissionError: CStarPermissionErrorImpl,
|
|
163
|
+
CStarConflictError: CStarConflictErrorImpl,
|
|
164
|
+
CStarServerError: CStarServerErrorImpl,
|
|
165
|
+
CStarFeatureDisabledError: CStarFeatureDisabledErrorImpl,
|
|
166
|
+
CStarRealtimeUnavailableError: CStarRealtimeUnavailableErrorImpl
|
|
167
|
+
});
|
|
168
|
+
var CStarError = registry.CStarError;
|
|
169
|
+
var CStarAuthenticationError = registry.CStarAuthenticationError;
|
|
170
|
+
var CStarRateLimitError = registry.CStarRateLimitError;
|
|
171
|
+
var CStarNotFoundError = registry.CStarNotFoundError;
|
|
172
|
+
var CStarValidationError = registry.CStarValidationError;
|
|
173
|
+
var CStarPermissionError = registry.CStarPermissionError;
|
|
174
|
+
var CStarConflictError = registry.CStarConflictError;
|
|
175
|
+
var CStarServerError = registry.CStarServerError;
|
|
176
|
+
var CStarFeatureDisabledError = registry.CStarFeatureDisabledError;
|
|
177
|
+
var CStarRealtimeUnavailableError = registry.CStarRealtimeUnavailableError;
|
|
178
|
+
function parsePublicError(status, body, fallbackMessage) {
|
|
179
|
+
const obj = body && typeof body === "object" ? body : null;
|
|
180
|
+
const errVal = obj?.error;
|
|
181
|
+
let message = fallbackMessage;
|
|
182
|
+
let structured = null;
|
|
183
|
+
if (typeof errVal === "string") {
|
|
184
|
+
message = errVal;
|
|
185
|
+
} else if (errVal && typeof errVal === "object") {
|
|
186
|
+
const e = errVal;
|
|
187
|
+
if (typeof e.message === "string") message = e.message;
|
|
188
|
+
structured = {
|
|
189
|
+
type: typeof e.type === "string" ? e.type : ErrorType.API_ERROR,
|
|
190
|
+
code: typeof e.code === "string" ? e.code : "api_error",
|
|
191
|
+
message,
|
|
192
|
+
param: typeof e.param === "string" ? e.param : void 0,
|
|
193
|
+
doc_url: typeof e.doc_url === "string" ? e.doc_url : DEFAULT_DOC_URL,
|
|
194
|
+
request_id: typeof e.request_id === "string" ? e.request_id : void 0,
|
|
195
|
+
feature: typeof e.feature === "string" ? e.feature : void 0,
|
|
196
|
+
enable_url: typeof e.enable_url === "string" ? e.enable_url : void 0
|
|
197
|
+
};
|
|
198
|
+
} else if (typeof obj?.message === "string") {
|
|
199
|
+
message = obj.message;
|
|
200
|
+
}
|
|
201
|
+
const errorBody = structured ?? {
|
|
202
|
+
type: ErrorType.API_ERROR,
|
|
203
|
+
code: status >= 500 ? "server_error" : "request_failed",
|
|
204
|
+
message,
|
|
205
|
+
doc_url: DEFAULT_DOC_URL
|
|
206
|
+
};
|
|
207
|
+
return parseError(status, { error: errorBody });
|
|
208
|
+
}
|
|
209
|
+
var STATUS_TO_CLASS = {
|
|
210
|
+
400: CStarValidationError,
|
|
211
|
+
401: CStarAuthenticationError,
|
|
212
|
+
403: CStarPermissionError,
|
|
213
|
+
404: CStarNotFoundError,
|
|
214
|
+
409: CStarConflictError,
|
|
215
|
+
422: CStarValidationError,
|
|
216
|
+
429: CStarRateLimitError
|
|
217
|
+
};
|
|
142
218
|
var TYPE_TO_CLASS = {
|
|
143
219
|
[ErrorType.AUTHENTICATION_ERROR]: CStarAuthenticationError,
|
|
144
220
|
[ErrorType.PERMISSION_ERROR]: CStarPermissionError,
|
|
@@ -149,16 +225,41 @@ var TYPE_TO_CLASS = {
|
|
|
149
225
|
[ErrorType.SERVER_ERROR]: CStarServerError,
|
|
150
226
|
[ErrorType.FEATURE_NOT_CONFIGURED]: CStarFeatureDisabledError
|
|
151
227
|
};
|
|
228
|
+
function parseError(statusCode, body, retryAfter) {
|
|
229
|
+
const errorBody = body?.error ?? {
|
|
230
|
+
type: ErrorType.API_ERROR,
|
|
231
|
+
code: "unknown_error",
|
|
232
|
+
message: `Request failed with status ${statusCode}`,
|
|
233
|
+
doc_url: DEFAULT_DOC_URL
|
|
234
|
+
};
|
|
235
|
+
if (statusCode === 429) {
|
|
236
|
+
return new CStarRateLimitError(statusCode, errorBody, retryAfter ?? 60);
|
|
237
|
+
}
|
|
238
|
+
const StatusCls = STATUS_TO_CLASS[statusCode];
|
|
239
|
+
if (StatusCls) {
|
|
240
|
+
return new StatusCls(statusCode, errorBody);
|
|
241
|
+
}
|
|
242
|
+
const TypeCls = TYPE_TO_CLASS[errorBody.type];
|
|
243
|
+
if (TypeCls) {
|
|
244
|
+
return new TypeCls(statusCode, errorBody);
|
|
245
|
+
}
|
|
246
|
+
if (statusCode >= 500) return new CStarServerError(statusCode, errorBody);
|
|
247
|
+
return new CStarError(statusCode, errorBody);
|
|
248
|
+
}
|
|
152
249
|
|
|
153
250
|
// src/quickhelp/index.ts
|
|
154
251
|
var DEFAULT_BASE_URL = "https://www.cstar.help";
|
|
155
252
|
var QuickHelpClient = class {
|
|
156
253
|
constructor(config) {
|
|
157
|
-
|
|
158
|
-
|
|
254
|
+
this.resolveTeamIdPromise = null;
|
|
255
|
+
if (!config?.teamId && !config?.teamSlug) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
'QuickHelpClient: either `teamId` or `teamSlug` is required. Pass `teamSlug` (e.g. "acme") for the easiest setup.'
|
|
258
|
+
);
|
|
159
259
|
}
|
|
160
260
|
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
161
|
-
this.teamId = config.teamId;
|
|
261
|
+
this.teamId = config.teamId ?? null;
|
|
262
|
+
this.teamSlug = config.teamSlug ?? null;
|
|
162
263
|
this.publishableKey = config.publishableKey ?? null;
|
|
163
264
|
this.mode = this.publishableKey?.startsWith("pk_test_") ? "test" : "live";
|
|
164
265
|
if (this.publishableKey && !/^pk_(test|live)_/.test(this.publishableKey)) {
|
|
@@ -170,6 +271,39 @@ var QuickHelpClient = class {
|
|
|
170
271
|
authHeaders() {
|
|
171
272
|
return this.publishableKey ? { Authorization: `Bearer ${this.publishableKey}` } : {};
|
|
172
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* Resolve `teamSlug` → `teamId` lazily on the first call that needs it.
|
|
276
|
+
* Cached for the life of the client. NEW-v2-05.
|
|
277
|
+
*/
|
|
278
|
+
async resolveTeamId() {
|
|
279
|
+
if (this.teamId) return this.teamId;
|
|
280
|
+
if (!this.teamSlug) throw new Error("QuickHelpClient: no teamId or teamSlug set");
|
|
281
|
+
if (this.resolveTeamIdPromise) return this.resolveTeamIdPromise;
|
|
282
|
+
this.resolveTeamIdPromise = (async () => {
|
|
283
|
+
const url = `${this.baseUrl}/api/widget/team-by-slug?teamSlug=${encodeURIComponent(this.teamSlug)}`;
|
|
284
|
+
const res = await globalThis.fetch(url, { method: "GET", headers: this.authHeaders() });
|
|
285
|
+
if (!res.ok) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`QuickHelpClient: could not resolve teamSlug "${this.teamSlug}" (HTTP ${res.status}).`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const body = await res.json();
|
|
291
|
+
const id = body?.data?.teamId;
|
|
292
|
+
if (!id) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`QuickHelpClient: lookup for teamSlug "${this.teamSlug}" returned no teamId.`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
this.teamId = id;
|
|
298
|
+
return id;
|
|
299
|
+
})();
|
|
300
|
+
try {
|
|
301
|
+
return await this.resolveTeamIdPromise;
|
|
302
|
+
} catch (err) {
|
|
303
|
+
this.resolveTeamIdPromise = null;
|
|
304
|
+
throw err;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
173
307
|
/**
|
|
174
308
|
* Fetch the sanitized public config (title, subtitle, placeholder,
|
|
175
309
|
* theme overrides). Never includes agent id or escalation contact
|
|
@@ -178,7 +312,8 @@ var QuickHelpClient = class {
|
|
|
178
312
|
* Returns `null` if Quick Help is disabled on the team (404).
|
|
179
313
|
*/
|
|
180
314
|
async config() {
|
|
181
|
-
const
|
|
315
|
+
const teamId = await this.resolveTeamId();
|
|
316
|
+
const url = `${this.baseUrl}/api/quickhelp/config/${encodeURIComponent(teamId)}`;
|
|
182
317
|
const res = await globalThis.fetch(url, { method: "GET", headers: this.authHeaders() });
|
|
183
318
|
if (res.status === 404) return null;
|
|
184
319
|
if (!res.ok) {
|
|
@@ -195,12 +330,13 @@ var QuickHelpClient = class {
|
|
|
195
330
|
* property set from the response body.
|
|
196
331
|
*/
|
|
197
332
|
async ask(params) {
|
|
333
|
+
const teamId = await this.resolveTeamId();
|
|
198
334
|
const url = `${this.baseUrl}/api/quickhelp/ask`;
|
|
199
335
|
const res = await globalThis.fetch(url, {
|
|
200
336
|
method: "POST",
|
|
201
337
|
headers: { "Content-Type": "application/json", ...this.authHeaders() },
|
|
202
338
|
body: JSON.stringify({
|
|
203
|
-
team_id:
|
|
339
|
+
team_id: teamId,
|
|
204
340
|
session_id: params.sessionId ?? null,
|
|
205
341
|
question: params.question
|
|
206
342
|
})
|
|
@@ -214,7 +350,7 @@ var QuickHelpClient = class {
|
|
|
214
350
|
message: body?.error?.message ?? "Quick Help is not available on this team. Enable an AI Agent (Settings \u2192 AI Agents) and configure Quick Help (Settings \u2192 Quick Help) before calling .ask().",
|
|
215
351
|
doc_url: "https://cstar.help/developers/api/errors#feature-not-configured",
|
|
216
352
|
feature: "quickHelp",
|
|
217
|
-
enable_url: `${this.baseUrl}/team/${encodeURIComponent(
|
|
353
|
+
enable_url: `${this.baseUrl}/team/${encodeURIComponent(teamId)}/settings?section=quickHelp`
|
|
218
354
|
});
|
|
219
355
|
}
|
|
220
356
|
const message = body?.error?.message ?? `Quick Help ask failed: ${res.status}`;
|
|
@@ -243,6 +379,19 @@ function parseRetryAfter(header) {
|
|
|
243
379
|
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
244
380
|
}
|
|
245
381
|
export {
|
|
382
|
+
CStarAuthenticationError,
|
|
383
|
+
CStarConflictError,
|
|
384
|
+
CStarError,
|
|
385
|
+
CStarFeatureDisabledError,
|
|
386
|
+
CStarNotFoundError,
|
|
387
|
+
CStarPermissionError,
|
|
388
|
+
CStarRateLimitError,
|
|
389
|
+
CStarRealtimeUnavailableError,
|
|
390
|
+
CStarServerError,
|
|
391
|
+
CStarValidationError,
|
|
392
|
+
ErrorType,
|
|
246
393
|
QuickHelpClient,
|
|
247
|
-
QuickHelpError
|
|
394
|
+
QuickHelpError,
|
|
395
|
+
parseError,
|
|
396
|
+
parsePublicError
|
|
248
397
|
};
|
package/dist/webhook/index.cjs
CHANGED
|
@@ -20,19 +20,55 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/webhook/index.ts
|
|
21
21
|
var webhook_exports = {};
|
|
22
22
|
__export(webhook_exports, {
|
|
23
|
+
DEFAULT_TOLERANCE_SECONDS: () => DEFAULT_TOLERANCE_SECONDS,
|
|
23
24
|
WebhookSignatureVerificationError: () => WebhookSignatureVerificationError,
|
|
24
25
|
constructEventAsync: () => constructEventAsync,
|
|
26
|
+
signPayloadStripeAsync: () => signPayloadStripeAsync,
|
|
25
27
|
verifySignatureAsync: () => verifySignatureAsync
|
|
26
28
|
});
|
|
27
29
|
module.exports = __toCommonJS(webhook_exports);
|
|
30
|
+
var DEFAULT_TOLERANCE_SECONDS = 300;
|
|
28
31
|
var WebhookSignatureVerificationError = class extends Error {
|
|
29
32
|
constructor(message) {
|
|
30
33
|
super(message);
|
|
31
34
|
this.name = "WebhookSignatureVerificationError";
|
|
32
35
|
}
|
|
33
36
|
};
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
function parseStripeHeader(header) {
|
|
38
|
+
if (!header || header.startsWith("sha256=")) return null;
|
|
39
|
+
let timestamp = null;
|
|
40
|
+
const v1 = [];
|
|
41
|
+
for (const part of header.split(",")) {
|
|
42
|
+
const eq = part.indexOf("=");
|
|
43
|
+
if (eq === -1) continue;
|
|
44
|
+
const key = part.slice(0, eq).trim();
|
|
45
|
+
const value = part.slice(eq + 1).trim();
|
|
46
|
+
if (key === "t") {
|
|
47
|
+
const parsed = Number.parseInt(value, 10);
|
|
48
|
+
if (Number.isFinite(parsed)) timestamp = parsed;
|
|
49
|
+
} else if (key === "v1") {
|
|
50
|
+
v1.push(value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (timestamp === null || v1.length === 0) return null;
|
|
54
|
+
return { timestamp, v1 };
|
|
55
|
+
}
|
|
56
|
+
async function signPayloadStripeAsync(rawBody, secret) {
|
|
57
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
58
|
+
const encoder = new TextEncoder();
|
|
59
|
+
const key = await crypto.subtle.importKey(
|
|
60
|
+
"raw",
|
|
61
|
+
encoder.encode(secret),
|
|
62
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
63
|
+
false,
|
|
64
|
+
["sign"]
|
|
65
|
+
);
|
|
66
|
+
const sigBytes = await crypto.subtle.sign("HMAC", key, encoder.encode(`${ts}.${rawBody}`));
|
|
67
|
+
const hex = Array.from(new Uint8Array(sigBytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
68
|
+
return `t=${ts},v1=${hex}`;
|
|
69
|
+
}
|
|
70
|
+
async function constructEventAsync(rawBody, signature, secret, options) {
|
|
71
|
+
const valid = await verifySignatureAsync(rawBody, signature, secret, options);
|
|
36
72
|
if (!valid) {
|
|
37
73
|
throw new WebhookSignatureVerificationError(
|
|
38
74
|
"Webhook signature verification failed. Ensure you are using the raw request body and the correct endpoint secret."
|
|
@@ -40,7 +76,7 @@ async function constructEventAsync(rawBody, signature, secret) {
|
|
|
40
76
|
}
|
|
41
77
|
return JSON.parse(rawBody);
|
|
42
78
|
}
|
|
43
|
-
async function
|
|
79
|
+
async function hmacHex(material, secret) {
|
|
44
80
|
const encoder = new TextEncoder();
|
|
45
81
|
const key = await crypto.subtle.importKey(
|
|
46
82
|
"raw",
|
|
@@ -49,18 +85,38 @@ async function verifySignatureAsync(rawBody, signature, secret) {
|
|
|
49
85
|
false,
|
|
50
86
|
["sign"]
|
|
51
87
|
);
|
|
52
|
-
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(
|
|
53
|
-
|
|
54
|
-
|
|
88
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(material));
|
|
89
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
90
|
+
}
|
|
91
|
+
function constantTimeEqual(a, b) {
|
|
92
|
+
if (a.length !== b.length) return false;
|
|
55
93
|
let result = 0;
|
|
56
|
-
for (let i = 0; i <
|
|
57
|
-
result |=
|
|
94
|
+
for (let i = 0; i < a.length; i++) {
|
|
95
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
58
96
|
}
|
|
59
97
|
return result === 0;
|
|
60
98
|
}
|
|
99
|
+
async function verifySignatureAsync(rawBody, signature, secret, options) {
|
|
100
|
+
if (typeof signature !== "string" || signature.length === 0) return false;
|
|
101
|
+
const stripe = parseStripeHeader(signature);
|
|
102
|
+
if (stripe) {
|
|
103
|
+
const tolerance = options?.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
|
|
104
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
105
|
+
if (Math.abs(now - stripe.timestamp) > tolerance) return false;
|
|
106
|
+
const expected2 = await hmacHex(`${stripe.timestamp}.${rawBody}`, secret);
|
|
107
|
+
for (const candidate of stripe.v1) {
|
|
108
|
+
if (constantTimeEqual(candidate, expected2)) return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const expected = "sha256=" + await hmacHex(rawBody, secret);
|
|
113
|
+
return constantTimeEqual(signature, expected);
|
|
114
|
+
}
|
|
61
115
|
// Annotate the CommonJS export names for ESM import in node:
|
|
62
116
|
0 && (module.exports = {
|
|
117
|
+
DEFAULT_TOLERANCE_SECONDS,
|
|
63
118
|
WebhookSignatureVerificationError,
|
|
64
119
|
constructEventAsync,
|
|
120
|
+
signPayloadStripeAsync,
|
|
65
121
|
verifySignatureAsync
|
|
66
122
|
});
|
package/dist/webhook/index.d.cts
CHANGED
|
@@ -412,10 +412,27 @@ interface WebhookEvent<T extends WebhookEventType = WebhookEventType> {
|
|
|
412
412
|
data: WebhookEventDataMap[T];
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
/** Default replay window for Stripe-style signatures: 5 minutes (300s). */
|
|
416
|
+
declare const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
417
|
+
/** Options for signature verification — accepted by both verify and construct calls. */
|
|
418
|
+
interface VerifyOptions {
|
|
419
|
+
/**
|
|
420
|
+
* Maximum age (in seconds) of a Stripe-style timestamp before the signature
|
|
421
|
+
* is rejected as expired. Defaults to {@link DEFAULT_TOLERANCE_SECONDS}.
|
|
422
|
+
* Ignored for legacy `sha256=<hex>` signatures (which carry no timestamp).
|
|
423
|
+
*/
|
|
424
|
+
tolerance?: number;
|
|
425
|
+
}
|
|
415
426
|
/** Thrown when webhook signature verification fails. */
|
|
416
427
|
declare class WebhookSignatureVerificationError extends Error {
|
|
417
428
|
constructor(message: string);
|
|
418
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Sign a payload in Stripe-style format `t=<unix>,v1=<hex>` using Web Crypto.
|
|
432
|
+
* The HMAC is computed over `${ts}.${rawBody}`. Used by the cStar server
|
|
433
|
+
* dispatcher and `cstar listen --forward-to`.
|
|
434
|
+
*/
|
|
435
|
+
declare function signPayloadStripeAsync(rawBody: string, secret: string): Promise<string>;
|
|
419
436
|
/**
|
|
420
437
|
* Verify a webhook signature and parse the event.
|
|
421
438
|
*
|
|
@@ -427,14 +444,21 @@ declare class WebhookSignatureVerificationError extends Error {
|
|
|
427
444
|
* @param rawBody - Raw request body as a string (NOT parsed JSON)
|
|
428
445
|
* @param signature - Value of the `X-Signature` header
|
|
429
446
|
* @param secret - Your webhook endpoint's secret (`whsec_*`)
|
|
447
|
+
* @param options - Optional verifier options (e.g. replay tolerance)
|
|
430
448
|
* @returns Parsed and verified webhook event
|
|
431
449
|
* @throws {WebhookSignatureVerificationError} If verification fails
|
|
432
450
|
*/
|
|
433
|
-
declare function constructEventAsync(rawBody: string, signature: string, secret: string): Promise<WebhookEvent>;
|
|
451
|
+
declare function constructEventAsync(rawBody: string, signature: string, secret: string, options?: VerifyOptions): Promise<WebhookEvent>;
|
|
434
452
|
/**
|
|
435
453
|
* Verify an HMAC-SHA256 signature using the Web Crypto API.
|
|
454
|
+
*
|
|
455
|
+
* Accepts both the canonical Stripe-style format `t=<unix>,v1=<hex>` (signed
|
|
456
|
+
* over `${ts}.${rawBody}` with replay-window enforcement) and the legacy
|
|
457
|
+
* `sha256=<hex>` format (signed over `rawBody` only, no timestamp). Use
|
|
458
|
+
* `options.tolerance` to override the default 300s replay window.
|
|
459
|
+
*
|
|
436
460
|
* Constant-time comparison to prevent timing attacks.
|
|
437
461
|
*/
|
|
438
|
-
declare function verifySignatureAsync(rawBody: string, signature: string, secret: string): Promise<boolean>;
|
|
462
|
+
declare function verifySignatureAsync(rawBody: string, signature: string, secret: string, options?: VerifyOptions): Promise<boolean>;
|
|
439
463
|
|
|
440
|
-
export { type WebhookEvent, WebhookSignatureVerificationError, constructEventAsync, verifySignatureAsync };
|
|
464
|
+
export { DEFAULT_TOLERANCE_SECONDS, type VerifyOptions, type WebhookEvent, WebhookSignatureVerificationError, constructEventAsync, signPayloadStripeAsync, verifySignatureAsync };
|
package/dist/webhook/index.d.ts
CHANGED
|
@@ -412,10 +412,27 @@ interface WebhookEvent<T extends WebhookEventType = WebhookEventType> {
|
|
|
412
412
|
data: WebhookEventDataMap[T];
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
+
/** Default replay window for Stripe-style signatures: 5 minutes (300s). */
|
|
416
|
+
declare const DEFAULT_TOLERANCE_SECONDS = 300;
|
|
417
|
+
/** Options for signature verification — accepted by both verify and construct calls. */
|
|
418
|
+
interface VerifyOptions {
|
|
419
|
+
/**
|
|
420
|
+
* Maximum age (in seconds) of a Stripe-style timestamp before the signature
|
|
421
|
+
* is rejected as expired. Defaults to {@link DEFAULT_TOLERANCE_SECONDS}.
|
|
422
|
+
* Ignored for legacy `sha256=<hex>` signatures (which carry no timestamp).
|
|
423
|
+
*/
|
|
424
|
+
tolerance?: number;
|
|
425
|
+
}
|
|
415
426
|
/** Thrown when webhook signature verification fails. */
|
|
416
427
|
declare class WebhookSignatureVerificationError extends Error {
|
|
417
428
|
constructor(message: string);
|
|
418
429
|
}
|
|
430
|
+
/**
|
|
431
|
+
* Sign a payload in Stripe-style format `t=<unix>,v1=<hex>` using Web Crypto.
|
|
432
|
+
* The HMAC is computed over `${ts}.${rawBody}`. Used by the cStar server
|
|
433
|
+
* dispatcher and `cstar listen --forward-to`.
|
|
434
|
+
*/
|
|
435
|
+
declare function signPayloadStripeAsync(rawBody: string, secret: string): Promise<string>;
|
|
419
436
|
/**
|
|
420
437
|
* Verify a webhook signature and parse the event.
|
|
421
438
|
*
|
|
@@ -427,14 +444,21 @@ declare class WebhookSignatureVerificationError extends Error {
|
|
|
427
444
|
* @param rawBody - Raw request body as a string (NOT parsed JSON)
|
|
428
445
|
* @param signature - Value of the `X-Signature` header
|
|
429
446
|
* @param secret - Your webhook endpoint's secret (`whsec_*`)
|
|
447
|
+
* @param options - Optional verifier options (e.g. replay tolerance)
|
|
430
448
|
* @returns Parsed and verified webhook event
|
|
431
449
|
* @throws {WebhookSignatureVerificationError} If verification fails
|
|
432
450
|
*/
|
|
433
|
-
declare function constructEventAsync(rawBody: string, signature: string, secret: string): Promise<WebhookEvent>;
|
|
451
|
+
declare function constructEventAsync(rawBody: string, signature: string, secret: string, options?: VerifyOptions): Promise<WebhookEvent>;
|
|
434
452
|
/**
|
|
435
453
|
* Verify an HMAC-SHA256 signature using the Web Crypto API.
|
|
454
|
+
*
|
|
455
|
+
* Accepts both the canonical Stripe-style format `t=<unix>,v1=<hex>` (signed
|
|
456
|
+
* over `${ts}.${rawBody}` with replay-window enforcement) and the legacy
|
|
457
|
+
* `sha256=<hex>` format (signed over `rawBody` only, no timestamp). Use
|
|
458
|
+
* `options.tolerance` to override the default 300s replay window.
|
|
459
|
+
*
|
|
436
460
|
* Constant-time comparison to prevent timing attacks.
|
|
437
461
|
*/
|
|
438
|
-
declare function verifySignatureAsync(rawBody: string, signature: string, secret: string): Promise<boolean>;
|
|
462
|
+
declare function verifySignatureAsync(rawBody: string, signature: string, secret: string, options?: VerifyOptions): Promise<boolean>;
|
|
439
463
|
|
|
440
|
-
export { type WebhookEvent, WebhookSignatureVerificationError, constructEventAsync, verifySignatureAsync };
|
|
464
|
+
export { DEFAULT_TOLERANCE_SECONDS, type VerifyOptions, type WebhookEvent, WebhookSignatureVerificationError, constructEventAsync, signPayloadStripeAsync, verifySignatureAsync };
|
package/dist/webhook/index.js
CHANGED
|
@@ -1,12 +1,46 @@
|
|
|
1
1
|
// src/webhook/index.ts
|
|
2
|
+
var DEFAULT_TOLERANCE_SECONDS = 300;
|
|
2
3
|
var WebhookSignatureVerificationError = class extends Error {
|
|
3
4
|
constructor(message) {
|
|
4
5
|
super(message);
|
|
5
6
|
this.name = "WebhookSignatureVerificationError";
|
|
6
7
|
}
|
|
7
8
|
};
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
function parseStripeHeader(header) {
|
|
10
|
+
if (!header || header.startsWith("sha256=")) return null;
|
|
11
|
+
let timestamp = null;
|
|
12
|
+
const v1 = [];
|
|
13
|
+
for (const part of header.split(",")) {
|
|
14
|
+
const eq = part.indexOf("=");
|
|
15
|
+
if (eq === -1) continue;
|
|
16
|
+
const key = part.slice(0, eq).trim();
|
|
17
|
+
const value = part.slice(eq + 1).trim();
|
|
18
|
+
if (key === "t") {
|
|
19
|
+
const parsed = Number.parseInt(value, 10);
|
|
20
|
+
if (Number.isFinite(parsed)) timestamp = parsed;
|
|
21
|
+
} else if (key === "v1") {
|
|
22
|
+
v1.push(value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (timestamp === null || v1.length === 0) return null;
|
|
26
|
+
return { timestamp, v1 };
|
|
27
|
+
}
|
|
28
|
+
async function signPayloadStripeAsync(rawBody, secret) {
|
|
29
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
30
|
+
const encoder = new TextEncoder();
|
|
31
|
+
const key = await crypto.subtle.importKey(
|
|
32
|
+
"raw",
|
|
33
|
+
encoder.encode(secret),
|
|
34
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
35
|
+
false,
|
|
36
|
+
["sign"]
|
|
37
|
+
);
|
|
38
|
+
const sigBytes = await crypto.subtle.sign("HMAC", key, encoder.encode(`${ts}.${rawBody}`));
|
|
39
|
+
const hex = Array.from(new Uint8Array(sigBytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
40
|
+
return `t=${ts},v1=${hex}`;
|
|
41
|
+
}
|
|
42
|
+
async function constructEventAsync(rawBody, signature, secret, options) {
|
|
43
|
+
const valid = await verifySignatureAsync(rawBody, signature, secret, options);
|
|
10
44
|
if (!valid) {
|
|
11
45
|
throw new WebhookSignatureVerificationError(
|
|
12
46
|
"Webhook signature verification failed. Ensure you are using the raw request body and the correct endpoint secret."
|
|
@@ -14,7 +48,7 @@ async function constructEventAsync(rawBody, signature, secret) {
|
|
|
14
48
|
}
|
|
15
49
|
return JSON.parse(rawBody);
|
|
16
50
|
}
|
|
17
|
-
async function
|
|
51
|
+
async function hmacHex(material, secret) {
|
|
18
52
|
const encoder = new TextEncoder();
|
|
19
53
|
const key = await crypto.subtle.importKey(
|
|
20
54
|
"raw",
|
|
@@ -23,17 +57,37 @@ async function verifySignatureAsync(rawBody, signature, secret) {
|
|
|
23
57
|
false,
|
|
24
58
|
["sign"]
|
|
25
59
|
);
|
|
26
|
-
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(
|
|
27
|
-
|
|
28
|
-
|
|
60
|
+
const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(material));
|
|
61
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
62
|
+
}
|
|
63
|
+
function constantTimeEqual(a, b) {
|
|
64
|
+
if (a.length !== b.length) return false;
|
|
29
65
|
let result = 0;
|
|
30
|
-
for (let i = 0; i <
|
|
31
|
-
result |=
|
|
66
|
+
for (let i = 0; i < a.length; i++) {
|
|
67
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
32
68
|
}
|
|
33
69
|
return result === 0;
|
|
34
70
|
}
|
|
71
|
+
async function verifySignatureAsync(rawBody, signature, secret, options) {
|
|
72
|
+
if (typeof signature !== "string" || signature.length === 0) return false;
|
|
73
|
+
const stripe = parseStripeHeader(signature);
|
|
74
|
+
if (stripe) {
|
|
75
|
+
const tolerance = options?.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
|
|
76
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
77
|
+
if (Math.abs(now - stripe.timestamp) > tolerance) return false;
|
|
78
|
+
const expected2 = await hmacHex(`${stripe.timestamp}.${rawBody}`, secret);
|
|
79
|
+
for (const candidate of stripe.v1) {
|
|
80
|
+
if (constantTimeEqual(candidate, expected2)) return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const expected = "sha256=" + await hmacHex(rawBody, secret);
|
|
85
|
+
return constantTimeEqual(signature, expected);
|
|
86
|
+
}
|
|
35
87
|
export {
|
|
88
|
+
DEFAULT_TOLERANCE_SECONDS,
|
|
36
89
|
WebhookSignatureVerificationError,
|
|
37
90
|
constructEventAsync,
|
|
91
|
+
signPayloadStripeAsync,
|
|
38
92
|
verifySignatureAsync
|
|
39
93
|
};
|