api-ape 3.0.1 → 4.1.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 +58 -570
- package/client/README.md +73 -14
- package/client/auth/crypto/aead.js +214 -0
- package/client/auth/crypto/constants.js +32 -0
- package/client/auth/crypto/encoding.js +104 -0
- package/client/auth/crypto/files.md +27 -0
- package/client/auth/crypto/kdf.js +217 -0
- package/client/auth/crypto-utils.js +118 -0
- package/client/auth/files.md +52 -0
- package/client/auth/key-recovery.js +288 -0
- package/client/auth/recovery/constants.js +37 -0
- package/client/auth/recovery/files.md +23 -0
- package/client/auth/recovery/key-derivation.js +61 -0
- package/client/auth/recovery/sss-browser.js +189 -0
- package/client/auth/share-storage.js +205 -0
- package/client/auth/storage/constants.js +18 -0
- package/client/auth/storage/db.js +132 -0
- package/client/auth/storage/files.md +27 -0
- package/client/auth/storage/keys.js +173 -0
- package/client/auth/storage/shares.js +200 -0
- package/client/browser.js +190 -23
- package/client/connectSocket.js +418 -988
- package/client/connection/README.md +23 -0
- package/client/connection/fileDownload.js +256 -0
- package/client/connection/fileHandling.js +450 -0
- package/client/connection/fileUtils.js +346 -0
- package/client/connection/files.md +71 -0
- package/client/connection/messageHandler.js +105 -0
- package/client/connection/network.js +350 -0
- package/client/connection/proxy.js +233 -0
- package/client/connection/sender.js +333 -0
- package/client/connection/state.js +321 -0
- package/client/connection/subscriptions.js +151 -0
- package/client/files.md +53 -0
- package/client/index.js +298 -142
- package/client/transports/README.md +50 -0
- package/client/transports/files.md +41 -0
- package/client/transports/streamParser.js +195 -0
- package/client/transports/streaming.js +555 -202
- package/dist/ape.js +6 -1
- package/dist/ape.js.map +4 -4
- package/index.d.ts +38 -16
- package/package.json +32 -7
- package/server/README.md +287 -53
- package/server/adapters/README.md +28 -19
- package/server/adapters/files.md +68 -0
- package/server/adapters/firebase.js +543 -160
- package/server/adapters/index.js +362 -112
- package/server/adapters/mongo.js +530 -140
- package/server/adapters/postgres.js +534 -155
- package/server/adapters/redis.js +508 -143
- package/server/adapters/supabase.js +555 -186
- package/server/client/README.md +43 -0
- package/server/client/connection.js +586 -0
- package/server/client/files.md +40 -0
- package/server/client/index.js +342 -0
- package/server/files.md +54 -0
- package/server/index.js +332 -27
- package/server/lib/README.md +26 -0
- package/server/lib/broadcast/clients.js +219 -0
- package/server/lib/broadcast/files.md +58 -0
- package/server/lib/broadcast/index.js +57 -0
- package/server/lib/broadcast/publishProxy.js +110 -0
- package/server/lib/broadcast/pubsub.js +137 -0
- package/server/lib/broadcast/sendProxy.js +103 -0
- package/server/lib/bun.js +315 -99
- package/server/lib/fileTransfer/README.md +63 -0
- package/server/lib/fileTransfer/files.md +30 -0
- package/server/lib/fileTransfer/streaming.js +435 -0
- package/server/lib/fileTransfer.js +710 -326
- package/server/lib/files.md +111 -0
- package/server/lib/httpUtils.js +283 -0
- package/server/lib/loader.js +208 -7
- package/server/lib/longPolling/README.md +63 -0
- package/server/lib/longPolling/files.md +44 -0
- package/server/lib/longPolling/getHandler.js +365 -0
- package/server/lib/longPolling/postHandler.js +327 -0
- package/server/lib/longPolling.js +174 -221
- package/server/lib/main.js +369 -532
- package/server/lib/runtimes/README.md +42 -0
- package/server/lib/runtimes/bun.js +586 -0
- package/server/lib/runtimes/files.md +56 -0
- package/server/lib/runtimes/node.js +511 -0
- package/server/lib/wiring.js +539 -98
- package/server/lib/ws/README.md +35 -0
- package/server/lib/ws/adapters/README.md +54 -0
- package/server/lib/ws/adapters/bun.js +538 -170
- package/server/lib/ws/adapters/deno.js +623 -149
- package/server/lib/ws/adapters/files.md +42 -0
- package/server/lib/ws/files.md +74 -0
- package/server/lib/ws/frames.js +532 -154
- package/server/lib/ws/index.js +207 -10
- package/server/lib/ws/server.js +385 -92
- package/server/lib/ws/socket.js +549 -181
- package/server/lib/wsProvider.js +363 -89
- package/server/plugins/binary.js +282 -0
- package/server/security/README.md +92 -0
- package/server/security/auth/README.md +319 -0
- package/server/security/auth/adapters/files.md +95 -0
- package/server/security/auth/adapters/ldap/constants.js +37 -0
- package/server/security/auth/adapters/ldap/files.md +19 -0
- package/server/security/auth/adapters/ldap/helpers.js +111 -0
- package/server/security/auth/adapters/ldap.js +353 -0
- package/server/security/auth/adapters/oauth2/constants.js +41 -0
- package/server/security/auth/adapters/oauth2/files.md +19 -0
- package/server/security/auth/adapters/oauth2/helpers.js +123 -0
- package/server/security/auth/adapters/oauth2.js +273 -0
- package/server/security/auth/adapters/opaque-handlers.js +314 -0
- package/server/security/auth/adapters/opaque.js +205 -0
- package/server/security/auth/adapters/saml/constants.js +52 -0
- package/server/security/auth/adapters/saml/files.md +19 -0
- package/server/security/auth/adapters/saml/helpers.js +74 -0
- package/server/security/auth/adapters/saml.js +173 -0
- package/server/security/auth/adapters/totp.js +703 -0
- package/server/security/auth/adapters/webauthn.js +625 -0
- package/server/security/auth/files.md +61 -0
- package/server/security/auth/framework/constants.js +27 -0
- package/server/security/auth/framework/files.md +23 -0
- package/server/security/auth/framework/handlers.js +272 -0
- package/server/security/auth/framework/socket-auth.js +177 -0
- package/server/security/auth/handlers/auth-messages.js +143 -0
- package/server/security/auth/handlers/files.md +28 -0
- package/server/security/auth/index.js +290 -0
- package/server/security/auth/mfa/crypto/aead.js +148 -0
- package/server/security/auth/mfa/crypto/constants.js +35 -0
- package/server/security/auth/mfa/crypto/files.md +27 -0
- package/server/security/auth/mfa/crypto/kdf.js +120 -0
- package/server/security/auth/mfa/crypto/utils.js +68 -0
- package/server/security/auth/mfa/crypto-utils.js +80 -0
- package/server/security/auth/mfa/files.md +77 -0
- package/server/security/auth/mfa/ledger/constants.js +75 -0
- package/server/security/auth/mfa/ledger/errors.js +73 -0
- package/server/security/auth/mfa/ledger/files.md +23 -0
- package/server/security/auth/mfa/ledger/share-record.js +32 -0
- package/server/security/auth/mfa/ledger.js +255 -0
- package/server/security/auth/mfa/recovery/constants.js +67 -0
- package/server/security/auth/mfa/recovery/files.md +19 -0
- package/server/security/auth/mfa/recovery/handlers.js +216 -0
- package/server/security/auth/mfa/recovery.js +191 -0
- package/server/security/auth/mfa/sss/constants.js +21 -0
- package/server/security/auth/mfa/sss/files.md +23 -0
- package/server/security/auth/mfa/sss/gf256.js +103 -0
- package/server/security/auth/mfa/sss/serialization.js +82 -0
- package/server/security/auth/mfa/sss.js +161 -0
- package/server/security/auth/mfa/two-of-three/constants.js +58 -0
- package/server/security/auth/mfa/two-of-three/files.md +23 -0
- package/server/security/auth/mfa/two-of-three/handlers.js +241 -0
- package/server/security/auth/mfa/two-of-three/helpers.js +71 -0
- package/server/security/auth/mfa/two-of-three.js +136 -0
- package/server/security/auth/nonce-manager.js +89 -0
- package/server/security/auth/state-machine-mfa.js +269 -0
- package/server/security/auth/state-machine.js +257 -0
- package/server/security/extractRootDomain.js +144 -16
- package/server/security/files.md +51 -0
- package/server/security/origin.js +197 -15
- package/server/security/reply.js +274 -16
- package/server/socket/README.md +119 -0
- package/server/socket/authMiddleware.js +299 -0
- package/server/socket/files.md +86 -0
- package/server/socket/open.js +154 -8
- package/server/socket/pluginHooks.js +334 -0
- package/server/socket/receive.js +184 -225
- package/server/socket/receiveContext.js +117 -0
- package/server/socket/send.js +416 -78
- package/server/socket/tagUtils.js +402 -0
- package/server/utils/README.md +19 -0
- package/server/utils/deepRequire.js +255 -30
- package/server/utils/files.md +57 -0
- package/server/utils/genId.js +182 -20
- package/server/utils/parseUserAgent.js +313 -251
- package/server/utils/userAgent/README.md +65 -0
- package/server/utils/userAgent/files.md +46 -0
- package/server/utils/userAgent/patterns.js +545 -0
- package/utils/README.md +21 -0
- package/utils/files.md +66 -0
- package/utils/jss/README.md +21 -0
- package/utils/jss/decode.js +471 -0
- package/utils/jss/encode.js +312 -0
- package/utils/jss/files.md +68 -0
- package/utils/jss/plugins.js +210 -0
- package/utils/jss.js +219 -273
- package/utils/messageHash.js +238 -35
- package/dist/api-ape.min.js +0 -2
- package/dist/api-ape.min.js.map +0 -7
- package/server/client.js +0 -308
- package/server/lib/broadcast.js +0 -146
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Binary Data Transfer Plugins for JSS
|
|
3
|
+
*
|
|
4
|
+
* This module provides plugins for handling binary data transfer in api-ape.
|
|
5
|
+
* It moves the binary handling logic from hardcoded server code into the
|
|
6
|
+
* pluggable JSS system.
|
|
7
|
+
*
|
|
8
|
+
* ## Plugin Tags
|
|
9
|
+
*
|
|
10
|
+
* | Tag | Direction | Description |
|
|
11
|
+
* |-----|-----------------|------------------------------------------------|
|
|
12
|
+
* | `I` | Server→Client | Inline base64 for small binary (<=100 chars) |
|
|
13
|
+
* | `L` | Server→Client | Link to downloadable binary data (large) |
|
|
14
|
+
* | `B` | Client→Server | Buffer upload (resolves to Buffer) |
|
|
15
|
+
* | `A` | Client→Server | ArrayBuffer upload (resolves to ArrayBuffer) |
|
|
16
|
+
* | `F` | Client→Client | Streaming file transfer |
|
|
17
|
+
*
|
|
18
|
+
* ## Usage
|
|
19
|
+
*
|
|
20
|
+
* ```javascript
|
|
21
|
+
* const { registerBinaryPlugins } = require('./plugins/binary')
|
|
22
|
+
*
|
|
23
|
+
* // During server initialization
|
|
24
|
+
* registerBinaryPlugins()
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @module server/plugins/binary
|
|
28
|
+
* @see {@link module:utils/jss/plugins} for plugin system
|
|
29
|
+
* @see {@link module:server/lib/fileTransfer} for file transfer management
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const jss = require("../../utils/jss");
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maximum size in base64 characters for inline binary encoding
|
|
36
|
+
* Data up to this size will be inlined as base64 in the message.
|
|
37
|
+
* Larger data will use HTTP transfer (L tag).
|
|
38
|
+
*
|
|
39
|
+
* 100 base64 chars ≈ 75 raw bytes
|
|
40
|
+
* @constant {number}
|
|
41
|
+
*/
|
|
42
|
+
const INLINE_BASE64_THRESHOLD = 100;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a value is binary data
|
|
46
|
+
*
|
|
47
|
+
* @param {any} value - Value to check
|
|
48
|
+
* @returns {boolean} True if value is Buffer, ArrayBuffer, or TypedArray
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
function isBinaryData(value) {
|
|
52
|
+
if (value === null || value === undefined) return false;
|
|
53
|
+
return (
|
|
54
|
+
Buffer.isBuffer(value) ||
|
|
55
|
+
value instanceof ArrayBuffer ||
|
|
56
|
+
ArrayBuffer.isView(value)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the base64 encoded length for binary data
|
|
62
|
+
*
|
|
63
|
+
* @param {Buffer|ArrayBuffer|ArrayBufferView} value - Binary data
|
|
64
|
+
* @returns {number} Length when encoded as base64
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
function getBase64Length(value) {
|
|
68
|
+
let byteLength;
|
|
69
|
+
if (Buffer.isBuffer(value)) {
|
|
70
|
+
byteLength = value.length;
|
|
71
|
+
} else if (value instanceof ArrayBuffer) {
|
|
72
|
+
byteLength = value.byteLength;
|
|
73
|
+
} else if (ArrayBuffer.isView(value)) {
|
|
74
|
+
byteLength = value.byteLength;
|
|
75
|
+
} else {
|
|
76
|
+
return Infinity; // Unknown type, use HTTP transfer
|
|
77
|
+
}
|
|
78
|
+
// Base64 encoding increases size by ~33%
|
|
79
|
+
return Math.ceil((byteLength * 4) / 3);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Register binary data plugins for server-side use
|
|
84
|
+
*
|
|
85
|
+
* Call this during server initialization to enable binary transfer
|
|
86
|
+
* functionality through the JSS plugin system.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* const { registerBinaryPlugins } = require('./plugins/binary')
|
|
90
|
+
* registerBinaryPlugins()
|
|
91
|
+
*
|
|
92
|
+
* // Now binary data in controller responses will be handled automatically
|
|
93
|
+
* // Controller: return { image: Buffer.from(...) }
|
|
94
|
+
* // Client receives: { 'image<!L>': 'hash123' }
|
|
95
|
+
*/
|
|
96
|
+
function registerBinaryPlugins() {
|
|
97
|
+
const { FileTransferManager } = require("../lib/fileTransfer");
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* I tag: Inline base64 for small binary data
|
|
101
|
+
*
|
|
102
|
+
* For small binary data (<=100 base64 chars / ~75 bytes):
|
|
103
|
+
* 1. Converts to base64 inline in the message
|
|
104
|
+
* 2. No HTTP transfer required - reduces latency
|
|
105
|
+
*
|
|
106
|
+
* NOTE: Must be registered BEFORE L tag so it's checked first
|
|
107
|
+
*/
|
|
108
|
+
jss.custom("I", {
|
|
109
|
+
// Check if value is SMALL binary data that should be inlined
|
|
110
|
+
check: (key, value) => {
|
|
111
|
+
if (!isBinaryData(value)) return false;
|
|
112
|
+
return getBase64Length(value) <= INLINE_BASE64_THRESHOLD;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Encode: convert to base64 string
|
|
116
|
+
encode: (path, key, value, context) => {
|
|
117
|
+
const buffer = Buffer.isBuffer(value)
|
|
118
|
+
? value
|
|
119
|
+
: Buffer.from(
|
|
120
|
+
value instanceof ArrayBuffer
|
|
121
|
+
? value
|
|
122
|
+
: value.buffer.slice(
|
|
123
|
+
value.byteOffset,
|
|
124
|
+
value.byteOffset + value.byteLength,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
return buffer.toString("base64");
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// Decode: handled by built-in I decoder in decode.js
|
|
131
|
+
decode: (value, path, context) => Buffer.from(value, "base64"),
|
|
132
|
+
|
|
133
|
+
// No onSend needed - data is inlined, no HTTP transfer
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* L tag: Server → Client downloads
|
|
138
|
+
*
|
|
139
|
+
* When a controller returns LARGE binary data (Buffer, ArrayBuffer, TypedArray),
|
|
140
|
+
* this plugin:
|
|
141
|
+
* 1. Registers the data as a pending download
|
|
142
|
+
* 2. Replaces the value with a hash reference
|
|
143
|
+
* 3. Client fetches via HTTP GET /api/ape/data/{hash}
|
|
144
|
+
*
|
|
145
|
+
* NOTE: Small binary data is handled by I tag (inline base64)
|
|
146
|
+
*/
|
|
147
|
+
jss.custom("L", {
|
|
148
|
+
// Check if value is LARGE binary data that should be sent as download
|
|
149
|
+
check: (key, value) => {
|
|
150
|
+
if (!isBinaryData(value)) return false;
|
|
151
|
+
return getBase64Length(value) > INLINE_BASE64_THRESHOLD;
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Encode: return placeholder (actual value set by onSend)
|
|
155
|
+
encode: (path, key, value, context) => "__pending__",
|
|
156
|
+
|
|
157
|
+
// Decode: on client side, the hash is returned as-is
|
|
158
|
+
// Client will fetch the actual data via HTTP
|
|
159
|
+
decode: (value, path, context) => value,
|
|
160
|
+
|
|
161
|
+
// onSend: register the binary data for HTTP download
|
|
162
|
+
onSend: (path, key, value, context) => {
|
|
163
|
+
const pathStr = path.length > 0 ? path.join(".") : "root";
|
|
164
|
+
const hash = FileTransferManager.generateHash(context.queryId, pathStr);
|
|
165
|
+
|
|
166
|
+
// Detect content type (could be enhanced with magic number detection)
|
|
167
|
+
const contentType = "application/octet-stream";
|
|
168
|
+
|
|
169
|
+
// Register for HTTP download
|
|
170
|
+
context.fileTransfer.registerDownload(
|
|
171
|
+
hash,
|
|
172
|
+
value,
|
|
173
|
+
contentType,
|
|
174
|
+
context.clientId,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return { replace: hash };
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* B tag: Client → Server Buffer uploads
|
|
183
|
+
*
|
|
184
|
+
* When a client sends binary data with <!B> tag:
|
|
185
|
+
* 1. Server registers an upload expectation
|
|
186
|
+
* 2. Client uploads via HTTP PUT /api/ape/data/{queryId}/{hash}
|
|
187
|
+
* 3. onReceive resolves with the uploaded Buffer
|
|
188
|
+
*/
|
|
189
|
+
jss.custom("B", {
|
|
190
|
+
// Check: B tags come from client, we don't check server-side values
|
|
191
|
+
check: () => false,
|
|
192
|
+
|
|
193
|
+
// Encode: not used (B is client→server only)
|
|
194
|
+
encode: (path, key, value, context) => value,
|
|
195
|
+
|
|
196
|
+
// Decode: the hash value is decoded as-is, actual data set by onReceive
|
|
197
|
+
decode: (value, path, context) => value,
|
|
198
|
+
|
|
199
|
+
// onReceive: wait for the binary upload
|
|
200
|
+
onReceive: async (path, key, hash, context) => {
|
|
201
|
+
const uploadData = await context.fileTransfer.registerUpload(
|
|
202
|
+
context.queryId,
|
|
203
|
+
hash,
|
|
204
|
+
context.clientId,
|
|
205
|
+
);
|
|
206
|
+
return uploadData;
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A tag: Client → Server ArrayBuffer uploads
|
|
212
|
+
*
|
|
213
|
+
* Same as B tag but returns ArrayBuffer instead of Buffer.
|
|
214
|
+
*/
|
|
215
|
+
jss.custom("A", {
|
|
216
|
+
// Check: A tags come from client, we don't check server-side values
|
|
217
|
+
check: () => false,
|
|
218
|
+
|
|
219
|
+
// Encode: not used (A is client→server only)
|
|
220
|
+
encode: (path, key, value, context) => value,
|
|
221
|
+
|
|
222
|
+
// Decode: the hash value is decoded as-is, actual data set by onReceive
|
|
223
|
+
decode: (value, path, context) => value,
|
|
224
|
+
|
|
225
|
+
// onReceive: wait for the binary upload, convert to ArrayBuffer
|
|
226
|
+
onReceive: async (path, key, hash, context) => {
|
|
227
|
+
const uploadData = await context.fileTransfer.registerUpload(
|
|
228
|
+
context.queryId,
|
|
229
|
+
hash,
|
|
230
|
+
context.clientId,
|
|
231
|
+
);
|
|
232
|
+
// Convert Buffer to ArrayBuffer
|
|
233
|
+
return uploadData.buffer.slice(
|
|
234
|
+
uploadData.byteOffset,
|
|
235
|
+
uploadData.byteOffset + uploadData.byteLength,
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* F tag: Client → Client streaming file transfers
|
|
242
|
+
*
|
|
243
|
+
* Used for peer-to-peer file sharing between clients.
|
|
244
|
+
* The server acts as an intermediary, streaming data from
|
|
245
|
+
* sender to receiver without storing the entire file.
|
|
246
|
+
*/
|
|
247
|
+
jss.custom("F", {
|
|
248
|
+
// Check: F tags come from client, we don't check server-side values
|
|
249
|
+
check: () => false,
|
|
250
|
+
|
|
251
|
+
// Encode: pass through (F tags are managed by client)
|
|
252
|
+
encode: (path, key, value, context) => value,
|
|
253
|
+
|
|
254
|
+
// Decode: pass through
|
|
255
|
+
decode: (value, path, context) => value,
|
|
256
|
+
|
|
257
|
+
// onReceive: register streaming file expectation
|
|
258
|
+
onReceive: async (path, key, hash, context) => {
|
|
259
|
+
// Register the streaming file to receive uploads
|
|
260
|
+
context.fileTransfer.registerStreamingFile(hash, context.clientId);
|
|
261
|
+
// Return the hash - actual streaming happens via HTTP
|
|
262
|
+
return hash;
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Check if binary plugins are currently registered
|
|
269
|
+
*
|
|
270
|
+
* @returns {boolean} True if the L plugin is registered
|
|
271
|
+
*/
|
|
272
|
+
function areBinaryPluginsRegistered() {
|
|
273
|
+
const { hasPlugin } = require("../../utils/jss/plugins");
|
|
274
|
+
return hasPlugin("L");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
registerBinaryPlugins,
|
|
279
|
+
areBinaryPluginsRegistered,
|
|
280
|
+
isBinaryData,
|
|
281
|
+
INLINE_BASE64_THRESHOLD,
|
|
282
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Security Module
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This module provides security features to protect api-ape connections from common web vulnerabilities, including Cross-Site Request Forgery (CSRF) protection and duplicate request detection.
|
|
6
|
+
|
|
7
|
+
**Key capabilities:**
|
|
8
|
+
|
|
9
|
+
- **Origin validation** — Verify WebSocket connections originate from the same domain
|
|
10
|
+
- **CSRF protection** — Automatically reject cross-origin WebSocket requests
|
|
11
|
+
- **Replay protection** — Detect and reject duplicate requests within a time window
|
|
12
|
+
- **Domain extraction** — Flexible matching for subdomains and complex hostnames
|
|
13
|
+
|
|
14
|
+
Origin validation is **enabled by default** with no configuration required.
|
|
15
|
+
|
|
16
|
+
> **Contributing?** See [`files.md`](./files.md) for directory structure and file descriptions.
|
|
17
|
+
|
|
18
|
+
## How CSRF Protection Works
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
22
|
+
│ WebSocket Upgrade Request │
|
|
23
|
+
├─────────────────────────────────────────────────────────────┤
|
|
24
|
+
│ │
|
|
25
|
+
│ Headers: │
|
|
26
|
+
│ Host: example.com │
|
|
27
|
+
│ Origin: https://example.com │
|
|
28
|
+
│ │
|
|
29
|
+
│ origin.js checks: │
|
|
30
|
+
│ 1. Extract domain from Host header │
|
|
31
|
+
│ 2. Extract domain from Origin header │
|
|
32
|
+
│ 3. Compare root domains │
|
|
33
|
+
│ 4. Accept if match, reject if mismatch │
|
|
34
|
+
│ │
|
|
35
|
+
│ ✓ Same origin: example.com === example.com → ALLOWED │
|
|
36
|
+
│ ✗ Cross origin: evil.com !== example.com → REJECTED │
|
|
37
|
+
│ │
|
|
38
|
+
└─────────────────────────────────────────────────────────────┘
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Why Origin Validation Matters
|
|
42
|
+
|
|
43
|
+
Without origin validation, malicious websites could:
|
|
44
|
+
|
|
45
|
+
1. Open WebSocket connections to your api-ape server
|
|
46
|
+
2. Execute API calls using the victim's session cookies
|
|
47
|
+
3. Access or modify data on behalf of authenticated users
|
|
48
|
+
|
|
49
|
+
Origin validation ensures only your own frontend can establish WebSocket connections.
|
|
50
|
+
|
|
51
|
+
## Authentication
|
|
52
|
+
|
|
53
|
+
The security module includes a complete authentication system with:
|
|
54
|
+
|
|
55
|
+
- **OPAQUE/PAKE authentication** — Password-authenticated key exchange (server never sees raw password)
|
|
56
|
+
- **Tiered security model** — Guest → Basic → Elevated → High Security
|
|
57
|
+
- **State machine enforcement** — No-downgrade rule, timeout handling, rate limiting
|
|
58
|
+
- **Authorization middleware** — Per-endpoint tier and permission checks
|
|
59
|
+
- **Adapter pattern** — Pluggable authentication methods
|
|
60
|
+
|
|
61
|
+
See [`auth/README.md`](auth/README.md) for full documentation.
|
|
62
|
+
|
|
63
|
+
### Quick Example
|
|
64
|
+
|
|
65
|
+
```js
|
|
66
|
+
const { createAuthFramework } = require('api-ape/server/security/auth');
|
|
67
|
+
const { createAuthMiddleware } = require('api-ape/server/socket/authMiddleware');
|
|
68
|
+
|
|
69
|
+
const authFramework = createAuthFramework({
|
|
70
|
+
opaque: {
|
|
71
|
+
getUser: async (username) => db.users.findOne({ username }),
|
|
72
|
+
saveUser: async (username, data) => db.users.insertOne({ username, ...data })
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const authMiddleware = createAuthMiddleware({
|
|
77
|
+
requirements: {
|
|
78
|
+
'admin/*': { tier: 2 }, // Admin requires MFA
|
|
79
|
+
'user/*': { tier: 1 }, // User requires auth
|
|
80
|
+
'public/*': { tier: 0 } // Public allows guests
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
ape(server, { where: 'api', authFramework, authMiddleware });
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## See Also
|
|
88
|
+
|
|
89
|
+
- [`auth/README.md`](auth/README.md) — Authentication module documentation
|
|
90
|
+
- [`../socket/open.js`](../socket/open.js) — Connection open handler using security
|
|
91
|
+
- [`../lib/wiring.js`](../lib/wiring.js) — WebSocket connection setup
|
|
92
|
+
- [OWASP CSRF Prevention](https://owasp.org/www-community/attacks/csrf) — CSRF attack documentation
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Authentication Module
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This module provides a tiered authentication system for api-ape WebSocket connections. It supports OPAQUE-based password authentication where the server never learns raw passwords, with MFA (WebAuthn/TOTP) for elevated security and extensibility for enterprise SSO adapters.
|
|
6
|
+
|
|
7
|
+
**Key capabilities:**
|
|
8
|
+
|
|
9
|
+
- **OPAQUE/PAKE authentication** — Password-authenticated key exchange (server never sees raw password)
|
|
10
|
+
- **MFA support** — WebAuthn (FIDO2) and TOTP (RFC 6238) for Tier 2 elevation
|
|
11
|
+
- **Passport.js compatible** — MFA adapters work with existing Passport.js strategies
|
|
12
|
+
- **Tiered security model** — Guest → Basic → Elevated → High Security
|
|
13
|
+
- **State machine enforcement** — No-downgrade rule, timeout handling, rate limiting
|
|
14
|
+
- **Authorization middleware** — Per-endpoint tier and permission checks
|
|
15
|
+
- **Adapter pattern** — Pluggable authentication methods (OPAQUE, WebAuthn, TOTP, LDAP, SAML, OAuth2)
|
|
16
|
+
|
|
17
|
+
> **Contributing?** See the module files for implementation details.
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌────────────────────────────────────────────────────────────────────┐
|
|
23
|
+
│ AuthFramework │
|
|
24
|
+
│ ┌──────────────────────────────────────────────────────────────┐ │
|
|
25
|
+
│ │ Adapter Registry │ │
|
|
26
|
+
│ │ - OPAQUE (Tier 1) ✓ │ │
|
|
27
|
+
│ │ - WebAuthn (Tier 2 MFA) ✓ │ │
|
|
28
|
+
│ │ - TOTP (Tier 2 MFA) ✓ │ │
|
|
29
|
+
│ │ - LDAP, SAML, OAuth2 (Tier 1, planned) │ │
|
|
30
|
+
│ └──────────────────────────────────────────────────────────────┘ │
|
|
31
|
+
│ ┌──────────────────────────────────────────────────────────────┐ │
|
|
32
|
+
│ │ Per-Socket State Machines │ │
|
|
33
|
+
│ │ - Tracks auth state per clientId │ │
|
|
34
|
+
│ │ - Enforces tier requirements │ │
|
|
35
|
+
│ └──────────────────────────────────────────────────────────────┘ │
|
|
36
|
+
│ ┌──────────────────────────────────────────────────────────────┐ │
|
|
37
|
+
│ │ Message Router │ │
|
|
38
|
+
│ │ - Routes auth messages to appropriate adapter │ │
|
|
39
|
+
│ │ - Handles opaque_*, webauthn_*, totp_*, mfa_* messages │ │
|
|
40
|
+
│ └──────────────────────────────────────────────────────────────┘ │
|
|
41
|
+
└────────────────────────────────────────────────────────────────────┘
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Authentication Tiers
|
|
45
|
+
|
|
46
|
+
| Tier | Name | Description |
|
|
47
|
+
|------|------|-------------|
|
|
48
|
+
| 0 | GUEST | Unauthenticated, public endpoints only |
|
|
49
|
+
| 1 | BASIC | Identity verified via OPAQUE/SRP or enterprise SSO |
|
|
50
|
+
| 2 | ELEVATED | Tier 1 + MFA (WebAuthn or TOTP) |
|
|
51
|
+
| 3 | HIGH_SECURITY | Full 2-of-3 scheme for client-side key reconstruction |
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### 1. Create the Auth Framework
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const { createAuthFramework } = require('api-ape/server/security/auth');
|
|
59
|
+
const { createAuthMiddleware } = require('api-ape/server/socket/authMiddleware');
|
|
60
|
+
|
|
61
|
+
const authFramework = createAuthFramework({
|
|
62
|
+
opaque: {
|
|
63
|
+
// Provide your user storage functions
|
|
64
|
+
getUser: async (username) => db.users.findOne({ username }),
|
|
65
|
+
saveUser: async (username, data) => db.users.insertOne({ username, ...data })
|
|
66
|
+
},
|
|
67
|
+
webauthn: {
|
|
68
|
+
rpId: 'example.com',
|
|
69
|
+
rpName: 'My App',
|
|
70
|
+
// Optional: provide credential storage
|
|
71
|
+
getCredentials: async (userId) => db.webauthn.find({ userId }),
|
|
72
|
+
saveCredential: async (userId, credential) => db.webauthn.insertOne({ userId, ...credential })
|
|
73
|
+
},
|
|
74
|
+
totp: {
|
|
75
|
+
issuer: 'My App',
|
|
76
|
+
// Optional: provide secret storage
|
|
77
|
+
getSecret: async (userId) => db.totp.findOne({ userId }),
|
|
78
|
+
saveSecret: async (userId, data) => db.totp.upsertOne({ userId }, data)
|
|
79
|
+
},
|
|
80
|
+
mfaMethods: ['webauthn', 'totp'],
|
|
81
|
+
onAuthSuccess: (clientId, principal) => {
|
|
82
|
+
console.log(`${clientId} authenticated as ${principal.userId}`);
|
|
83
|
+
},
|
|
84
|
+
onMFASuccess: (clientId, principal, method) => {
|
|
85
|
+
console.log(`${clientId} elevated via ${method}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2. Configure Authorization Rules
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
const authMiddleware = createAuthMiddleware({
|
|
94
|
+
requirements: {
|
|
95
|
+
'admin/*': { tier: 2 }, // Admin endpoints require MFA
|
|
96
|
+
'user/*': { tier: 1 }, // User endpoints require auth
|
|
97
|
+
'public/*': { tier: 0 } // Public endpoints allow guests
|
|
98
|
+
},
|
|
99
|
+
defaultTier: 0
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 3. Pass to ape()
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
const { ape } = require('api-ape');
|
|
107
|
+
|
|
108
|
+
ape(server, {
|
|
109
|
+
where: 'api',
|
|
110
|
+
authFramework, // Enable authentication
|
|
111
|
+
authMiddleware // Enable authorization
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Message Protocol
|
|
116
|
+
|
|
117
|
+
### OPAQUE Registration (Tier 1)
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
Client Server
|
|
121
|
+
|-- opaque_reg_start ----------->| { user, clientNonce, regRequest }
|
|
122
|
+
|<- opaque_reg_response ---------| { serverNonce, ts, regResponse }
|
|
123
|
+
|-- opaque_reg_finish ---------->| { regRecord }
|
|
124
|
+
|<- opaque_reg_ok ---------------| { msg: "registered" }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### OPAQUE Authentication (Tier 1)
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
Client Server
|
|
131
|
+
|-- opaque_auth_start ---------->| { user, clientNonce }
|
|
132
|
+
|<- opaque_auth_1 ---------------| { serverNonce, ts, envelope, oprfResponse }
|
|
133
|
+
|-- opaque_auth_2 -------------->| { clientAuth }
|
|
134
|
+
|<- opaque_auth_ok --------------| { assignedPrincipal, serverProof, tier: 1 }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### WebAuthn Registration (MFA Setup)
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
Client Server
|
|
141
|
+
|-- webauthn_reg_start --------->| { userId, userName }
|
|
142
|
+
|<- webauthn_reg_challenge ------| { challenge, rp, user, pubKeyCredParams }
|
|
143
|
+
|-- webauthn_reg_finish -------->| { challenge, attestation }
|
|
144
|
+
|<- webauthn_reg_ok -------------| { credentialId }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### WebAuthn Authentication (Tier 2 Elevation)
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
Client Server
|
|
151
|
+
|-- webauthn_auth_start -------->| { userId }
|
|
152
|
+
|<- webauthn_auth_challenge -----| { challenge, allowCredentials }
|
|
153
|
+
|-- webauthn_auth_finish ------->| { challenge, assertion }
|
|
154
|
+
|<- webauthn_auth_ok ------------| { tier: 2 }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### TOTP Setup
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
Client Server
|
|
161
|
+
|-- totp_setup_start ----------->| { userId }
|
|
162
|
+
|<- totp_setup_challenge --------| { secret, otpauthUri }
|
|
163
|
+
|-- totp_setup_verify ---------->| { code }
|
|
164
|
+
|<- totp_setup_ok ---------------| { }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### TOTP Verification (Tier 2 Elevation)
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
Client Server
|
|
171
|
+
|-- totp_verify ---------------->| { userId, code }
|
|
172
|
+
|<- totp_ok --------------------| { tier: 2 }
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Generic MFA Challenge Flow
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
Client Server
|
|
179
|
+
|-- mfa_challenge -------------->| { }
|
|
180
|
+
|<- mfa_challenge ---------------| { methods: [{ method: "totp" }, { method: "webauthn", challenge: {...} }] }
|
|
181
|
+
|-- mfa_verify ----------------->| { method: "totp", code: "123456" }
|
|
182
|
+
|<- mfa_elevated ----------------| { method: "totp", tier: 2 }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Passport.js Compatibility
|
|
186
|
+
|
|
187
|
+
Both WebAuthn and TOTP adapters are compatible with Passport.js:
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
const passport = require('passport');
|
|
191
|
+
const { WebAuthnStrategy, TOTPStrategy } = require('api-ape/server/security/auth');
|
|
192
|
+
|
|
193
|
+
// Use with Passport.js
|
|
194
|
+
passport.use('webauthn', new WebAuthnStrategy({
|
|
195
|
+
rpId: 'example.com',
|
|
196
|
+
rpName: 'My App'
|
|
197
|
+
}, (user, done) => {
|
|
198
|
+
// Custom verification logic
|
|
199
|
+
done(null, user);
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
passport.use('totp', new TOTPStrategy({
|
|
203
|
+
issuer: 'My App'
|
|
204
|
+
}, (user, done) => {
|
|
205
|
+
// Custom verification logic
|
|
206
|
+
done(null, user);
|
|
207
|
+
}));
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Controller Context
|
|
211
|
+
|
|
212
|
+
After authentication, controllers have access to auth state via `this`:
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
// api/protected/data.js
|
|
216
|
+
module.exports = function(query) {
|
|
217
|
+
// Check authentication
|
|
218
|
+
if (!this.isAuthenticated) {
|
|
219
|
+
throw new Error('Authentication required');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Access user info
|
|
223
|
+
console.log('User:', this.principal.userId);
|
|
224
|
+
console.log('Roles:', this.principal.roles);
|
|
225
|
+
console.log('Tier:', this.authTier);
|
|
226
|
+
|
|
227
|
+
// Check tier requirement
|
|
228
|
+
if (!this.requiresTier(2)) {
|
|
229
|
+
throw new Error('MFA required for this operation');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { data: 'sensitive info' };
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Available Properties
|
|
237
|
+
|
|
238
|
+
| Property | Type | Description |
|
|
239
|
+
|----------|------|-------------|
|
|
240
|
+
| `this.isAuthenticated` | `boolean` | Whether socket is authenticated (Tier ≥ 1) |
|
|
241
|
+
| `this.authTier` | `number` | Current tier (0-3) |
|
|
242
|
+
| `this.principal` | `object\|null` | User info: `{ userId, roles, permissions }` |
|
|
243
|
+
| `this.authState` | `object\|null` | Full auth state object |
|
|
244
|
+
| `this.requiresTier(n)` | `function` | Check if socket meets minimum tier |
|
|
245
|
+
|
|
246
|
+
## Client Tracking
|
|
247
|
+
|
|
248
|
+
Query auth state for any connected client:
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
const client = this.clients.get(targetClientId);
|
|
252
|
+
|
|
253
|
+
if (client.isAuthenticated) {
|
|
254
|
+
console.log('User:', client.authState.principal.userId);
|
|
255
|
+
console.log('Tier:', client.authTier);
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## State Machine
|
|
260
|
+
|
|
261
|
+
```
|
|
262
|
+
GUEST → AUTHENTICATING → AUTHENTICATED (Tier 1)
|
|
263
|
+
│
|
|
264
|
+
┌───────────────┼───────────────┐
|
|
265
|
+
│ │ │
|
|
266
|
+
MFA_PENDING KEY_RECOVERY (stay Tier 1)
|
|
267
|
+
│ │
|
|
268
|
+
▼ ▼
|
|
269
|
+
ELEVATED (2) HIGH_SECURITY (3)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Rules:**
|
|
273
|
+
|
|
274
|
+
- No downgrade after authentication
|
|
275
|
+
- Higher tiers require completing lower tiers first
|
|
276
|
+
- Auth timeout closes incomplete sessions
|
|
277
|
+
- Rate limiting and lockout after failed attempts
|
|
278
|
+
|
|
279
|
+
## Security Features
|
|
280
|
+
|
|
281
|
+
| Feature | Description |
|
|
282
|
+
|---------|-------------|
|
|
283
|
+
| **Password protection** | OPAQUE ensures server never sees raw password |
|
|
284
|
+
| **Replay prevention** | Single-use nonces with 30s expiry |
|
|
285
|
+
| **No-downgrade** | Cannot return to lower tier after auth |
|
|
286
|
+
| **Rate limiting** | Lockout after configurable failed attempts |
|
|
287
|
+
| **Session binding** | Auth bound to `clientId + nonces + timestamp` |
|
|
288
|
+
| **TOTP replay protection** | Tracks used counters to prevent code reuse |
|
|
289
|
+
| **WebAuthn counter** | Validates and updates authenticator counter |
|
|
290
|
+
|
|
291
|
+
## File Structure
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
auth/
|
|
295
|
+
├── index.js # Auth framework coordinator
|
|
296
|
+
├── index.test.js # Integration tests (23 tests)
|
|
297
|
+
├── state-machine.js # State transitions, tier management
|
|
298
|
+
├── state-machine.test.js # State machine tests (31 tests)
|
|
299
|
+
├── nonce-manager.js # Single-use nonce handling
|
|
300
|
+
├── adapters/
|
|
301
|
+
│ ├── opaque.js # OPAQUE/SRP implementation
|
|
302
|
+
│ ├── opaque.test.js # OPAQUE tests (12 tests)
|
|
303
|
+
│ ├── webauthn.js # WebAuthn/FIDO2 adapter (Passport.js compatible)
|
|
304
|
+
│ ├── webauthn.test.js # WebAuthn tests (25 tests)
|
|
305
|
+
│ ├── totp.js # TOTP RFC 6238 adapter (Passport.js compatible)
|
|
306
|
+
│ └── totp.test.js # TOTP tests (35 tests)
|
|
307
|
+
└── handlers/
|
|
308
|
+
└── auth-messages.js # Message routing
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Total: 114 tests passing**
|
|
312
|
+
|
|
313
|
+
## See Also
|
|
314
|
+
|
|
315
|
+
- [`../README.md`](../README.md) — Security module overview
|
|
316
|
+
- [`../../socket/authMiddleware.js`](../../socket/authMiddleware.js) — Authorization middleware
|
|
317
|
+
- [`../../socket/receiveContext.js`](../../socket/receiveContext.js) — Controller context with auth
|
|
318
|
+
- [`../../../todo/Security.md`](../../../todo/Security.md) — Security architecture design
|
|
319
|
+
- [`../../../todo/implementation-checklist.md`](../../../todo/implementation-checklist.md) — Implementation progress
|