@playmos/sdk 0.1.5 → 0.2.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/README.md +87 -0
- package/dist/chunk-B7SHFZYY.js +62 -0
- package/dist/chunk-B7SHFZYY.js.map +1 -0
- package/dist/errors-CjL85YKR.d.cts +502 -0
- package/dist/errors-CjL85YKR.d.ts +502 -0
- package/dist/index.cjs +610 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +321 -250
- package/dist/index.d.ts +321 -250
- package/dist/index.js +603 -113
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +63 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +26 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +47 -0
- package/dist/server.js.map +1 -0
- package/package.json +8 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/webhooks.ts"],"names":["createHmac","timingSafeEqual"],"mappings":"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","file":"server.cjs","sourcesContent":["/**\n * Typed, actionable errors — the Stripe bar (spec §11).\n *\n * Every error carries a stable machine-readable `code` and is thrown at the\n * EARLIEST possible layer: input errors fire client-side before any network or\n * chain call, so a studio never pays gas to discover a typo.\n */\n\nexport type PlaymosErrorCode =\n | \"invalid_amount\"\n | \"missing_field\"\n | \"insufficient_gas\"\n | \"wallet_connection\"\n | \"payment_failed\"\n | \"auth\"\n | \"api_error\"\n | \"config\";\n\nexport class PlaymosError extends Error {\n readonly code: PlaymosErrorCode;\n /** Optional machine context (e.g. the offending field, the http status). */\n readonly detail?: Record<string, unknown>;\n\n constructor(code: PlaymosErrorCode, message: string, detail?: Record<string, unknown>) {\n super(message);\n this.name = new.target.name;\n this.code = code;\n this.detail = detail;\n // Restore prototype chain for `instanceof` across transpile targets.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** amount ≤ 0, non-numeric, empty, or more than 2 decimal places. */\nexport class InvalidAmountError extends PlaymosError {\n constructor(amount: unknown) {\n super(\n \"invalid_amount\",\n `Invalid amount: ${JSON.stringify(amount)}. Provide a positive USD decimal string with at most 2 decimals, e.g. \"4.99\".`,\n { amount },\n );\n }\n}\n\n/** A required field (sku, playerId, gameId, roundId, agentId…) was empty. */\nexport class MissingFieldError extends PlaymosError {\n constructor(field: string) {\n super(\"missing_field\", `Missing required field: \"${field}\".`, { field });\n }\n}\n\n/** gas mode \"player\" and the player's ETH is too low to cover gas (pre-check, §7). */\nexport class InsufficientGasError extends PlaymosError {\n constructor(detail?: Record<string, unknown>) {\n super(\n \"insufficient_gas\",\n \"The player's wallet has too little ETH to pay gas. Ask them to add a little ETH, or switch to gas.mode: \\\"sponsored\\\".\",\n detail,\n );\n }\n}\n\n/** The player closed or failed the wallet sheet, or no provider is available. */\nexport class WalletConnectionError extends PlaymosError {\n constructor(message = \"Could not connect the player's wallet.\", detail?: Record<string, unknown>) {\n super(\"wallet_connection\", message, detail);\n }\n}\n\n/** The on-chain settlement reverted, was cancelled, or timed out. */\nexport class PaymentFailedError extends PlaymosError {\n constructor(message = \"The on-chain payment did not complete.\", detail?: Record<string, unknown>) {\n super(\"payment_failed\", message, detail);\n }\n}\n\n/** Bad, missing, or wrong-environment API key (e.g. a pk_test_ key on a live route). */\nexport class AuthError extends PlaymosError {\n constructor(message = \"Invalid or missing API key.\", detail?: Record<string, unknown>) {\n super(\"auth\", message, detail);\n }\n}\n\n/** The Playmos service returned a non-2xx we don't have a more specific error for. */\nexport class ApiError extends PlaymosError {\n constructor(message: string, detail?: Record<string, unknown>) {\n super(\"api_error\", message, detail);\n }\n}\n\n/** SDK misconfiguration (e.g. a missing contract address for on-chain mode). */\nexport class ConfigError extends PlaymosError {\n constructor(message: string, detail?: Record<string, unknown>) {\n super(\"config\", message, detail);\n }\n}\n","/**\n * Webhook signature verification (spec §6.2) — server-side only.\n *\n * Signature scheme (mirrors the Playmos service byte-for-byte):\n * header `X-Playmos-Signature: t=<unixSeconds>,v1=<hex>`\n * where <hex> = HMAC_SHA256(secret, `${t}.${rawBody}`)\n *\n * Verify BEFORE trusting an event. Uses node:crypto (constant-time compare) and\n * rejects stale timestamps to blunt replay. Throws on any mismatch.\n */\n\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { PlaymosError } from \"./errors.js\";\nimport type { WebhookEvent } from \"./types.js\";\n\nexport class WebhookSignatureError extends PlaymosError {\n constructor(message: string) {\n super(\"api_error\", `Webhook signature verification failed: ${message}`);\n }\n}\n\nfunction parseHeader(header: string): { t: number; v1: string } {\n const parts = Object.fromEntries(\n header.split(\",\").map((kv) => {\n const [k, v] = kv.split(\"=\");\n return [k?.trim(), v?.trim()];\n }),\n );\n const t = Number(parts[\"t\"]);\n const v1 = parts[\"v1\"];\n if (!Number.isFinite(t) || !v1) throw new WebhookSignatureError(\"malformed signature header\");\n return { t, v1 };\n}\n\nfunction computeSignature(secret: string, t: number, rawBody: string): string {\n return createHmac(\"sha256\", secret).update(`${t}.${rawBody}`).digest(\"hex\");\n}\n\n/**\n * Verify a webhook and return the parsed event. `rawBody` MUST be the exact raw\n * request bytes (use express.raw / fastify rawBody) — a re-serialized JSON body\n * will not match the signature.\n */\nexport function verifyWebhook(\n rawBody: string | Buffer,\n signatureHeader: string | string[] | undefined,\n secret: string,\n opts: { toleranceSeconds?: number } = {},\n): WebhookEvent {\n if (!signatureHeader || Array.isArray(signatureHeader)) {\n throw new WebhookSignatureError(\"missing signature header\");\n }\n if (!secret) throw new WebhookSignatureError(\"missing signing secret\");\n\n const body = typeof rawBody === \"string\" ? rawBody : rawBody.toString(\"utf8\");\n const { t, v1 } = parseHeader(signatureHeader);\n\n const tolerance = opts.toleranceSeconds ?? 300;\n const nowSec = Math.floor(Date.now() / 1000);\n if (Math.abs(nowSec - t) > tolerance) {\n throw new WebhookSignatureError(`timestamp outside tolerance (${tolerance}s)`);\n }\n\n const expected = computeSignature(secret, t, body);\n const a = Buffer.from(expected, \"hex\");\n const b = Buffer.from(v1, \"hex\");\n if (a.length !== b.length || !timingSafeEqual(a, b)) {\n throw new WebhookSignatureError(\"signature mismatch\");\n }\n\n return JSON.parse(body) as WebhookEvent;\n}\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z as PlaymosError, W as WebhookEvent } from './errors-CjL85YKR.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webhook signature verification (spec §6.2) — server-side only.
|
|
5
|
+
*
|
|
6
|
+
* Signature scheme (mirrors the Playmos service byte-for-byte):
|
|
7
|
+
* header `X-Playmos-Signature: t=<unixSeconds>,v1=<hex>`
|
|
8
|
+
* where <hex> = HMAC_SHA256(secret, `${t}.${rawBody}`)
|
|
9
|
+
*
|
|
10
|
+
* Verify BEFORE trusting an event. Uses node:crypto (constant-time compare) and
|
|
11
|
+
* rejects stale timestamps to blunt replay. Throws on any mismatch.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
declare class WebhookSignatureError extends PlaymosError {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Verify a webhook and return the parsed event. `rawBody` MUST be the exact raw
|
|
19
|
+
* request bytes (use express.raw / fastify rawBody) — a re-serialized JSON body
|
|
20
|
+
* will not match the signature.
|
|
21
|
+
*/
|
|
22
|
+
declare function verifyWebhook(rawBody: string | Buffer, signatureHeader: string | string[] | undefined, secret: string, opts?: {
|
|
23
|
+
toleranceSeconds?: number;
|
|
24
|
+
}): WebhookEvent;
|
|
25
|
+
|
|
26
|
+
export { WebhookSignatureError, verifyWebhook };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z as PlaymosError, W as WebhookEvent } from './errors-CjL85YKR.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webhook signature verification (spec §6.2) — server-side only.
|
|
5
|
+
*
|
|
6
|
+
* Signature scheme (mirrors the Playmos service byte-for-byte):
|
|
7
|
+
* header `X-Playmos-Signature: t=<unixSeconds>,v1=<hex>`
|
|
8
|
+
* where <hex> = HMAC_SHA256(secret, `${t}.${rawBody}`)
|
|
9
|
+
*
|
|
10
|
+
* Verify BEFORE trusting an event. Uses node:crypto (constant-time compare) and
|
|
11
|
+
* rejects stale timestamps to blunt replay. Throws on any mismatch.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
declare class WebhookSignatureError extends PlaymosError {
|
|
15
|
+
constructor(message: string);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Verify a webhook and return the parsed event. `rawBody` MUST be the exact raw
|
|
19
|
+
* request bytes (use express.raw / fastify rawBody) — a re-serialized JSON body
|
|
20
|
+
* will not match the signature.
|
|
21
|
+
*/
|
|
22
|
+
declare function verifyWebhook(rawBody: string | Buffer, signatureHeader: string | string[] | undefined, secret: string, opts?: {
|
|
23
|
+
toleranceSeconds?: number;
|
|
24
|
+
}): WebhookEvent;
|
|
25
|
+
|
|
26
|
+
export { WebhookSignatureError, verifyWebhook };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PlaymosError } from './chunk-B7SHFZYY.js';
|
|
2
|
+
import { timingSafeEqual, createHmac } from 'crypto';
|
|
3
|
+
|
|
4
|
+
var WebhookSignatureError = class extends PlaymosError {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super("api_error", `Webhook signature verification failed: ${message}`);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function parseHeader(header) {
|
|
10
|
+
const parts = Object.fromEntries(
|
|
11
|
+
header.split(",").map((kv) => {
|
|
12
|
+
const [k, v] = kv.split("=");
|
|
13
|
+
return [k?.trim(), v?.trim()];
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
const t = Number(parts["t"]);
|
|
17
|
+
const v1 = parts["v1"];
|
|
18
|
+
if (!Number.isFinite(t) || !v1) throw new WebhookSignatureError("malformed signature header");
|
|
19
|
+
return { t, v1 };
|
|
20
|
+
}
|
|
21
|
+
function computeSignature(secret, t, rawBody) {
|
|
22
|
+
return createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
|
|
23
|
+
}
|
|
24
|
+
function verifyWebhook(rawBody, signatureHeader, secret, opts = {}) {
|
|
25
|
+
if (!signatureHeader || Array.isArray(signatureHeader)) {
|
|
26
|
+
throw new WebhookSignatureError("missing signature header");
|
|
27
|
+
}
|
|
28
|
+
if (!secret) throw new WebhookSignatureError("missing signing secret");
|
|
29
|
+
const body = typeof rawBody === "string" ? rawBody : rawBody.toString("utf8");
|
|
30
|
+
const { t, v1 } = parseHeader(signatureHeader);
|
|
31
|
+
const tolerance = opts.toleranceSeconds ?? 300;
|
|
32
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
33
|
+
if (Math.abs(nowSec - t) > tolerance) {
|
|
34
|
+
throw new WebhookSignatureError(`timestamp outside tolerance (${tolerance}s)`);
|
|
35
|
+
}
|
|
36
|
+
const expected = computeSignature(secret, t, body);
|
|
37
|
+
const a = Buffer.from(expected, "hex");
|
|
38
|
+
const b = Buffer.from(v1, "hex");
|
|
39
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
40
|
+
throw new WebhookSignatureError("signature mismatch");
|
|
41
|
+
}
|
|
42
|
+
return JSON.parse(body);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { WebhookSignatureError, verifyWebhook };
|
|
46
|
+
//# sourceMappingURL=server.js.map
|
|
47
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/webhooks.ts"],"names":[],"mappings":";;;AAeO,IAAM,qBAAA,GAAN,cAAoC,YAAA,CAAa;AAAA,EACtD,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,WAAA,EAAa,CAAA,uCAAA,EAA0C,OAAO,CAAA,CAAE,CAAA;AAAA,EACxE;AACF;AAEA,SAAS,YAAY,MAAA,EAA2C;AAC9D,EAAA,MAAM,QAAQ,MAAA,CAAO,WAAA;AAAA,IACnB,OAAO,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,EAAA,KAAO;AAC5B,MAAA,MAAM,CAAC,CAAA,EAAG,CAAC,CAAA,GAAI,EAAA,CAAG,MAAM,GAAG,CAAA;AAC3B,MAAA,OAAO,CAAC,CAAA,EAAG,IAAA,EAAK,EAAG,CAAA,EAAG,MAAM,CAAA;AAAA,IAC9B,CAAC;AAAA,GACH;AACA,EAAA,MAAM,CAAA,GAAI,MAAA,CAAO,KAAA,CAAM,GAAG,CAAC,CAAA;AAC3B,EAAA,MAAM,EAAA,GAAK,MAAM,IAAI,CAAA;AACrB,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,IAAK,CAAC,EAAA,EAAI,MAAM,IAAI,qBAAA,CAAsB,4BAA4B,CAAA;AAC5F,EAAA,OAAO,EAAE,GAAG,EAAA,EAAG;AACjB;AAEA,SAAS,gBAAA,CAAiB,MAAA,EAAgB,CAAA,EAAW,OAAA,EAAyB;AAC5E,EAAA,OAAO,UAAA,CAAW,QAAA,EAAU,MAAM,CAAA,CAAE,MAAA,CAAO,CAAA,EAAG,CAAC,CAAA,CAAA,EAAI,OAAO,CAAA,CAAE,CAAA,CAAE,MAAA,CAAO,KAAK,CAAA;AAC5E;AAOO,SAAS,cACd,OAAA,EACA,eAAA,EACA,MAAA,EACA,IAAA,GAAsC,EAAC,EACzB;AACd,EAAA,IAAI,CAAC,eAAA,IAAmB,KAAA,CAAM,OAAA,CAAQ,eAAe,CAAA,EAAG;AACtD,IAAA,MAAM,IAAI,sBAAsB,0BAA0B,CAAA;AAAA,EAC5D;AACA,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,sBAAsB,wBAAwB,CAAA;AAErE,EAAA,MAAM,OAAO,OAAO,OAAA,KAAY,WAAW,OAAA,GAAU,OAAA,CAAQ,SAAS,MAAM,CAAA;AAC5E,EAAA,MAAM,EAAE,CAAA,EAAG,EAAA,EAAG,GAAI,YAAY,eAAe,CAAA;AAE7C,EAAA,MAAM,SAAA,GAAY,KAAK,gBAAA,IAAoB,GAAA;AAC3C,EAAA,MAAM,SAAS,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,KAAQ,GAAI,CAAA;AAC3C,EAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,GAAS,CAAC,IAAI,SAAA,EAAW;AACpC,IAAA,MAAM,IAAI,qBAAA,CAAsB,CAAA,6BAAA,EAAgC,SAAS,CAAA,EAAA,CAAI,CAAA;AAAA,EAC/E;AAEA,EAAA,MAAM,QAAA,GAAW,gBAAA,CAAiB,MAAA,EAAQ,CAAA,EAAG,IAAI,CAAA;AACjD,EAAA,MAAM,CAAA,GAAI,MAAA,CAAO,IAAA,CAAK,QAAA,EAAU,KAAK,CAAA;AACrC,EAAA,MAAM,CAAA,GAAI,MAAA,CAAO,IAAA,CAAK,EAAA,EAAI,KAAK,CAAA;AAC/B,EAAA,IAAI,CAAA,CAAE,WAAW,CAAA,CAAE,MAAA,IAAU,CAAC,eAAA,CAAgB,CAAA,EAAG,CAAC,CAAA,EAAG;AACnD,IAAA,MAAM,IAAI,sBAAsB,oBAAoB,CAAA;AAAA,EACtD;AAEA,EAAA,OAAO,IAAA,CAAK,MAAM,IAAI,CAAA;AACxB","file":"server.js","sourcesContent":["/**\n * Webhook signature verification (spec §6.2) — server-side only.\n *\n * Signature scheme (mirrors the Playmos service byte-for-byte):\n * header `X-Playmos-Signature: t=<unixSeconds>,v1=<hex>`\n * where <hex> = HMAC_SHA256(secret, `${t}.${rawBody}`)\n *\n * Verify BEFORE trusting an event. Uses node:crypto (constant-time compare) and\n * rejects stale timestamps to blunt replay. Throws on any mismatch.\n */\n\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { PlaymosError } from \"./errors.js\";\nimport type { WebhookEvent } from \"./types.js\";\n\nexport class WebhookSignatureError extends PlaymosError {\n constructor(message: string) {\n super(\"api_error\", `Webhook signature verification failed: ${message}`);\n }\n}\n\nfunction parseHeader(header: string): { t: number; v1: string } {\n const parts = Object.fromEntries(\n header.split(\",\").map((kv) => {\n const [k, v] = kv.split(\"=\");\n return [k?.trim(), v?.trim()];\n }),\n );\n const t = Number(parts[\"t\"]);\n const v1 = parts[\"v1\"];\n if (!Number.isFinite(t) || !v1) throw new WebhookSignatureError(\"malformed signature header\");\n return { t, v1 };\n}\n\nfunction computeSignature(secret: string, t: number, rawBody: string): string {\n return createHmac(\"sha256\", secret).update(`${t}.${rawBody}`).digest(\"hex\");\n}\n\n/**\n * Verify a webhook and return the parsed event. `rawBody` MUST be the exact raw\n * request bytes (use express.raw / fastify rawBody) — a re-serialized JSON body\n * will not match the signature.\n */\nexport function verifyWebhook(\n rawBody: string | Buffer,\n signatureHeader: string | string[] | undefined,\n secret: string,\n opts: { toleranceSeconds?: number } = {},\n): WebhookEvent {\n if (!signatureHeader || Array.isArray(signatureHeader)) {\n throw new WebhookSignatureError(\"missing signature header\");\n }\n if (!secret) throw new WebhookSignatureError(\"missing signing secret\");\n\n const body = typeof rawBody === \"string\" ? rawBody : rawBody.toString(\"utf8\");\n const { t, v1 } = parseHeader(signatureHeader);\n\n const tolerance = opts.toleranceSeconds ?? 300;\n const nowSec = Math.floor(Date.now() / 1000);\n if (Math.abs(nowSec - t) > tolerance) {\n throw new WebhookSignatureError(`timestamp outside tolerance (${tolerance}s)`);\n }\n\n const expected = computeSignature(secret, t, body);\n const a = Buffer.from(expected, \"hex\");\n const b = Buffer.from(v1, \"hex\");\n if (a.length !== b.length || !timingSafeEqual(a, b)) {\n throw new WebhookSignatureError(\"signature mismatch\");\n }\n\n return JSON.parse(body) as WebhookEvent;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playmos/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Playmos SDK
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Playmos SDK \u2014 stablecoin payments for games on Base. One SDK for IAP (1%), skill-game prize-pool entries (10%, 60/30/10), and agent economies. USD in, USDC on-chain, no crypto UX for players.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
7
7
|
"type": "module",
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"import": "./dist/index.js",
|
|
16
16
|
"require": "./dist/index.cjs"
|
|
17
|
+
},
|
|
18
|
+
"./server": {
|
|
19
|
+
"types": "./dist/server.d.ts",
|
|
20
|
+
"import": "./dist/server.js",
|
|
21
|
+
"require": "./dist/server.cjs"
|
|
17
22
|
}
|
|
18
23
|
},
|
|
19
24
|
"files": [
|
|
@@ -37,7 +42,7 @@
|
|
|
37
42
|
"vitest": "^2.1.0"
|
|
38
43
|
},
|
|
39
44
|
"engines": {
|
|
40
|
-
"node": ">=18"
|
|
45
|
+
"node": ">=18 <25"
|
|
41
46
|
},
|
|
42
47
|
"keywords": [
|
|
43
48
|
"playmos",
|