@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 CHANGED
@@ -1,32 +1,54 @@
1
1
  # @devorash/node
2
2
 
3
- Recording, masking and activity preferences are configured in Devora Settings.
4
- SDK initialization overrides are ignored. New sessions retain the server's policy
5
- snapshot across exchange and resume. Developer privacy labels take effect only
6
- when selected in Settings; sensitive-field protection remains mandatory.
7
- See [migration details](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
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: "your-org-id",
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 { targetUser } = req.devoraContext!
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
- const token = await generateAuthToken(targetUser.id)
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
- debug: false, // Enable debug logging
139
- timestampTolerance: 300, // Max clock drift in seconds (default: 5 min)
140
- collectStats: false, // Collect request statistics
141
- logRequests: false, // Log all requests
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 built-in endpoints:
189
+ The SDK automatically provides two signed endpoints. Like every SDK route, they
190
+ answer unsigned requests with `401`.
149
191
 
150
- ### `/test` - Connection Test
192
+ ### `/health` - Health Check
151
193
 
152
- Used by the Devora platform to verify your integration is working.
194
+ Returns SDK health information and the registered endpoints. Devora's **Test
195
+ connection** check calls it.
153
196
 
154
197
  ```
155
- GET /devora/test
198
+ GET /devora/health
156
199
  ```
157
200
 
158
- ### `/health` - Health Check
201
+ ### `/test` - Connection Test
159
202
 
160
- Returns SDK health information and registered endpoints.
203
+ Returns the verified organization and key ID, for your own signed checks.
161
204
 
162
205
  ```
163
- GET /devora/health
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 route
171
- lookup and reject unsigned, altered, stale or replayed requests. See
172
- [SIGNING.md](../SIGNING.md) for the protocol and the replay-store contract.
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 = await createClient({ url: process.env.REDIS_URL }).connect()
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
- // Atomic insert-if-absent that lives until expiresAt (Unix ms).
188
- // Errors propagate: the SDK then fails closed with a 503.
189
- const result = await redis.set(`devora:replay:${namespace}:${requestId}`, "1", {
190
- NX: true,
191
- PXAT: expiresAt,
192
- })
193
- return result === "OK"
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 (if `collectStats: true`).
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
- ## License
319
+ ### `createImpersonationGuard(options)`
257
320
 
258
- MIT
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
- Mount Express SDK routes before application-wide body parsers. The router and
263
- middleware include a bounded JSON parser (1 MiB by default, configurable through
264
- `maxBodySize`); compressed request bodies are rejected. The browser-session
265
- handler uses a 4 KiB limit. A parser placed earlier can already have allocated the
266
- entire body, so its own limit must be at least as strict for these routes.
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, apply the same
269
- byte limit in the host's raw-body reader before parsing or constructing
270
- `AdapterRequest`. The generic handler's check on an already-parsed object cannot
271
- bound earlier allocations. Configure request-size and read-timeout limits at the
272
- HTTP server/reverse proxy as well; middleware cannot undo upstream buffering.
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 { devoraSDK, DEVORA_ENDPOINTS } from "@devorash/node";
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: "your-org-id",
14
- * });
15
- *
16
- * const routes = [
17
- * sdk.register(DEVORA_ENDPOINTS.USER_SEARCH, async (req) => {
18
- * const { term } = req.query;
19
- * return { users: await searchUsers(term) };
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
- * ];
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;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"}
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 { devoraSDK, DEVORA_ENDPOINTS } from "@devorash/node";
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: "your-org-id",
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
- * const routes = [
17
- * sdk.register(DEVORA_ENDPOINTS.USER_SEARCH, async (req) => {
18
- * const { term } = req.query;
19
- * return { users: await searchUsers(term) };
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;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"}
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.0",
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.0"
45
+ "@devorash/core": "0.1.1"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=20.0.0"