@feelflow/ffid-sdk 0.1.0 → 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.
@@ -0,0 +1,237 @@
1
+ import { createHmac, timingSafeEqual } from 'crypto';
2
+
3
+ // src/webhooks/constants.ts
4
+ var FFID_WEBHOOK_SIGNATURE_HEADER = "X-FFID-Signature";
5
+ var FFID_WEBHOOK_TIMESTAMP_HEADER = "X-FFID-Timestamp";
6
+ var FFID_WEBHOOK_EVENT_ID_HEADER = "X-FFID-Event-ID";
7
+ var FFID_WEBHOOK_SIGNATURE_VERSION = "v1";
8
+ var DEFAULT_TOLERANCE_SECONDS = 300;
9
+ var MILLISECONDS_PER_SECOND = 1e3;
10
+
11
+ // src/webhooks/errors.ts
12
+ var FFIDWebhookError = class extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "FFIDWebhookError";
16
+ }
17
+ };
18
+ var FFIDWebhookSignatureError = class extends FFIDWebhookError {
19
+ constructor(message = "Webhook signature verification failed") {
20
+ super(message);
21
+ this.name = "FFIDWebhookSignatureError";
22
+ }
23
+ };
24
+ var FFIDWebhookTimestampError = class extends FFIDWebhookError {
25
+ constructor(message = "Webhook timestamp is outside tolerance window") {
26
+ super(message);
27
+ this.name = "FFIDWebhookTimestampError";
28
+ }
29
+ };
30
+ var FFIDWebhookPayloadError = class extends FFIDWebhookError {
31
+ constructor(message = "Webhook payload is not valid JSON") {
32
+ super(message);
33
+ this.name = "FFIDWebhookPayloadError";
34
+ }
35
+ };
36
+ function parseSignatureHeader(header) {
37
+ const parts = header.split(",");
38
+ let timestamp = null;
39
+ let signature = null;
40
+ for (const part of parts) {
41
+ const [key, value] = part.split("=", 2);
42
+ if (key === "t" && value) {
43
+ timestamp = parseInt(value, 10);
44
+ } else if (key === FFID_WEBHOOK_SIGNATURE_VERSION && value) {
45
+ signature = value;
46
+ }
47
+ }
48
+ if (timestamp === null || isNaN(timestamp) || !signature) {
49
+ return null;
50
+ }
51
+ return { timestamp, signature };
52
+ }
53
+ function computeSignature(secret, timestamp, body) {
54
+ const signedContent = `${timestamp}.${body}`;
55
+ return createHmac("sha256", secret).update(signedContent).digest("hex");
56
+ }
57
+ function verifyWebhookSignature(rawBody, signatureHeader, secret, toleranceSeconds = DEFAULT_TOLERANCE_SECONDS) {
58
+ const parsed = parseSignatureHeader(signatureHeader);
59
+ if (!parsed) {
60
+ throw new FFIDWebhookSignatureError("Invalid signature header format");
61
+ }
62
+ const { timestamp, signature } = parsed;
63
+ const now = Math.floor(Date.now() / MILLISECONDS_PER_SECOND);
64
+ if (Math.abs(now - timestamp) > toleranceSeconds) {
65
+ throw new FFIDWebhookTimestampError(
66
+ `Webhook timestamp is outside tolerance window (${toleranceSeconds}s)`
67
+ );
68
+ }
69
+ const expected = computeSignature(secret, timestamp, rawBody);
70
+ const HEX_REGEX = /^[0-9a-f]+$/i;
71
+ if (!HEX_REGEX.test(signature)) {
72
+ throw new FFIDWebhookSignatureError("Webhook signature verification failed");
73
+ }
74
+ const sigBuffer = Buffer.from(signature, "hex");
75
+ const expectedBuffer = Buffer.from(expected, "hex");
76
+ if (sigBuffer.length !== expectedBuffer.length) {
77
+ throw new FFIDWebhookSignatureError("Webhook signature verification failed");
78
+ }
79
+ if (!timingSafeEqual(sigBuffer, expectedBuffer)) {
80
+ throw new FFIDWebhookSignatureError("Webhook signature verification failed");
81
+ }
82
+ try {
83
+ return JSON.parse(rawBody);
84
+ } catch {
85
+ throw new FFIDWebhookPayloadError("Webhook payload is not valid JSON");
86
+ }
87
+ }
88
+
89
+ // src/webhooks/handler.ts
90
+ var SDK_LOG_PREFIX = "[FFID Webhook SDK]";
91
+ var HTTP_OK = 200;
92
+ var HTTP_BAD_REQUEST = 400;
93
+ var noopLogger = {
94
+ debug: () => {
95
+ },
96
+ info: () => {
97
+ },
98
+ warn: () => {
99
+ },
100
+ error: (...args) => console.error(SDK_LOG_PREFIX, ...args)
101
+ };
102
+ function createFFIDWebhookHandler(config) {
103
+ if (!config.secret) {
104
+ throw new Error("FFID Webhook Handler: secret is required");
105
+ }
106
+ const logger = config.logger ?? noopLogger;
107
+ const toleranceSeconds = config.toleranceSeconds;
108
+ const handlers = /* @__PURE__ */ new Map();
109
+ const allHandlers = /* @__PURE__ */ new Set();
110
+ function on(eventType, handler) {
111
+ if (!handlers.has(eventType)) {
112
+ handlers.set(eventType, /* @__PURE__ */ new Set());
113
+ }
114
+ handlers.get(eventType).add(handler);
115
+ logger.debug(`Registered handler for ${eventType}`);
116
+ }
117
+ function onAll(handler) {
118
+ allHandlers.add(handler);
119
+ logger.debug("Registered catch-all handler");
120
+ }
121
+ function off(eventType, handler) {
122
+ const set = handlers.get(eventType);
123
+ if (set) {
124
+ set.delete(handler);
125
+ if (set.size === 0) {
126
+ handlers.delete(eventType);
127
+ }
128
+ }
129
+ }
130
+ async function dispatchEvent(event) {
131
+ const typeHandlers = handlers.get(event.type);
132
+ const promises = [];
133
+ if (typeHandlers) {
134
+ for (const handler of typeHandlers) {
135
+ promises.push(Promise.resolve(handler(event)));
136
+ }
137
+ }
138
+ for (const handler of allHandlers) {
139
+ promises.push(Promise.resolve(handler(event)));
140
+ }
141
+ await Promise.all(promises);
142
+ }
143
+ async function handleEvent(rawBody, signatureHeader) {
144
+ const event = verifyWebhookSignature(
145
+ rawBody,
146
+ signatureHeader,
147
+ config.secret,
148
+ toleranceSeconds
149
+ );
150
+ logger.info(`Received event: ${event.type} (${event.id})`);
151
+ await dispatchEvent(event);
152
+ return event;
153
+ }
154
+ async function handleRaw(rawBody, headers) {
155
+ const headerLower = FFID_WEBHOOK_SIGNATURE_HEADER.toLowerCase();
156
+ let signatureHeader;
157
+ for (const [key, value] of Object.entries(headers)) {
158
+ if (key.toLowerCase() === headerLower) {
159
+ signatureHeader = value;
160
+ break;
161
+ }
162
+ }
163
+ if (!signatureHeader) {
164
+ throw new FFIDWebhookError(
165
+ `Missing ${FFID_WEBHOOK_SIGNATURE_HEADER} header`
166
+ );
167
+ }
168
+ return handleEvent(rawBody, signatureHeader);
169
+ }
170
+ function expressMiddleware() {
171
+ return (req, res, _next) => {
172
+ const rawBody = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf-8") : JSON.stringify(req.body);
173
+ const signatureHeader = req.headers[FFID_WEBHOOK_SIGNATURE_HEADER.toLowerCase()];
174
+ if (!signatureHeader) {
175
+ res.status(HTTP_BAD_REQUEST).json({
176
+ error: `Missing ${FFID_WEBHOOK_SIGNATURE_HEADER} header`
177
+ });
178
+ return;
179
+ }
180
+ handleEvent(rawBody, signatureHeader).then((event) => {
181
+ res.status(HTTP_OK).json({ received: true, eventId: event.id });
182
+ }).catch((error) => {
183
+ logger.error("Webhook processing failed:", error);
184
+ const message = error instanceof Error ? error.message : "Webhook processing failed";
185
+ res.status(HTTP_BAD_REQUEST).json({ error: message });
186
+ });
187
+ };
188
+ }
189
+ function nextHandler() {
190
+ return async (request) => {
191
+ try {
192
+ const rawBody = await request.text();
193
+ const signatureHeader = request.headers.get(
194
+ FFID_WEBHOOK_SIGNATURE_HEADER
195
+ );
196
+ if (!signatureHeader) {
197
+ return new Response(
198
+ JSON.stringify({
199
+ error: `Missing ${FFID_WEBHOOK_SIGNATURE_HEADER} header`
200
+ }),
201
+ { status: HTTP_BAD_REQUEST, headers: { "Content-Type": "application/json" } }
202
+ );
203
+ }
204
+ const event = await handleEvent(rawBody, signatureHeader);
205
+ return new Response(
206
+ JSON.stringify({ received: true, eventId: event.id }),
207
+ { status: HTTP_OK, headers: { "Content-Type": "application/json" } }
208
+ );
209
+ } catch (error) {
210
+ logger.error("Webhook processing failed:", error);
211
+ const message = error instanceof Error ? error.message : "Webhook processing failed";
212
+ return new Response(
213
+ JSON.stringify({ error: message }),
214
+ { status: HTTP_BAD_REQUEST, headers: { "Content-Type": "application/json" } }
215
+ );
216
+ }
217
+ };
218
+ }
219
+ return {
220
+ /** Register a handler for a specific event type */
221
+ on,
222
+ /** Register a handler that receives all events */
223
+ onAll,
224
+ /** Unregister a handler for a specific event type */
225
+ off,
226
+ /** Verify signature and dispatch event (core method) */
227
+ handleEvent,
228
+ /** Process webhook from raw body + headers object */
229
+ handleRaw,
230
+ /** Express/Connect middleware */
231
+ expressMiddleware,
232
+ /** Next.js App Router route handler */
233
+ nextHandler
234
+ };
235
+ }
236
+
237
+ export { DEFAULT_TOLERANCE_SECONDS, FFIDWebhookError, FFIDWebhookPayloadError, FFIDWebhookSignatureError, FFIDWebhookTimestampError, FFID_WEBHOOK_EVENT_ID_HEADER, FFID_WEBHOOK_SIGNATURE_HEADER, FFID_WEBHOOK_SIGNATURE_VERSION, FFID_WEBHOOK_TIMESTAMP_HEADER, computeSignature, createFFIDWebhookHandler, parseSignatureHeader, verifyWebhookSignature };
package/package.json CHANGED
@@ -1,8 +1,15 @@
1
1
  {
2
2
  "name": "@feelflow/ffid-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "FeelFlow ID Platform SDK for React/Next.js applications",
5
- "keywords": ["feelflow", "ffid", "sdk", "react", "nextjs", "authentication"],
5
+ "keywords": [
6
+ "feelflow",
7
+ "ffid",
8
+ "sdk",
9
+ "react",
10
+ "nextjs",
11
+ "authentication"
12
+ ],
6
13
  "author": "FeelFlow <dev@feelflow.co.jp>",
7
14
  "license": "MIT",
8
15
  "homepage": "https://github.com/feel-flow/feelflow-id-platform/tree/develop/sdk/typescript#readme",
@@ -33,6 +40,16 @@
33
40
  "types": "./dist/legal/index.d.ts",
34
41
  "import": "./dist/legal/index.js",
35
42
  "require": "./dist/legal/index.cjs"
43
+ },
44
+ "./webhooks": {
45
+ "types": "./dist/webhooks/index.d.ts",
46
+ "import": "./dist/webhooks/index.js",
47
+ "require": "./dist/webhooks/index.cjs"
48
+ },
49
+ "./announcements": {
50
+ "types": "./dist/announcements/index.d.ts",
51
+ "import": "./dist/announcements/index.js",
52
+ "require": "./dist/announcements/index.cjs"
36
53
  }
37
54
  },
38
55
  "files": [
@@ -41,7 +58,7 @@
41
58
  ],
42
59
  "sideEffects": false,
43
60
  "scripts": {
44
- "build": "rm -rf dist && tsup",
61
+ "build": "rimraf dist && tsup",
45
62
  "dev": "tsup --watch",
46
63
  "type-check": "tsc --noEmit",
47
64
  "lint": "eslint src",
@@ -71,6 +88,7 @@
71
88
  "jsdom": "^27.0.0",
72
89
  "react": "^19.0.0",
73
90
  "react-dom": "^19.0.0",
91
+ "rimraf": "^6.1.2",
74
92
  "tsup": "^8.0.0",
75
93
  "typescript": "^5.0.0",
76
94
  "vitest": "^4.0.0"