@peable.to/sdk 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/browser/mount.js +308 -0
- package/dist/cjs/browser/payerClient.js +135 -0
- package/dist/cjs/checkout.js +17 -0
- package/dist/cjs/core/client.js +77 -0
- package/dist/cjs/core/config.js +38 -0
- package/dist/cjs/core/errors.js +112 -0
- package/dist/cjs/core/serviceToken.js +81 -0
- package/dist/cjs/index.js +41 -0
- package/dist/cjs/resources/checkoutSessions.js +26 -0
- package/dist/cjs/resources/paymentIntents.js +30 -0
- package/dist/cjs/resources/paymentLinks.js +23 -0
- package/dist/cjs/resources/webhooks.js +65 -0
- package/dist/esm/browser/mount.js +298 -0
- package/dist/esm/browser/payerClient.js +132 -0
- package/dist/esm/checkout.js +12 -0
- package/dist/esm/core/client.js +74 -0
- package/dist/esm/core/config.js +34 -0
- package/dist/esm/core/errors.js +102 -0
- package/dist/esm/core/serviceToken.js +78 -0
- package/dist/esm/index.js +28 -0
- package/dist/esm/resources/checkoutSessions.js +21 -0
- package/dist/esm/resources/paymentIntents.js +26 -0
- package/dist/esm/resources/paymentLinks.js +19 -0
- package/dist/esm/resources/webhooks.js +61 -0
- package/dist/types/browser/mount.d.ts +80 -0
- package/dist/types/browser/payerClient.d.ts +23 -0
- package/dist/types/checkout.d.ts +2 -0
- package/dist/types/core/client.d.ts +17 -0
- package/dist/types/core/config.d.ts +30 -0
- package/dist/types/core/errors.d.ts +54 -0
- package/dist/types/core/serviceToken.d.ts +11 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/resources/checkoutSessions.d.ts +15 -0
- package/dist/types/resources/paymentIntents.d.ts +29 -0
- package/dist/types/resources/paymentLinks.d.ts +29 -0
- package/dist/types/resources/webhooks.d.ts +18 -0
- package/package.json +83 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The `PeableError` hierarchy — every non-2xx Gateway/oxy-api response is
|
|
3
|
+
// mapped into one of these, never a bare `Error` or a raw `Response`.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.PeableSignatureVerificationError = exports.PeableApiError = exports.PeablePermissionError = exports.PeableInvalidRequestError = exports.PeableAuthenticationError = exports.PeableError = void 0;
|
|
6
|
+
exports.errorFromResponse = errorFromResponse;
|
|
7
|
+
class PeableError extends Error {
|
|
8
|
+
constructor(type, message, details = {}) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'PeableError';
|
|
11
|
+
this.type = type;
|
|
12
|
+
this.statusCode = details.statusCode;
|
|
13
|
+
this.code = details.code;
|
|
14
|
+
Object.setPrototypeOf(this, PeableError.prototype);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.PeableError = PeableError;
|
|
18
|
+
class PeableAuthenticationError extends PeableError {
|
|
19
|
+
constructor(message, details = {}) {
|
|
20
|
+
super('authentication_error', message, details);
|
|
21
|
+
this.name = 'PeableAuthenticationError';
|
|
22
|
+
Object.setPrototypeOf(this, PeableAuthenticationError.prototype);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.PeableAuthenticationError = PeableAuthenticationError;
|
|
26
|
+
class PeableInvalidRequestError extends PeableError {
|
|
27
|
+
constructor(message, details = {}) {
|
|
28
|
+
super('invalid_request_error', message, details);
|
|
29
|
+
this.name = 'PeableInvalidRequestError';
|
|
30
|
+
Object.setPrototypeOf(this, PeableInvalidRequestError.prototype);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.PeableInvalidRequestError = PeableInvalidRequestError;
|
|
34
|
+
class PeablePermissionError extends PeableError {
|
|
35
|
+
constructor(message, details = {}) {
|
|
36
|
+
super('permission_error', message, details);
|
|
37
|
+
this.name = 'PeablePermissionError';
|
|
38
|
+
Object.setPrototypeOf(this, PeablePermissionError.prototype);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.PeablePermissionError = PeablePermissionError;
|
|
42
|
+
class PeableApiError extends PeableError {
|
|
43
|
+
constructor(message, details = {}) {
|
|
44
|
+
super('api_error', message, details);
|
|
45
|
+
this.name = 'PeableApiError';
|
|
46
|
+
Object.setPrototypeOf(this, PeableApiError.prototype);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.PeableApiError = PeableApiError;
|
|
50
|
+
/** Thrown by `webhooks.constructEvent` on a bad/stale/tampered signature. */
|
|
51
|
+
class PeableSignatureVerificationError extends PeableError {
|
|
52
|
+
constructor(message) {
|
|
53
|
+
super('signature_verification_error', message);
|
|
54
|
+
this.name = 'PeableSignatureVerificationError';
|
|
55
|
+
Object.setPrototypeOf(this, PeableSignatureVerificationError.prototype);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.PeableSignatureVerificationError = PeableSignatureVerificationError;
|
|
59
|
+
function isNestedGatewayErrorBody(body) {
|
|
60
|
+
if (typeof body !== 'object' || body === null || !('error' in body))
|
|
61
|
+
return false;
|
|
62
|
+
const { error } = body;
|
|
63
|
+
if (typeof error !== 'object' || error === null)
|
|
64
|
+
return false;
|
|
65
|
+
const nested = error;
|
|
66
|
+
return typeof nested.type === 'string' && typeof nested.message === 'string';
|
|
67
|
+
}
|
|
68
|
+
function isFlatGatewayErrorBody(body) {
|
|
69
|
+
if (typeof body !== 'object' || body === null || !('error' in body))
|
|
70
|
+
return false;
|
|
71
|
+
const { error } = body;
|
|
72
|
+
return typeof error === 'string';
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Map an HTTP status + parsed JSON body into a typed `PeableError`.
|
|
76
|
+
*
|
|
77
|
+
* The Oxy ecosystem returns TWO distinct error envelopes depending on WHERE a
|
|
78
|
+
* request was rejected:
|
|
79
|
+
* - The Gateway's own route handlers use the Stripe-style nested envelope
|
|
80
|
+
* `{ error: { type, message } }` (`lib/http.ts`'s `sendError`).
|
|
81
|
+
* - `@oxyhq/core`'s Express auth middleware (`oxyClient.serviceAuth()` /
|
|
82
|
+
* `requireScope()` — rejecting a missing/expired/insufficiently-scoped
|
|
83
|
+
* service token BEFORE a route handler ever runs) and oxy-api's own
|
|
84
|
+
* `ApiError.toJSON()` use a FLAT envelope: `{ error: <CODE_STRING>,
|
|
85
|
+
* message, code? }`.
|
|
86
|
+
*
|
|
87
|
+
* A caller of this SDK hits the flat shape on every auth failure (expired
|
|
88
|
+
* token, missing scope, bad credentials at mint time), so the HTTP status
|
|
89
|
+
* code — not `error.type` — is the primary classifier here; the raw
|
|
90
|
+
* upstream `type`/`code` string is still attached to the resulting error
|
|
91
|
+
* (`.code`) for callers who want it.
|
|
92
|
+
*/
|
|
93
|
+
function errorFromResponse(status, body) {
|
|
94
|
+
let upstreamCode;
|
|
95
|
+
let message = `Peable request failed with status ${status}`;
|
|
96
|
+
if (isNestedGatewayErrorBody(body)) {
|
|
97
|
+
upstreamCode = body.error.type;
|
|
98
|
+
message = body.error.message;
|
|
99
|
+
}
|
|
100
|
+
else if (isFlatGatewayErrorBody(body)) {
|
|
101
|
+
upstreamCode = body.error;
|
|
102
|
+
message = body.message || message;
|
|
103
|
+
}
|
|
104
|
+
const details = { statusCode: status, code: upstreamCode };
|
|
105
|
+
if (status === 401)
|
|
106
|
+
return new PeableAuthenticationError(message, details);
|
|
107
|
+
if (status === 403)
|
|
108
|
+
return new PeablePermissionError(message, details);
|
|
109
|
+
if (status >= 400 && status < 500)
|
|
110
|
+
return new PeableInvalidRequestError(message, details);
|
|
111
|
+
return new PeableApiError(message, details);
|
|
112
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Mints a short-lived Oxy service token from an `ApplicationCredential`
|
|
3
|
+
// (`{publicKey, secret}`) by presenting it to oxy-api's `POST
|
|
4
|
+
// /auth/service-token` — the exact mechanism `oxyClient.serviceAuth()`
|
|
5
|
+
// validates at the Gateway. Caches the token until near expiry and re-mints
|
|
6
|
+
// reactively when `RestClient` sees a 401 (see `client.ts`).
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.createServiceTokenProvider = createServiceTokenProvider;
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
const errors_1 = require("./errors");
|
|
11
|
+
/** Path (relative to `oxyApiUrl`) of oxy-api's service-token mint endpoint. */
|
|
12
|
+
const SERVICE_TOKEN_PATH = '/auth/service-token';
|
|
13
|
+
/** Refresh proactively this long before the minted token's actual expiry. */
|
|
14
|
+
const REFRESH_MARGIN_MS = 60000;
|
|
15
|
+
function isServiceTokenMintEnvelope(body) {
|
|
16
|
+
if (typeof body !== 'object' || body === null || !('data' in body))
|
|
17
|
+
return false;
|
|
18
|
+
const { data } = body;
|
|
19
|
+
if (typeof data !== 'object' || data === null)
|
|
20
|
+
return false;
|
|
21
|
+
const candidate = data;
|
|
22
|
+
return typeof candidate.token === 'string' && typeof candidate.expiresIn === 'number';
|
|
23
|
+
}
|
|
24
|
+
async function readJsonBody(response) {
|
|
25
|
+
const text = await response.text();
|
|
26
|
+
if (text.length === 0)
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(text);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function createServiceTokenProvider(cfg, deps = {}) {
|
|
36
|
+
const resolved = (0, config_1.resolveConfig)(cfg);
|
|
37
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
38
|
+
let cachedToken = null;
|
|
39
|
+
let cachedExpiresAt = 0;
|
|
40
|
+
let inflight = null;
|
|
41
|
+
async function mint() {
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await fetchImpl(`${resolved.oxyApiUrl}${SERVICE_TOKEN_PATH}`, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({ apiKey: resolved.publicKey, apiSecret: resolved.secret }),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (cause) {
|
|
51
|
+
throw new errors_1.PeableApiError(`Failed to reach ${resolved.oxyApiUrl} to mint an Peable service token: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
52
|
+
}
|
|
53
|
+
const body = await readJsonBody(response);
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw (0, errors_1.errorFromResponse)(response.status, body);
|
|
56
|
+
}
|
|
57
|
+
if (!isServiceTokenMintEnvelope(body)) {
|
|
58
|
+
throw new errors_1.PeableApiError('Peable: the service-token mint response was missing `data.token`/`data.expiresIn`');
|
|
59
|
+
}
|
|
60
|
+
cachedToken = body.data.token;
|
|
61
|
+
cachedExpiresAt = Date.now() + body.data.expiresIn * 1000;
|
|
62
|
+
return cachedToken;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
async getToken() {
|
|
66
|
+
if (cachedToken !== null && Date.now() < cachedExpiresAt - REFRESH_MARGIN_MS) {
|
|
67
|
+
return cachedToken;
|
|
68
|
+
}
|
|
69
|
+
if (inflight === null) {
|
|
70
|
+
inflight = mint().finally(() => {
|
|
71
|
+
inflight = null;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return inflight;
|
|
75
|
+
},
|
|
76
|
+
invalidate() {
|
|
77
|
+
cachedToken = null;
|
|
78
|
+
cachedExpiresAt = 0;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WEBHOOK_SIGNATURE_HEADER = exports.PeableSignatureVerificationError = exports.PeableApiError = exports.PeablePermissionError = exports.PeableInvalidRequestError = exports.PeableAuthenticationError = exports.PeableError = exports.DEFAULT_OXY_API_URL = exports.DEFAULT_GATEWAY_BASE_URL = exports.Peable = void 0;
|
|
4
|
+
// Server entry (`@peable.to/sdk`) — the merchant-authed SDK. Configured with a
|
|
5
|
+
// confidential `ApplicationCredential` (`{publicKey, secret}`); mints and
|
|
6
|
+
// caches an Oxy service token, and exposes Stripe-ergonomics resource
|
|
7
|
+
// namespaces over the Gateway REST contract.
|
|
8
|
+
//
|
|
9
|
+
// Deliberately never imports `socket.io-client` or `./browser/*` — the
|
|
10
|
+
// server bundle stays lean and holds no browser-only code.
|
|
11
|
+
const config_1 = require("./core/config");
|
|
12
|
+
const serviceToken_1 = require("./core/serviceToken");
|
|
13
|
+
const client_1 = require("./core/client");
|
|
14
|
+
const paymentIntents_1 = require("./resources/paymentIntents");
|
|
15
|
+
const paymentLinks_1 = require("./resources/paymentLinks");
|
|
16
|
+
const checkoutSessions_1 = require("./resources/checkoutSessions");
|
|
17
|
+
const webhooks_1 = require("./resources/webhooks");
|
|
18
|
+
class Peable {
|
|
19
|
+
constructor(config) {
|
|
20
|
+
const resolved = (0, config_1.resolveConfig)(config);
|
|
21
|
+
const tokenProvider = (0, serviceToken_1.createServiceTokenProvider)(config);
|
|
22
|
+
const client = (0, client_1.createRestClient)({ baseURL: resolved.baseURL }, tokenProvider);
|
|
23
|
+
this.paymentIntents = new paymentIntents_1.PaymentIntentsResource(client);
|
|
24
|
+
this.paymentLinks = new paymentLinks_1.PaymentLinksResource(client);
|
|
25
|
+
this.checkout = new checkoutSessions_1.CheckoutResource(client);
|
|
26
|
+
this.webhooks = new webhooks_1.WebhooksResource();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.Peable = Peable;
|
|
30
|
+
var config_2 = require("./core/config");
|
|
31
|
+
Object.defineProperty(exports, "DEFAULT_GATEWAY_BASE_URL", { enumerable: true, get: function () { return config_2.DEFAULT_GATEWAY_BASE_URL; } });
|
|
32
|
+
Object.defineProperty(exports, "DEFAULT_OXY_API_URL", { enumerable: true, get: function () { return config_2.DEFAULT_OXY_API_URL; } });
|
|
33
|
+
var errors_1 = require("./core/errors");
|
|
34
|
+
Object.defineProperty(exports, "PeableError", { enumerable: true, get: function () { return errors_1.PeableError; } });
|
|
35
|
+
Object.defineProperty(exports, "PeableAuthenticationError", { enumerable: true, get: function () { return errors_1.PeableAuthenticationError; } });
|
|
36
|
+
Object.defineProperty(exports, "PeableInvalidRequestError", { enumerable: true, get: function () { return errors_1.PeableInvalidRequestError; } });
|
|
37
|
+
Object.defineProperty(exports, "PeablePermissionError", { enumerable: true, get: function () { return errors_1.PeablePermissionError; } });
|
|
38
|
+
Object.defineProperty(exports, "PeableApiError", { enumerable: true, get: function () { return errors_1.PeableApiError; } });
|
|
39
|
+
Object.defineProperty(exports, "PeableSignatureVerificationError", { enumerable: true, get: function () { return errors_1.PeableSignatureVerificationError; } });
|
|
40
|
+
var webhooks_2 = require("./resources/webhooks");
|
|
41
|
+
Object.defineProperty(exports, "WEBHOOK_SIGNATURE_HEADER", { enumerable: true, get: function () { return webhooks_2.WEBHOOK_SIGNATURE_HEADER; } });
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CheckoutResource = exports.CheckoutSessionsResource = void 0;
|
|
4
|
+
class CheckoutSessionsResource {
|
|
5
|
+
constructor(client) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
create(params) {
|
|
9
|
+
return this.client.request('POST', '/v1/checkout_sessions', {
|
|
10
|
+
body: params,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
retrieve(id) {
|
|
14
|
+
return this.client.request('GET', `/v1/checkout_sessions/${encodeURIComponent(id)}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.CheckoutSessionsResource = CheckoutSessionsResource;
|
|
18
|
+
/** `peable.checkout.sessions.*` — a thin namespace wrapper so the public API
|
|
19
|
+
* reads `peable.checkout.sessions.create(...)` (Stripe Checkout Session
|
|
20
|
+
* parity), leaving room for future `peable.checkout.*` resources. */
|
|
21
|
+
class CheckoutResource {
|
|
22
|
+
constructor(client) {
|
|
23
|
+
this.sessions = new CheckoutSessionsResource(client);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.CheckoutResource = CheckoutResource;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PaymentIntentsResource = void 0;
|
|
4
|
+
class PaymentIntentsResource {
|
|
5
|
+
constructor(client) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
create(params, options) {
|
|
9
|
+
return this.client.request('POST', '/v1/payment_intents', {
|
|
10
|
+
body: params,
|
|
11
|
+
idempotencyKey: options.idempotencyKey,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
retrieve(id) {
|
|
15
|
+
return this.client.request('GET', `/v1/payment_intents/${encodeURIComponent(id)}`);
|
|
16
|
+
}
|
|
17
|
+
list(params = {}) {
|
|
18
|
+
return this.client.request('GET', '/v1/payment_intents', {
|
|
19
|
+
query: {
|
|
20
|
+
status: params.status,
|
|
21
|
+
limit: params.limit,
|
|
22
|
+
starting_after: params.starting_after,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
reject(id) {
|
|
27
|
+
return this.client.request('POST', `/v1/payment_intents/${encodeURIComponent(id)}/reject`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.PaymentIntentsResource = PaymentIntentsResource;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PaymentLinksResource = void 0;
|
|
4
|
+
class PaymentLinksResource {
|
|
5
|
+
constructor(client) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
create(params) {
|
|
9
|
+
return this.client.request('POST', '/v1/payment_links', { body: params });
|
|
10
|
+
}
|
|
11
|
+
retrieve(id) {
|
|
12
|
+
return this.client.request('GET', `/v1/payment_links/${encodeURIComponent(id)}`);
|
|
13
|
+
}
|
|
14
|
+
list(params = {}) {
|
|
15
|
+
return this.client.request('GET', '/v1/payment_links', {
|
|
16
|
+
query: { limit: params.limit, starting_after: params.starting_after },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
update(id, params) {
|
|
20
|
+
return this.client.request('PATCH', `/v1/payment_links/${encodeURIComponent(id)}`, { body: params });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.PaymentLinksResource = PaymentLinksResource;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebhooksResource = exports.WEBHOOK_SIGNATURE_HEADER = void 0;
|
|
4
|
+
const shared_types_1 = require("@peable.to/shared-types");
|
|
5
|
+
const errors_1 = require("../core/errors");
|
|
6
|
+
/** Header the Gateway's `webhookDispatcher.deliver` signs with (`Peable-Signature`). */
|
|
7
|
+
exports.WEBHOOK_SIGNATURE_HEADER = 'Peable-Signature';
|
|
8
|
+
/** Default replay-tolerance window, matching the backend's own tests/usage. */
|
|
9
|
+
const DEFAULT_TOLERANCE_SEC = 300;
|
|
10
|
+
const WEBHOOK_EVENT_TYPES = [
|
|
11
|
+
'payment_intent.confirming',
|
|
12
|
+
'payment_intent.settled',
|
|
13
|
+
'payment_intent.failed',
|
|
14
|
+
'payment_intent.rejected',
|
|
15
|
+
'payment_intent.expired',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Structural guard for the parsed payload — the signature already proves the
|
|
19
|
+
* bytes were produced by `signWebhook` on the SAME secret, but a caller could
|
|
20
|
+
* still pass an unrelated (validly HMAC'd under a shared secret) JSON blob;
|
|
21
|
+
* this keeps `constructEvent`'s return type honest rather than a blind cast.
|
|
22
|
+
*/
|
|
23
|
+
function isWebhookEventShape(value) {
|
|
24
|
+
if (typeof value !== 'object' || value === null)
|
|
25
|
+
return false;
|
|
26
|
+
const candidate = value;
|
|
27
|
+
return (typeof candidate.id === 'string' &&
|
|
28
|
+
candidate.object === 'event' &&
|
|
29
|
+
typeof candidate.type === 'string' &&
|
|
30
|
+
WEBHOOK_EVENT_TYPES.includes(candidate.type) &&
|
|
31
|
+
typeof candidate.created === 'string' &&
|
|
32
|
+
typeof candidate.data === 'object' &&
|
|
33
|
+
candidate.data !== null &&
|
|
34
|
+
'object' in candidate.data);
|
|
35
|
+
}
|
|
36
|
+
class WebhooksResource {
|
|
37
|
+
/**
|
|
38
|
+
* Verify a webhook delivery and parse it into a typed `WebhookEvent`.
|
|
39
|
+
* Verifies via the SAME `verifyWebhook` the Gateway signs with
|
|
40
|
+
* (`@peable.to/shared-types`) so the algorithm can never drift between the two
|
|
41
|
+
* sides. Throws `PeableSignatureVerificationError` on a bad/stale/tampered
|
|
42
|
+
* signature or a malformed payload — never returns a partially-trusted
|
|
43
|
+
* event.
|
|
44
|
+
*/
|
|
45
|
+
constructEvent(rawBody, signatureHeader, endpointSecret, opts = {}) {
|
|
46
|
+
const toleranceSec = opts.toleranceSec ?? DEFAULT_TOLERANCE_SEC;
|
|
47
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
48
|
+
const verified = (0, shared_types_1.verifyWebhook)(endpointSecret, rawBody, signatureHeader, toleranceSec, nowSec);
|
|
49
|
+
if (!verified) {
|
|
50
|
+
throw new errors_1.PeableSignatureVerificationError('Webhook signature verification failed — the payload, signature header, or secret do not match, or the timestamp is stale.');
|
|
51
|
+
}
|
|
52
|
+
let parsed;
|
|
53
|
+
try {
|
|
54
|
+
parsed = JSON.parse(rawBody);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new errors_1.PeableSignatureVerificationError('Webhook signature verified, but the payload is not valid JSON');
|
|
58
|
+
}
|
|
59
|
+
if (!isWebhookEventShape(parsed)) {
|
|
60
|
+
throw new errors_1.PeableSignatureVerificationError('Webhook signature verified, but the payload does not match the expected event shape');
|
|
61
|
+
}
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.WebhooksResource = WebhooksResource;
|