@subscriptonarc/sdk 1.0.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 +67 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +125 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @subscriptonarc/sdk
|
|
2
|
+
|
|
3
|
+
Typed SubScript API client for Arc USDC payments — payment intents, subscriptions, metered usage, and webhook verification. Zero runtime dependencies (uses native `fetch` and `node:crypto`).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @subscriptonarc/sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { SubScript, usdc } from "@subscriptonarc/sdk";
|
|
13
|
+
|
|
14
|
+
const subscript = new SubScript({ secretKey: process.env.SUBSCRIPT_SECRET_KEY! });
|
|
15
|
+
|
|
16
|
+
// One-time payment
|
|
17
|
+
const intent = await subscript.intents.create({
|
|
18
|
+
title: "Order #1042",
|
|
19
|
+
amountUsdcMicros: usdc(15), // 15 USDC -> "15000000"
|
|
20
|
+
successUrl: "https://example.com/thanks",
|
|
21
|
+
});
|
|
22
|
+
console.log(intent.checkoutUrl);
|
|
23
|
+
|
|
24
|
+
// Subscription
|
|
25
|
+
const sub = await subscript.subscriptions.create({
|
|
26
|
+
amountUsdcMicros: usdc(9.99),
|
|
27
|
+
interval: "monthly",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Metered usage
|
|
31
|
+
await subscript.usage.report({ userAddress: "0x…", amountUsdcMicros: usdc(0.5) });
|
|
32
|
+
|
|
33
|
+
// Check status
|
|
34
|
+
const status = await subscript.intents.retrieve(intent.id);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Webhooks
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { SubScript } from "@subscriptonarc/sdk";
|
|
41
|
+
|
|
42
|
+
// In your webhook route (rawBody is the unparsed request body string):
|
|
43
|
+
const event = subscript.webhooks.constructEvent(
|
|
44
|
+
rawBody,
|
|
45
|
+
request.headers["x-subscript-signature"],
|
|
46
|
+
process.env.SUBSCRIPT_WEBHOOK_SECRET!,
|
|
47
|
+
);
|
|
48
|
+
// throws if the signature is invalid; otherwise returns the parsed event
|
|
49
|
+
switch (event.type) {
|
|
50
|
+
case "payment.succeeded": /* … */ break;
|
|
51
|
+
case "subscription.renewed": /* … */ break;
|
|
52
|
+
case "subscription.payment_failed": /* dunning */ break;
|
|
53
|
+
case "subscription.canceled": /* … */ break;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
- `subscript.intents.create(params)` / `.retrieve(id)`
|
|
60
|
+
- `subscript.subscriptions.create(params)` / `.retrieve(id)` / `.list({ subscriber })` / `.cancel(id)`
|
|
61
|
+
- `subscript.usage.report({ userAddress, amountUsdcMicros })`
|
|
62
|
+
- `subscript.webhooks.verify(rawBody, sigHeader, secret)` / `.constructEvent(...)`
|
|
63
|
+
- Helpers: `usdc(decimal)` → micro-USDC string, `fromMicros(micros)` → decimal string
|
|
64
|
+
|
|
65
|
+
All amounts are integer **micro-USDC** (1 USDC = 1,000,000). The full contract is published as an [OpenAPI 3.1 spec](https://www.subscriptonarc.com/openapi.json).
|
|
66
|
+
|
|
67
|
+
Non-2xx responses throw `SubScriptError` (`.status`, `.body`).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** Error thrown for any non-2xx API response. `body` holds the parsed error payload. */
|
|
2
|
+
export declare class SubScriptError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly body: unknown;
|
|
5
|
+
constructor(message: string, status: number, body: unknown);
|
|
6
|
+
}
|
|
7
|
+
export interface SubScriptOptions {
|
|
8
|
+
/** Secret API key: `sk_test_…` (sandbox) or `sk_live_…` (production). */
|
|
9
|
+
secretKey: string;
|
|
10
|
+
/** Override the API base URL (defaults to https://www.subscriptonarc.com). */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
/** Custom fetch implementation (defaults to global fetch; Node >=18). */
|
|
13
|
+
fetchImpl?: typeof fetch;
|
|
14
|
+
}
|
|
15
|
+
/** Convert a decimal USDC amount (e.g. 15 or "15.50") to canonical integer micro-USDC. */
|
|
16
|
+
export declare function usdc(amount: number | string): string;
|
|
17
|
+
/** Convert integer micro-USDC to a decimal USDC string. */
|
|
18
|
+
export declare function fromMicros(micros: string | bigint | number): string;
|
|
19
|
+
/** Verify an `x-subscript-signature` header (`t=…,v1=…`) against the raw body and secret. */
|
|
20
|
+
export declare function verifyWebhookSignature(rawBody: string, signatureHeader: string, secret: string, toleranceSeconds?: number): boolean;
|
|
21
|
+
export interface Intent {
|
|
22
|
+
id: string;
|
|
23
|
+
checkoutSessionId?: string;
|
|
24
|
+
title: string;
|
|
25
|
+
description?: string | null;
|
|
26
|
+
amountUsdcMicros: string;
|
|
27
|
+
amountUsdc: string;
|
|
28
|
+
status: string;
|
|
29
|
+
merchantAddress: string;
|
|
30
|
+
receiptToken?: string | null;
|
|
31
|
+
checkoutUrl: string;
|
|
32
|
+
chainId: number;
|
|
33
|
+
usdcAddress: string;
|
|
34
|
+
returnUrls?: {
|
|
35
|
+
successUrl?: string;
|
|
36
|
+
cancelUrl?: string;
|
|
37
|
+
};
|
|
38
|
+
latestPayment?: {
|
|
39
|
+
txHash: string | null;
|
|
40
|
+
payerAddress: string;
|
|
41
|
+
credited: boolean;
|
|
42
|
+
explorerUrl?: string | null;
|
|
43
|
+
} | null;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
export interface CreateIntentParams {
|
|
47
|
+
title: string;
|
|
48
|
+
amountUsdcMicros: string | bigint | number;
|
|
49
|
+
description?: string;
|
|
50
|
+
externalReference?: string;
|
|
51
|
+
maxUses?: number;
|
|
52
|
+
expiresAt?: number | string;
|
|
53
|
+
successUrl?: string;
|
|
54
|
+
cancelUrl?: string;
|
|
55
|
+
idempotencyKey?: string;
|
|
56
|
+
sandbox?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface Subscription {
|
|
59
|
+
id: string;
|
|
60
|
+
object: "subscription";
|
|
61
|
+
status: "incomplete" | "active" | "inactive" | "past_due" | "canceled";
|
|
62
|
+
merchantAddress?: string;
|
|
63
|
+
subscriber?: string | null;
|
|
64
|
+
amountUsdcMicros: string;
|
|
65
|
+
amountUsdc: string;
|
|
66
|
+
intervalSeconds?: number;
|
|
67
|
+
intervalCount?: number;
|
|
68
|
+
interval?: string | null;
|
|
69
|
+
checkoutUrl?: string;
|
|
70
|
+
cancelAtPeriodEnd?: boolean;
|
|
71
|
+
[key: string]: unknown;
|
|
72
|
+
}
|
|
73
|
+
export interface CreateSubscriptionParams {
|
|
74
|
+
amountUsdcMicros?: string | bigint | number;
|
|
75
|
+
planId?: string;
|
|
76
|
+
interval?: "daily" | "weekly" | "monthly" | "yearly";
|
|
77
|
+
intervalSeconds?: number;
|
|
78
|
+
intervalCount?: number;
|
|
79
|
+
subscriber?: string;
|
|
80
|
+
title?: string;
|
|
81
|
+
externalReference?: string;
|
|
82
|
+
idempotencyKey?: string;
|
|
83
|
+
sandbox?: boolean;
|
|
84
|
+
}
|
|
85
|
+
export interface ReportUsageParams {
|
|
86
|
+
userAddress: string;
|
|
87
|
+
amountUsdcMicros: string | bigint | number;
|
|
88
|
+
}
|
|
89
|
+
export interface WebhookEvent {
|
|
90
|
+
id: string;
|
|
91
|
+
type: string;
|
|
92
|
+
event?: string;
|
|
93
|
+
created: number;
|
|
94
|
+
data: Record<string, unknown>;
|
|
95
|
+
}
|
|
96
|
+
export declare class SubScript {
|
|
97
|
+
private readonly secretKey;
|
|
98
|
+
private readonly baseUrl;
|
|
99
|
+
private readonly fetchImpl;
|
|
100
|
+
constructor(options: SubScriptOptions);
|
|
101
|
+
private request;
|
|
102
|
+
/** One-time payment intents (hosted checkout). */
|
|
103
|
+
readonly intents: {
|
|
104
|
+
create: (params: CreateIntentParams) => Promise<Intent>;
|
|
105
|
+
retrieve: (id: string) => Promise<Intent>;
|
|
106
|
+
};
|
|
107
|
+
/** Recurring subscriptions. */
|
|
108
|
+
readonly subscriptions: {
|
|
109
|
+
create: (params: CreateSubscriptionParams) => Promise<Subscription>;
|
|
110
|
+
retrieve: (id: string) => Promise<Subscription>;
|
|
111
|
+
list: (params?: {
|
|
112
|
+
subscriber?: string;
|
|
113
|
+
}) => Promise<Subscription[]>;
|
|
114
|
+
cancel: (id: string) => Promise<Subscription>;
|
|
115
|
+
};
|
|
116
|
+
/** Metered usage reporting. */
|
|
117
|
+
readonly usage: {
|
|
118
|
+
report: (params: ReportUsageParams) => Promise<Record<string, unknown>>;
|
|
119
|
+
};
|
|
120
|
+
/** Webhook signature verification (no network calls). */
|
|
121
|
+
readonly webhooks: {
|
|
122
|
+
verify: typeof verifyWebhookSignature;
|
|
123
|
+
/** Verify the signature and return the parsed event; throws SubScriptError if invalid. */
|
|
124
|
+
constructEvent: (rawBody: string, signatureHeader: string, secret: string) => WebhookEvent;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
export default SubScript;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
const DEFAULT_BASE_URL = "https://www.subscriptonarc.com";
|
|
3
|
+
/** Error thrown for any non-2xx API response. `body` holds the parsed error payload. */
|
|
4
|
+
export class SubScriptError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
body;
|
|
7
|
+
constructor(message, status, body) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "SubScriptError";
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.body = body;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/* ------------------------------- Amount helpers ------------------------------ */
|
|
15
|
+
/** Convert a decimal USDC amount (e.g. 15 or "15.50") to canonical integer micro-USDC. */
|
|
16
|
+
export function usdc(amount) {
|
|
17
|
+
const n = typeof amount === "number" ? amount : Number(amount);
|
|
18
|
+
if (!Number.isFinite(n) || n < 0)
|
|
19
|
+
throw new Error(`Invalid USDC amount: ${amount}`);
|
|
20
|
+
return BigInt(Math.round(n * 1_000_000)).toString();
|
|
21
|
+
}
|
|
22
|
+
/** Convert integer micro-USDC to a decimal USDC string. */
|
|
23
|
+
export function fromMicros(micros) {
|
|
24
|
+
const b = typeof micros === "bigint" ? micros : BigInt(micros);
|
|
25
|
+
const whole = b / 1000000n;
|
|
26
|
+
const frac = (b % 1000000n).toString().padStart(6, "0").replace(/0+$/, "");
|
|
27
|
+
return frac ? `${whole}.${frac}` : whole.toString();
|
|
28
|
+
}
|
|
29
|
+
/* ------------------------------ Webhook helpers ------------------------------ */
|
|
30
|
+
/** Verify an `x-subscript-signature` header (`t=…,v1=…`) against the raw body and secret. */
|
|
31
|
+
export function verifyWebhookSignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
|
|
32
|
+
if (!signatureHeader || !secret)
|
|
33
|
+
return false;
|
|
34
|
+
let t = "";
|
|
35
|
+
let v1 = "";
|
|
36
|
+
for (const part of signatureHeader.split(",")) {
|
|
37
|
+
const [k, val] = part.split("=");
|
|
38
|
+
if (k === "t")
|
|
39
|
+
t = val;
|
|
40
|
+
if (k === "v1")
|
|
41
|
+
v1 = val;
|
|
42
|
+
}
|
|
43
|
+
if (!t || !v1)
|
|
44
|
+
return false;
|
|
45
|
+
const ts = Number.parseInt(t, 10);
|
|
46
|
+
if (Number.isNaN(ts) || Math.abs(Math.floor(Date.now() / 1000) - ts) > toleranceSeconds)
|
|
47
|
+
return false;
|
|
48
|
+
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
|
|
49
|
+
const a = Buffer.from(v1, "hex");
|
|
50
|
+
const b = Buffer.from(expected, "hex");
|
|
51
|
+
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
52
|
+
}
|
|
53
|
+
function stringifyMicros(params) {
|
|
54
|
+
if (params && params.amountUsdcMicros != null && typeof params.amountUsdcMicros !== "string") {
|
|
55
|
+
return { ...params, amountUsdcMicros: String(params.amountUsdcMicros) };
|
|
56
|
+
}
|
|
57
|
+
return params;
|
|
58
|
+
}
|
|
59
|
+
/* --------------------------------- Client ------------------------------------ */
|
|
60
|
+
export class SubScript {
|
|
61
|
+
secretKey;
|
|
62
|
+
baseUrl;
|
|
63
|
+
fetchImpl;
|
|
64
|
+
constructor(options) {
|
|
65
|
+
if (!options?.secretKey)
|
|
66
|
+
throw new Error("SubScript: `secretKey` is required");
|
|
67
|
+
this.secretKey = options.secretKey;
|
|
68
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
69
|
+
const f = options.fetchImpl ?? globalThis.fetch;
|
|
70
|
+
if (!f)
|
|
71
|
+
throw new Error("SubScript: no global fetch available — pass `fetchImpl` or use Node >=18");
|
|
72
|
+
this.fetchImpl = f;
|
|
73
|
+
}
|
|
74
|
+
async request(method, path, body) {
|
|
75
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
76
|
+
method,
|
|
77
|
+
headers: {
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
Authorization: `Bearer ${this.secretKey}`,
|
|
80
|
+
},
|
|
81
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
82
|
+
});
|
|
83
|
+
const text = await res.text();
|
|
84
|
+
let json;
|
|
85
|
+
try {
|
|
86
|
+
json = text ? JSON.parse(text) : {};
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
json = { raw: text };
|
|
90
|
+
}
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const message = json?.error ?? `SubScript request failed (${res.status})`;
|
|
93
|
+
throw new SubScriptError(message, res.status, json);
|
|
94
|
+
}
|
|
95
|
+
return json;
|
|
96
|
+
}
|
|
97
|
+
/** One-time payment intents (hosted checkout). */
|
|
98
|
+
intents = {
|
|
99
|
+
create: (params) => this.request("POST", "/api/intent", stringifyMicros(params)).then((r) => r.intent),
|
|
100
|
+
retrieve: (id) => this.request("GET", `/api/intent/status?id=${encodeURIComponent(id)}`).then((r) => r.intent),
|
|
101
|
+
};
|
|
102
|
+
/** Recurring subscriptions. */
|
|
103
|
+
subscriptions = {
|
|
104
|
+
create: (params) => this.request("POST", "/api/v1/subscriptions", stringifyMicros(params)).then((r) => r.subscription),
|
|
105
|
+
retrieve: (id) => this.request("GET", `/api/v1/subscriptions?id=${encodeURIComponent(id)}`),
|
|
106
|
+
list: (params) => this.request("GET", `/api/v1/subscriptions${params?.subscriber ? `?subscriber=${encodeURIComponent(params.subscriber)}` : ""}`).then((r) => r.data),
|
|
107
|
+
cancel: (id) => this.request("DELETE", `/api/v1/subscriptions?id=${encodeURIComponent(id)}`),
|
|
108
|
+
};
|
|
109
|
+
/** Metered usage reporting. */
|
|
110
|
+
usage = {
|
|
111
|
+
report: (params) => this.request("POST", "/api/user/vault/report-usage", stringifyMicros(params)),
|
|
112
|
+
};
|
|
113
|
+
/** Webhook signature verification (no network calls). */
|
|
114
|
+
webhooks = {
|
|
115
|
+
verify: verifyWebhookSignature,
|
|
116
|
+
/** Verify the signature and return the parsed event; throws SubScriptError if invalid. */
|
|
117
|
+
constructEvent: (rawBody, signatureHeader, secret) => {
|
|
118
|
+
if (!verifyWebhookSignature(rawBody, signatureHeader, secret)) {
|
|
119
|
+
throw new SubScriptError("Invalid webhook signature", 400, null);
|
|
120
|
+
}
|
|
121
|
+
return JSON.parse(rawBody);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export default SubScript;
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@subscriptonarc/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Typed SubScript API client — payment intents, subscriptions, usage reporting, and webhook verification for Arc USDC payments.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"subscript",
|
|
23
|
+
"arc",
|
|
24
|
+
"usdc",
|
|
25
|
+
"payments",
|
|
26
|
+
"stablecoin",
|
|
27
|
+
"subscriptions",
|
|
28
|
+
"usage-billing",
|
|
29
|
+
"webhooks",
|
|
30
|
+
"sdk"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/KristienOWeb3/SubScript.git",
|
|
35
|
+
"directory": "packages/sdk"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsc -p tsconfig.json",
|
|
39
|
+
"prepublishOnly": "npm run build"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20",
|
|
43
|
+
"typescript": "^5"
|
|
44
|
+
}
|
|
45
|
+
}
|