@devorash/node 0.1.0 → 0.1.1
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 +120 -51
- package/dist/index.d.ts +37 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -18
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,32 +1,54 @@
|
|
|
1
1
|
# @devorash/node
|
|
2
2
|
|
|
3
|
-
Recording, masking and
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
See [
|
|
3
|
+
Recording, masking, capture and scope policy are configured in the Devora
|
|
4
|
+
dashboard and authorized server-side for each session. The backend SDK accepts
|
|
5
|
+
no settings for them: `devoraSDK` ignores any option not listed under
|
|
6
|
+
[Configuration](#configuration).
|
|
7
|
+
See [SETTINGS.md](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
|
|
8
8
|
|
|
9
9
|
Devora Backend SDK for Node.js applications. Enables secure impersonation by providing endpoints that the Devora platform can communicate with.
|
|
10
10
|
|
|
11
|
+
Requires Node.js 20 or later. Use it through a framework adapter:
|
|
12
|
+
[`@devorash/express`](https://www.npmjs.com/package/@devorash/express),
|
|
13
|
+
[`@devorash/fastify`](https://www.npmjs.com/package/@devorash/fastify) or
|
|
14
|
+
[`@devorash/hono`](https://www.npmjs.com/package/@devorash/hono).
|
|
15
|
+
|
|
11
16
|
## Installation
|
|
12
17
|
|
|
13
18
|
```bash
|
|
14
|
-
npm install @devorash/node
|
|
19
|
+
npm install @devorash/node redis
|
|
15
20
|
# or
|
|
16
|
-
bun add @devorash/node
|
|
21
|
+
bun add @devorash/node redis
|
|
17
22
|
```
|
|
18
23
|
|
|
19
24
|
## Quick Start
|
|
20
25
|
|
|
21
26
|
```typescript
|
|
27
|
+
import { createClient } from "redis"
|
|
22
28
|
import { devoraSDK, DEVORA_ENDPOINTS } from "@devorash/node"
|
|
23
29
|
|
|
30
|
+
// Replay protection shared by every instance of your backend (required in production).
|
|
31
|
+
const redis = createClient({ url: process.env.REDIS_URL }).on("error", (err) => console.error("Redis error", err))
|
|
32
|
+
let redisReady: Promise<unknown> | undefined // one connection, shared by concurrent first requests
|
|
33
|
+
|
|
24
34
|
// Initialize SDK with your server key ID and secret key
|
|
25
35
|
const sdk = devoraSDK({
|
|
26
|
-
apiKey: process.env.DEVORA_API_KEY!,
|
|
27
|
-
secretKey: process.env.DEVORA_SECRET_KEY!,
|
|
28
|
-
orgId:
|
|
36
|
+
apiKey: process.env.DEVORA_API_KEY!, // pk_server_live_...
|
|
37
|
+
secretKey: process.env.DEVORA_SECRET_KEY!, // sk_server_live_...
|
|
38
|
+
orgId: process.env.DEVORA_ORG_ID!,
|
|
29
39
|
debug: process.env.NODE_ENV === "development",
|
|
40
|
+
replayStore: {
|
|
41
|
+
async consume(namespace, requestId, expiresAt) {
|
|
42
|
+
await (redisReady ??= redis.connect().catch((err) => {
|
|
43
|
+
redisReady = undefined // retry on the next request
|
|
44
|
+
throw err
|
|
45
|
+
}))
|
|
46
|
+
const key = `devora:replay:${namespace}:${requestId}`
|
|
47
|
+
// Atomic insert-if-absent kept until expiresAt; an error makes the SDK fail closed (503).
|
|
48
|
+
const reply = await redis.sendCommand<string | null>(["SET", key, "1", "NX", "PXAT", String(expiresAt)])
|
|
49
|
+
return reply === "OK"
|
|
50
|
+
},
|
|
51
|
+
},
|
|
30
52
|
})
|
|
31
53
|
|
|
32
54
|
// User search endpoint
|
|
@@ -45,10 +67,12 @@ sdk.register(DEVORA_ENDPOINTS.USER_SEARCH, async (req) => {
|
|
|
45
67
|
// Impersonation start endpoint
|
|
46
68
|
sdk.register(DEVORA_ENDPOINTS.IMPERSONATE, async (req) => {
|
|
47
69
|
const { id } = req.params
|
|
48
|
-
const
|
|
70
|
+
const ctx = req.devoraContext
|
|
71
|
+
if (!ctx) throw new Error("Missing verified Devora context")
|
|
49
72
|
|
|
50
|
-
// Generate a short-lived app token for the target user.
|
|
51
|
-
|
|
73
|
+
// Generate a short-lived app token for the target user. Store ctx unchanged in
|
|
74
|
+
// it: createImpersonationGuard reads every field back. Expire it by ctx.expiresAt.
|
|
75
|
+
const token = await generateAuthToken(ctx.targetUser.id, ctx)
|
|
52
76
|
|
|
53
77
|
// Optionally include user settings for frontend initialization.
|
|
54
78
|
const userSettings = await getUserSettings(id)
|
|
@@ -75,6 +99,19 @@ sdk.register(DEVORA_ENDPOINTS.TERMINATE, async (req) => {
|
|
|
75
99
|
export { sdk }
|
|
76
100
|
```
|
|
77
101
|
|
|
102
|
+
The environment comes from the `environment` option, else `DEVORA_ENV`, else
|
|
103
|
+
`NODE_ENV`. Anything other than `development` or `test` (including unset) is
|
|
104
|
+
production, and production refuses to start without a `replayStore`; see
|
|
105
|
+
[Security](#security).
|
|
106
|
+
|
|
107
|
+
**Local development only:** to run without Redis, omit `replayStore` and start
|
|
108
|
+
the process with `DEVORA_ENV=development` (or pass `environment: "development"`).
|
|
109
|
+
The SDK then keeps request ids in memory, which protects a single process only.
|
|
110
|
+
Never use this in production.
|
|
111
|
+
|
|
112
|
+
Register every handler before creating an adapter: adapters read the route list
|
|
113
|
+
once, when they are created.
|
|
114
|
+
|
|
78
115
|
## Framework Integration
|
|
79
116
|
|
|
80
117
|
Use the SDK with your preferred web framework:
|
|
@@ -133,43 +170,64 @@ const sdk = devoraSDK({
|
|
|
133
170
|
apiKey: "pk_server_live_xxx", // Your public server key ID
|
|
134
171
|
secretKey: "sk_server_live_xxx", // Your server secret key
|
|
135
172
|
orgId: "org_xxx", // Your organization ID
|
|
173
|
+
replayStore, // Required in production (see Security)
|
|
136
174
|
|
|
137
175
|
// Optional
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
logger: customLogger, // Custom logger implementation
|
|
176
|
+
environment: "production", // "development" | "test" | "production"; defaults from DEVORA_ENV, then NODE_ENV, else production
|
|
177
|
+
debug: false, // SDK debug logging (default: on when NODE_ENV=development)
|
|
178
|
+
timestampTolerance: 300, // Max clock drift in seconds, a non-negative integer (default: 300)
|
|
179
|
+
collectStats: false, // Per-endpoint counts in getStats() and counters in the /health response
|
|
143
180
|
})
|
|
144
181
|
```
|
|
145
182
|
|
|
183
|
+
`apiKey` must be `pk_server_live_` followed by 32 characters and `secretKey`
|
|
184
|
+
`sk_server_live_` followed by 64; `devoraSDK` throws on anything else. `apiUrl`
|
|
185
|
+
(an https origin) overrides the Devora API origin for self-hosted deployments.
|
|
186
|
+
|
|
146
187
|
## Built-in Endpoints
|
|
147
188
|
|
|
148
|
-
The SDK automatically provides two
|
|
189
|
+
The SDK automatically provides two signed endpoints. Like every SDK route, they
|
|
190
|
+
answer unsigned requests with `401`.
|
|
149
191
|
|
|
150
|
-
### `/
|
|
192
|
+
### `/health` - Health Check
|
|
151
193
|
|
|
152
|
-
|
|
194
|
+
Returns SDK health information and the registered endpoints. Devora's **Test
|
|
195
|
+
connection** check calls it.
|
|
153
196
|
|
|
154
197
|
```
|
|
155
|
-
GET /devora/
|
|
198
|
+
GET /devora/health
|
|
156
199
|
```
|
|
157
200
|
|
|
158
|
-
### `/
|
|
201
|
+
### `/test` - Connection Test
|
|
159
202
|
|
|
160
|
-
Returns
|
|
203
|
+
Returns the verified organization and key ID, for your own signed checks.
|
|
161
204
|
|
|
162
205
|
```
|
|
163
|
-
GET /devora/
|
|
206
|
+
GET /devora/test
|
|
164
207
|
```
|
|
165
208
|
|
|
209
|
+
## Responses and errors
|
|
210
|
+
|
|
211
|
+
A handler's return value is sent as `{ success: true, data, timestamp }` with
|
|
212
|
+
status 200. Failures are `{ success: false, error, errorCode, timestamp }`:
|
|
213
|
+
|
|
214
|
+
| Status | `errorCode` | Cause |
|
|
215
|
+
| ------ | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
216
|
+
| 401 | `INVALID_SIGNATURE_HEADERS`, `UNSUPPORTED_SIGNATURE_VERSION`, `ORG_MISMATCH`, `TIMESTAMP_EXPIRED`, `INVALID_SIGNATURE`, `REPLAYED_REQUEST` | Unsigned, altered, stale or replayed request, or a different key or org |
|
|
217
|
+
| 401 | `INVALID_IMPERSONATION_CONTEXT` | Incomplete or expired impersonation-start body |
|
|
218
|
+
| 400 | `INVALID_REQUEST_TARGET`, `INVALID_BODY` | Malformed path or query; body that is not strict JSON |
|
|
219
|
+
| 400 | `HANDLER_ERROR` | Your handler threw (its message is not sent) |
|
|
220
|
+
| 413 | `BODY_TOO_LARGE` | Body larger than `maxBodySize` |
|
|
221
|
+
| 415 | `UNSUPPORTED_CONTENT_ENCODING` | `Content-Encoding` other than `identity` |
|
|
222
|
+
| 503 | `REPLAY_STORE_UNAVAILABLE` | `replayStore.consume` threw or did not return a boolean |
|
|
223
|
+
|
|
166
224
|
## Security
|
|
167
225
|
|
|
168
226
|
Every request from Devora is signed with HMAC-SHA256 (request signing v3). The
|
|
169
227
|
signature covers the exact path, query and body bytes, the request's direction,
|
|
170
|
-
a timestamp and a single-use request id. The adapters verify it before any
|
|
171
|
-
|
|
172
|
-
[SIGNING.md](
|
|
228
|
+
a timestamp and a single-use request id. The adapters verify it before any
|
|
229
|
+
handler runs and reject unsigned, altered, stale or replayed requests. See
|
|
230
|
+
[SIGNING.md](https://github.com/getdevora/devora-sdks/blob/main/SIGNING.md) for the protocol and the replay-store contract.
|
|
173
231
|
|
|
174
232
|
Production deployments need a shared, atomic `replayStore`: every instance
|
|
175
233
|
must see every request id, and the insert must be atomic. The in-memory store
|
|
@@ -180,17 +238,19 @@ refuses to start without a `replayStore`.
|
|
|
180
238
|
import { createClient } from "redis"
|
|
181
239
|
import { devoraSDK, type ReplayStore } from "@devorash/node"
|
|
182
240
|
|
|
183
|
-
const redis =
|
|
241
|
+
const redis = createClient({ url: process.env.REDIS_URL }).on("error", (err) => console.error("Redis error", err))
|
|
242
|
+
let redisReady: Promise<unknown> | undefined // one connection, shared by concurrent first requests
|
|
184
243
|
|
|
185
244
|
const replayStore: ReplayStore = {
|
|
186
245
|
async consume(namespace, requestId, expiresAt) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
246
|
+
await (redisReady ??= redis.connect().catch((err) => {
|
|
247
|
+
redisReady = undefined // retry on the next request
|
|
248
|
+
throw err
|
|
249
|
+
}))
|
|
250
|
+
const key = `devora:replay:${namespace}:${requestId}`
|
|
251
|
+
// Atomic insert-if-absent kept until expiresAt; an error makes the SDK fail closed (503).
|
|
252
|
+
const reply = await redis.sendCommand<string | null>(["SET", key, "1", "NX", "PXAT", String(expiresAt)])
|
|
253
|
+
return reply === "OK"
|
|
194
254
|
},
|
|
195
255
|
}
|
|
196
256
|
|
|
@@ -232,11 +292,13 @@ sdk.register<
|
|
|
232
292
|
|
|
233
293
|
### `devoraSDK(config)`
|
|
234
294
|
|
|
235
|
-
Create a new SDK instance.
|
|
295
|
+
Create a new SDK instance. It starts loading the dashboard-managed scope policy
|
|
296
|
+
in the background (`sdk.ready` resolves when that first attempt finishes).
|
|
236
297
|
|
|
237
298
|
### `sdk.register(path, handler, options?)`
|
|
238
299
|
|
|
239
|
-
Register an endpoint handler.
|
|
300
|
+
Register an endpoint handler. The method is detected for `DEVORA_ENDPOINTS`
|
|
301
|
+
paths; `/test` and `/health` cannot be overridden.
|
|
240
302
|
|
|
241
303
|
### `sdk.getRoutes()`
|
|
242
304
|
|
|
@@ -244,7 +306,8 @@ Get all registered routes.
|
|
|
244
306
|
|
|
245
307
|
### `sdk.getStats()`
|
|
246
308
|
|
|
247
|
-
Get request statistics
|
|
309
|
+
Get request statistics. Totals are always counted; `requestsByEndpoint` is
|
|
310
|
+
filled only with `collectStats: true`.
|
|
248
311
|
|
|
249
312
|
### `sdk.verifyRequest({ method, path, query, body, headers }, options?)`
|
|
250
313
|
|
|
@@ -253,23 +316,25 @@ relative to the SDK mount and still percent-encoded, `query` is everything
|
|
|
253
316
|
after the first `?`, and `body` is a `Uint8Array`. The adapters call this for
|
|
254
317
|
you; use it only for a custom framework integration.
|
|
255
318
|
|
|
256
|
-
|
|
319
|
+
### `createImpersonationGuard(options)`
|
|
257
320
|
|
|
258
|
-
|
|
321
|
+
Backend scope enforcement for your application routes, using the policy managed
|
|
322
|
+
in the dashboard. See the
|
|
323
|
+
[`@devorash/node` reference](https://docs.devora.sh/reference/node#scope-guard).
|
|
259
324
|
|
|
260
325
|
## Request body limits
|
|
261
326
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
327
|
+
The Express router and middleware read the raw body themselves (1 MiB by
|
|
328
|
+
default, configurable through `maxBodySize`); compressed request bodies are
|
|
329
|
+
rejected. Mount them before application-wide body parsers: a body another
|
|
330
|
+
parser already consumed is rejected with `DEVORA_BODY_ALREADY_PARSED`. The
|
|
331
|
+
Express browser-session handler uses a 4 KiB limit.
|
|
267
332
|
|
|
268
|
-
For direct `processRequest`/`createGenericHandler` integrations,
|
|
269
|
-
byte limit in the host's raw-body
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
333
|
+
For direct `processRequest`/`createGenericHandler` integrations, pass the exact
|
|
334
|
+
body bytes as a `Uint8Array` and apply the byte limit in the host's raw-body
|
|
335
|
+
reader: the handler's own `maxBodySize` check runs only after the body is
|
|
336
|
+
buffered. Configure request-size and read-timeout limits at the HTTP
|
|
337
|
+
server/reverse proxy as well; middleware cannot undo upstream buffering.
|
|
273
338
|
|
|
274
339
|
The impersonation guard retains at most 1,000 liveness verdicts and 64 distinct
|
|
275
340
|
in-flight lookups per guard. Lookup waits end after five seconds. An unresponsive
|
|
@@ -277,3 +342,7 @@ custom transport keeps its slot until it settles, preventing unlimited
|
|
|
277
342
|
background requests; saturation follows the unavailable policy (deny by default).
|
|
278
343
|
TTL hits never extend a verdict, and invalid unavailable-policy values are
|
|
279
344
|
configuration errors.
|
|
345
|
+
|
|
346
|
+
## License
|
|
347
|
+
|
|
348
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -5,29 +5,47 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* ```typescript
|
|
8
|
-
* import {
|
|
8
|
+
* import { createClient } from "redis"
|
|
9
|
+
* import { devoraSDK, DEVORA_ENDPOINTS } from "@devorash/node"
|
|
10
|
+
*
|
|
11
|
+
* // Replay protection shared by every instance of your backend (required in production).
|
|
12
|
+
* const redis = createClient({ url: process.env.REDIS_URL }).on("error", (err) => console.error("Redis error", err))
|
|
13
|
+
* let redisReady: Promise<unknown> | undefined // one connection, shared by concurrent first requests
|
|
9
14
|
*
|
|
10
15
|
* const sdk = devoraSDK({
|
|
11
16
|
* apiKey: process.env.DEVORA_API_KEY!,
|
|
12
17
|
* secretKey: process.env.DEVORA_SECRET_KEY!,
|
|
13
|
-
* orgId:
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
18
|
+
* orgId: process.env.DEVORA_ORG_ID!,
|
|
19
|
+
* replayStore: {
|
|
20
|
+
* async consume(namespace, requestId, expiresAt) {
|
|
21
|
+
* await (redisReady ??= redis.connect().catch((err) => {
|
|
22
|
+
* redisReady = undefined // retry on the next request
|
|
23
|
+
* throw err
|
|
24
|
+
* }))
|
|
25
|
+
* const key = `devora:replay:${namespace}:${requestId}`
|
|
26
|
+
* // Atomic insert-if-absent kept until expiresAt; an error makes the SDK fail closed (503).
|
|
27
|
+
* const reply = await redis.sendCommand<string | null>(["SET", key, "1", "NX", "PXAT", String(expiresAt)])
|
|
28
|
+
* return reply === "OK"
|
|
29
|
+
* },
|
|
30
|
+
* },
|
|
31
|
+
* })
|
|
32
|
+
*
|
|
33
|
+
* sdk.register(DEVORA_ENDPOINTS.USER_SEARCH, async (req) => ({
|
|
34
|
+
* users: await searchUsers(String(req.query.term ?? "")),
|
|
35
|
+
* }))
|
|
36
|
+
*
|
|
37
|
+
* sdk.register(DEVORA_ENDPOINTS.IMPERSONATE, async (req) => {
|
|
38
|
+
* const ctx = req.devoraContext // set only after the signed request is verified
|
|
39
|
+
* if (!ctx) throw new Error("Missing verified Devora context")
|
|
40
|
+
* // Store ctx unchanged in the credential: createImpersonationGuard reads every
|
|
41
|
+
* // field of it back. Expire the credential no later than ctx.expiresAt.
|
|
42
|
+
* return { token: await generateToken(ctx.targetUser.id, ctx) }
|
|
43
|
+
* })
|
|
44
|
+
*
|
|
45
|
+
* sdk.register(DEVORA_ENDPOINTS.TERMINATE, async (req) => {
|
|
46
|
+
* await invalidateSession(req.sessionId ?? req.params.id)
|
|
47
|
+
* return { success: true }
|
|
48
|
+
* })
|
|
31
49
|
* ```
|
|
32
50
|
*
|
|
33
51
|
* @packageDocumentation
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAGH,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAGxD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EACN,iBAAiB,EACjB,qBAAqB,EACrB,yBAAyB,EACzB,0BAA0B,GAC1B,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EACN,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,KAAK,cAAc,GACnB,MAAM,cAAc,CAAA;AAGrB,OAAO,EACN,wBAAwB,EACxB,4BAA4B,EAC5B,8BAA8B,EAC9B,gBAAgB,GAChB,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACX,oBAAoB,EACpB,0BAA0B,EAC1B,yBAAyB,EACzB,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,EACd,aAAa,GACb,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAA;AAC5D,YAAY,EAAE,WAAW,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAA;AAG/E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAGpD,YAAY,EACX,oBAAoB,EACpB,MAAM,EACN,QAAQ,EACR,gBAAgB,EAChB,eAAe,EACf,QAAQ,EACR,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,GACjB,MAAM,YAAY,CAAA;AAGnB,OAAO,EAEN,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EAEnB,gBAAgB,EAEhB,YAAY,EACZ,WAAW,EAEX,WAAW,EAEX,cAAc,EACd,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EAEjB,SAAS,EACT,qBAAqB,EACrB,mBAAmB,GACnB,MAAM,gBAAgB,CAAA;AAGvB,YAAY,EACX,aAAa,EACb,cAAc,EACd,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,0BAA0B,EAC1B,yBAAyB,EACzB,0BAA0B,EAC1B,6BAA6B,EAC7B,8BAA8B,EAC9B,YAAY,EACZ,eAAe,EACf,sBAAsB,EACtB,mBAAmB,GACnB,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EACN,qBAAqB,EACrB,gCAAgC,EAChC,KAAK,0BAA0B,GAC/B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AACvD,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -5,29 +5,47 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @example
|
|
7
7
|
* ```typescript
|
|
8
|
-
* import {
|
|
8
|
+
* import { createClient } from "redis"
|
|
9
|
+
* import { devoraSDK, DEVORA_ENDPOINTS } from "@devorash/node"
|
|
10
|
+
*
|
|
11
|
+
* // Replay protection shared by every instance of your backend (required in production).
|
|
12
|
+
* const redis = createClient({ url: process.env.REDIS_URL }).on("error", (err) => console.error("Redis error", err))
|
|
13
|
+
* let redisReady: Promise<unknown> | undefined // one connection, shared by concurrent first requests
|
|
9
14
|
*
|
|
10
15
|
* const sdk = devoraSDK({
|
|
11
16
|
* apiKey: process.env.DEVORA_API_KEY!,
|
|
12
17
|
* secretKey: process.env.DEVORA_SECRET_KEY!,
|
|
13
|
-
* orgId:
|
|
14
|
-
*
|
|
18
|
+
* orgId: process.env.DEVORA_ORG_ID!,
|
|
19
|
+
* replayStore: {
|
|
20
|
+
* async consume(namespace, requestId, expiresAt) {
|
|
21
|
+
* await (redisReady ??= redis.connect().catch((err) => {
|
|
22
|
+
* redisReady = undefined // retry on the next request
|
|
23
|
+
* throw err
|
|
24
|
+
* }))
|
|
25
|
+
* const key = `devora:replay:${namespace}:${requestId}`
|
|
26
|
+
* // Atomic insert-if-absent kept until expiresAt; an error makes the SDK fail closed (503).
|
|
27
|
+
* const reply = await redis.sendCommand<string | null>(["SET", key, "1", "NX", "PXAT", String(expiresAt)])
|
|
28
|
+
* return reply === "OK"
|
|
29
|
+
* },
|
|
30
|
+
* },
|
|
31
|
+
* })
|
|
32
|
+
*
|
|
33
|
+
* sdk.register(DEVORA_ENDPOINTS.USER_SEARCH, async (req) => ({
|
|
34
|
+
* users: await searchUsers(String(req.query.term ?? "")),
|
|
35
|
+
* }))
|
|
36
|
+
*
|
|
37
|
+
* sdk.register(DEVORA_ENDPOINTS.IMPERSONATE, async (req) => {
|
|
38
|
+
* const ctx = req.devoraContext // set only after the signed request is verified
|
|
39
|
+
* if (!ctx) throw new Error("Missing verified Devora context")
|
|
40
|
+
* // Store ctx unchanged in the credential: createImpersonationGuard reads every
|
|
41
|
+
* // field of it back. Expire the credential no later than ctx.expiresAt.
|
|
42
|
+
* return { token: await generateToken(ctx.targetUser.id, ctx) }
|
|
43
|
+
* })
|
|
15
44
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* }),
|
|
21
|
-
* sdk.register(DEVORA_ENDPOINTS.IMPERSONATE, async (req) => {
|
|
22
|
-
* const { id } = req.params;
|
|
23
|
-
* return { token: await generateToken(id), data: {} };
|
|
24
|
-
* }),
|
|
25
|
-
* sdk.register(DEVORA_ENDPOINTS.TERMINATE, async (req) => {
|
|
26
|
-
* const { id } = req.params;
|
|
27
|
-
* await invalidateSession(id);
|
|
28
|
-
* return { success: true };
|
|
29
|
-
* }),
|
|
30
|
-
* ];
|
|
45
|
+
* sdk.register(DEVORA_ENDPOINTS.TERMINATE, async (req) => {
|
|
46
|
+
* await invalidateSession(req.sessionId ?? req.params.id)
|
|
47
|
+
* return { success: true }
|
|
48
|
+
* })
|
|
31
49
|
* ```
|
|
32
50
|
*
|
|
33
51
|
* @packageDocumentation
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,WAAW;AACX,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAExD,iBAAiB;AACjB,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EACN,iBAAiB,EACjB,qBAAqB,EACrB,yBAAyB,EACzB,0BAA0B,GAC1B,MAAM,gBAAgB,CAAA;AAEvB,kBAAkB;AAClB,OAAO,EACN,cAAc,EACd,oBAAoB,EACpB,cAAc,EACd,qBAAqB,GAErB,MAAM,cAAc,CAAA;AAErB,2BAA2B;AAC3B,OAAO,EACN,wBAAwB,EACxB,4BAA4B,EAC5B,8BAA8B,EAC9B,gBAAgB,GAChB,MAAM,iBAAiB,CAAA;AAWxB,sBAAsB;AACtB,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAA;AAG5D,+BAA+B;AAC/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAkBvD,0CAA0C;AAC1C,OAAO;AACN,YAAY;AACZ,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB;AACnB,mBAAmB;AACnB,gBAAgB;AAChB,WAAW;AACX,YAAY,EACZ,WAAW;AACX,cAAc;AACd,WAAW;AACX,SAAS;AACT,cAAc,EACd,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB;AACjB,YAAY;AACZ,SAAS,EACT,qBAAqB,EACrB,mBAAmB,GACnB,MAAM,gBAAgB,CAAA;AAsBvB,uCAAuC;AACvC,OAAO,EACN,qBAAqB,EACrB,gCAAgC,GAEhC,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devorash/node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Devora Node.js Backend SDK for secure impersonation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"backend",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"provenance": true
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@devorash/core": "0.1.
|
|
45
|
+
"@devorash/core": "0.1.1"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=20.0.0"
|