🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@offcoin/sdk

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@offcoin/sdk - npm Package Compare versions

Comparing version
0.0.4
to
0.0.5
+130
dist/webhooks.d.ts
/**
* Webhook utilities for verifying Offcoin webhook signatures
*/
/**
* Webhook event types that can be subscribed to
*/
export declare const WebhookEventTypes: {
readonly MEMBER_BALANCE_UPDATED: "member.balance_updated";
readonly MEMBER_XP_UPDATED: "member.xp_updated";
readonly ACHIEVEMENT_UNLOCKED: "achievement.unlocked";
readonly PERMISSION_GRANTED: "permission.granted";
};
export type WebhookEventType = (typeof WebhookEventTypes)[keyof typeof WebhookEventTypes];
/**
* Webhook payload structure sent by Offcoin
*/
export interface WebhookPayload<T = unknown> {
id: string;
type: WebhookEventType;
timestamp: string;
tenantId: string;
data: T;
}
/**
* Data payload for member.balance_updated events
*/
export interface BalanceUpdatedData {
memberId: string;
memberName: string;
previousBalance: number;
newBalance: number;
amount: number;
reason?: string;
}
/**
* Data payload for member.xp_updated events
*/
export interface XpUpdatedData {
memberId: string;
memberName: string;
previousXp: number;
newXp: number;
amount: number;
previousLevel: number;
newLevel: number;
}
/**
* Data payload for achievement.unlocked events
*/
export interface AchievementUnlockedData {
memberId: string;
memberName: string;
achievementId: string;
achievementKey: string;
achievementName: string;
automatic: boolean;
}
/**
* Data payload for permission.granted events
*/
export interface PermissionGrantedData {
memberId: string;
memberName: string;
permissionId: string;
permissionKey: string;
permissionName: string;
grantType: 'level' | 'purchase';
}
/**
* Result of webhook signature verification
*/
export interface VerifyWebhookResult {
valid: boolean;
payload?: WebhookPayload;
error?: string;
}
/**
* Options for webhook verification
*/
export interface VerifyWebhookOptions {
/**
* Maximum age of the webhook in seconds (default: 300 = 5 minutes)
* Set to 0 to disable timestamp validation
*/
maxAgeSeconds?: number;
}
/**
* Verify a webhook signature from Offcoin
*
* @param payload - The raw request body as a string
* @param signature - The value from the X-Webhook-Signature header
* @param timestamp - The value from the X-Webhook-Timestamp header
* @param secret - Your webhook signing secret (starts with 'whsec_')
* @param options - Verification options
* @returns Verification result with parsed payload if valid
*
* @example
* ```typescript
* import { verifyWebhookSignature } from '@offcoin/sdk';
*
* // In your webhook handler (e.g., Express)
* app.post('/webhooks/offcoin', async (req, res) => {
* const signature = req.headers['x-webhook-signature'] as string;
* const timestamp = req.headers['x-webhook-timestamp'] as string;
*
* const result = await verifyWebhookSignature(
* req.body, // raw body string
* signature,
* timestamp,
* process.env.OFFCOIN_WEBHOOK_SECRET!
* );
*
* if (!result.valid) {
* return res.status(401).json({ error: result.error });
* }
*
* // Process the verified webhook
* const { type, data } = result.payload!;
* switch (type) {
* case 'member.balance_updated':
* console.log(`Balance updated for ${data.memberName}`);
* break;
* // ... handle other events
* }
*
* res.status(200).json({ received: true });
* });
* ```
*/
export declare function verifyWebhookSignature(payload: string, signature: string, timestamp: string, secret: string, options?: VerifyWebhookOptions): Promise<VerifyWebhookResult>;
/**
* Webhook utilities for verifying Offcoin webhook signatures
*/
/**
* Webhook event types that can be subscribed to
*/
export const WebhookEventTypes = {
MEMBER_BALANCE_UPDATED: 'member.balance_updated',
MEMBER_XP_UPDATED: 'member.xp_updated',
ACHIEVEMENT_UNLOCKED: 'achievement.unlocked',
PERMISSION_GRANTED: 'permission.granted'
};
/**
* Compute an HMAC-SHA256 signature for webhook verification
* @internal
*/
async function computeSignature(payload, timestamp, secret) {
const signedPayload = `${timestamp}.${payload}`;
const encoder = new TextEncoder();
// Use Web Crypto API (works in Node.js 18+, browsers, and edge runtimes)
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(signedPayload));
const hashArray = Array.from(new Uint8Array(signature));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
return `v1=${hashHex}`;
}
/**
* Verify a webhook signature from Offcoin
*
* @param payload - The raw request body as a string
* @param signature - The value from the X-Webhook-Signature header
* @param timestamp - The value from the X-Webhook-Timestamp header
* @param secret - Your webhook signing secret (starts with 'whsec_')
* @param options - Verification options
* @returns Verification result with parsed payload if valid
*
* @example
* ```typescript
* import { verifyWebhookSignature } from '@offcoin/sdk';
*
* // In your webhook handler (e.g., Express)
* app.post('/webhooks/offcoin', async (req, res) => {
* const signature = req.headers['x-webhook-signature'] as string;
* const timestamp = req.headers['x-webhook-timestamp'] as string;
*
* const result = await verifyWebhookSignature(
* req.body, // raw body string
* signature,
* timestamp,
* process.env.OFFCOIN_WEBHOOK_SECRET!
* );
*
* if (!result.valid) {
* return res.status(401).json({ error: result.error });
* }
*
* // Process the verified webhook
* const { type, data } = result.payload!;
* switch (type) {
* case 'member.balance_updated':
* console.log(`Balance updated for ${data.memberName}`);
* break;
* // ... handle other events
* }
*
* res.status(200).json({ received: true });
* });
* ```
*/
export async function verifyWebhookSignature(payload, signature, timestamp, secret, options = {}) {
const { maxAgeSeconds = 300 } = options;
// Validate inputs
if (!payload) {
return { valid: false, error: 'Missing payload' };
}
if (!signature) {
return { valid: false, error: 'Missing signature' };
}
if (!timestamp) {
return { valid: false, error: 'Missing timestamp' };
}
if (!secret) {
return { valid: false, error: 'Missing secret' };
}
// Parse timestamp
const timestampNum = parseInt(timestamp, 10);
if (isNaN(timestampNum)) {
return { valid: false, error: 'Invalid timestamp format' };
}
// Check timestamp age (prevent replay attacks)
if (maxAgeSeconds > 0) {
const now = Math.floor(Date.now() / 1000);
const age = now - timestampNum;
if (age > maxAgeSeconds) {
return { valid: false, error: `Webhook too old (${age}s > ${maxAgeSeconds}s)` };
}
if (age < -60) {
// Allow 1 minute clock skew
return { valid: false, error: 'Webhook timestamp is in the future' };
}
}
// Compute expected signature
const expectedSignature = await computeSignature(payload, timestampNum, secret);
// Constant-time comparison to prevent timing attacks
if (signature.length !== expectedSignature.length) {
return { valid: false, error: 'Invalid signature' };
}
let match = true;
for (let i = 0; i < signature.length; i++) {
if (signature[i] !== expectedSignature[i]) {
match = false;
}
}
if (!match) {
return { valid: false, error: 'Invalid signature' };
}
// Parse and return payload
try {
const parsedPayload = JSON.parse(payload);
return { valid: true, payload: parsedPayload };
}
catch {
return { valid: false, error: 'Failed to parse payload JSON' };
}
}
+1
-0

@@ -60,1 +60,2 @@ /**

export { OffcoinError, AuthenticationError, NotFoundError, ValidationError, ForbiddenError, BadRequestError, InternalError, RateLimitedError, NetworkError } from './errors.js';
export { verifyWebhookSignature, WebhookEventTypes, type WebhookEventType, type WebhookPayload, type BalanceUpdatedData, type XpUpdatedData, type AchievementUnlockedData, type PermissionGrantedData, type VerifyWebhookResult, type VerifyWebhookOptions } from './webhooks.js';

@@ -66,1 +66,3 @@ /**

export { OffcoinError, AuthenticationError, NotFoundError, ValidationError, ForbiddenError, BadRequestError, InternalError, RateLimitedError, NetworkError } from './errors.js';
// Re-export webhook utilities
export { verifyWebhookSignature, WebhookEventTypes } from './webhooks.js';
+1
-1
{
"name": "@offcoin/sdk",
"version": "0.0.4",
"version": "0.0.5",
"description": "TypeScript/JavaScript SDK for the Offcoin API",

@@ -5,0 +5,0 @@ "scripts": {

+169
-1

@@ -128,2 +128,163 @@ # @offcoin/sdk

## Webhooks
Offcoin can send HTTP notifications to your server when events occur. The SDK provides utilities for verifying webhook signatures to ensure authenticity.
### Setting Up Webhooks
1. Go to **Settings > Webhooks** in your Offcoin admin dashboard
2. Click **Create webhook** and enter your endpoint URL
3. Select the events you want to receive
4. Copy the signing secret (shown only once)
### Event Types
| Event | Description |
|-------|-------------|
| `member.balance_updated` | Token balance changed (add/subtract) |
| `member.xp_updated` | XP changed |
| `achievement.unlocked` | Member unlocked an achievement |
| `permission.granted` | Member received a permission (via level or purchase) |
### Verifying Webhook Signatures
Always verify webhook signatures to ensure requests are from Offcoin:
```typescript
import { verifyWebhookSignature, WebhookEventTypes } from '@offcoin/sdk';
// Express.js example - note: you need raw body, not parsed JSON
app.post('/webhooks/offcoin', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const timestamp = req.headers['x-webhook-timestamp'] as string;
const rawBody = req.body.toString();
const result = await verifyWebhookSignature(
rawBody,
signature,
timestamp,
process.env.OFFCOIN_WEBHOOK_SECRET!
);
if (!result.valid) {
console.error('Webhook verification failed:', result.error);
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the verified webhook
const { type, data } = result.payload!;
switch (type) {
case WebhookEventTypes.MEMBER_BALANCE_UPDATED:
console.log(`Balance: ${data.memberName} now has ${data.newBalance} tokens`);
break;
case WebhookEventTypes.MEMBER_XP_UPDATED:
console.log(`XP: ${data.memberName} is now level ${data.newLevel}`);
break;
case WebhookEventTypes.ACHIEVEMENT_UNLOCKED:
console.log(`Achievement: ${data.memberName} unlocked "${data.achievementName}"`);
break;
case WebhookEventTypes.PERMISSION_GRANTED:
console.log(`Permission: ${data.memberName} received "${data.permissionName}"`);
break;
}
res.status(200).json({ received: true });
});
```
### SvelteKit Example
```typescript
// src/routes/webhooks/offcoin/+server.ts
import { json } from '@sveltejs/kit';
import { verifyWebhookSignature } from '@offcoin/sdk';
import { OFFCOIN_WEBHOOK_SECRET } from '$env/static/private';
export async function POST({ request }) {
const signature = request.headers.get('x-webhook-signature');
const timestamp = request.headers.get('x-webhook-timestamp');
const rawBody = await request.text();
const result = await verifyWebhookSignature(
rawBody,
signature ?? '',
timestamp ?? '',
OFFCOIN_WEBHOOK_SECRET
);
if (!result.valid) {
return json({ error: result.error }, { status: 401 });
}
const { type, data } = result.payload!;
// Handle webhook...
return json({ received: true });
}
```
### Webhook Payload Structure
All webhooks include these fields:
```typescript
interface WebhookPayload {
id: string; // Unique event ID
type: string; // Event type (e.g., "member.balance_updated")
timestamp: string; // ISO 8601 timestamp
tenantId: string; // Your workspace ID
data: object; // Event-specific data
}
```
### Webhook Types
```typescript
import type {
WebhookPayload,
BalanceUpdatedData,
XpUpdatedData,
AchievementUnlockedData,
PermissionGrantedData
} from '@offcoin/sdk';
// Type-safe webhook handling
function handleBalanceUpdate(payload: WebhookPayload<BalanceUpdatedData>) {
const { memberId, memberName, previousBalance, newBalance, amount, reason } = payload.data;
}
```
### Verification Options
```typescript
const result = await verifyWebhookSignature(
rawBody,
signature,
timestamp,
secret,
{
maxAgeSeconds: 300 // Reject webhooks older than 5 minutes (default)
}
);
```
### Manual Signature Verification
If you prefer to implement verification yourself, here's the algorithm:
1. Concatenate timestamp and payload: `{timestamp}.{payload}`
2. Compute HMAC-SHA256 using your webhook secret
3. Compare with signature header (format: `v1={hex}`)
```typescript
// Headers sent with each webhook
// X-Webhook-Signature: v1=abc123...
// X-Webhook-Timestamp: 1702828800
// X-Webhook-Id: evt_xxx
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = 'v1=' + hmacSHA256(signedPayload, secret);
```
## Error Handling

@@ -181,3 +342,10 @@

Permission,
PurchasePermissionResult
PurchasePermissionResult,
// Webhook types
WebhookPayload,
WebhookEventType,
BalanceUpdatedData,
XpUpdatedData,
AchievementUnlockedData,
PermissionGrantedData
} from '@offcoin/sdk';

@@ -184,0 +352,0 @@ ```