@dropby/server 0.4.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/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/index.d.ts +882 -0
- package/dist/index.js +343 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simpllyf Software
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# `@dropby/server`
|
|
2
|
+
|
|
3
|
+
Server-side client for calling DropBy with your product's secret key. Use it from
|
|
4
|
+
your authenticated backend to mint browser sessions and to operate on chat,
|
|
5
|
+
ideas, asks, and evaluated configuration.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @dropby/server
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It has no dependencies and uses only the standard `fetch` and Web Crypto APIs.
|
|
14
|
+
On Node.js it needs version 22.12 or newer.
|
|
15
|
+
|
|
16
|
+
## Keys
|
|
17
|
+
|
|
18
|
+
DropBy gives each product and environment three kinds of credential, all from
|
|
19
|
+
the dashboard's API keys page:
|
|
20
|
+
|
|
21
|
+
- **The secret key** (`sk_live_...` or `sk_test_...`) is for your backend only:
|
|
22
|
+
never ship it to a browser or a mobile app. It can do what you allowed when
|
|
23
|
+
you created it, such as `browser_session:create` to create browser sessions or
|
|
24
|
+
`config:read` to evaluate flags. Revoking it also ends the browser sessions it
|
|
25
|
+
created: open pages using them get `forbidden` until they reload, so switch
|
|
26
|
+
your backend to a new key before you revoke the old one.
|
|
27
|
+
- **The public key** (`pk_live_...` or `pk_test_...`) is for the browser. It names
|
|
28
|
+
the product, the environment and the allowed origins, and grants nothing on
|
|
29
|
+
its own.
|
|
30
|
+
- **The identity secret** belongs to one browser key and signs identity proofs
|
|
31
|
+
on your backend (below). Keep it there too; the proof it signs is readable in
|
|
32
|
+
the browser, so it carries no secrets.
|
|
33
|
+
|
|
34
|
+
## Mint a browser session
|
|
35
|
+
|
|
36
|
+
Derive the user and account from the host application's authenticated session,
|
|
37
|
+
then create the DropBy session on the server. Do not copy a secret key to the
|
|
38
|
+
browser, and do not let the browser choose the identity sent to this method.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { createDropByServer } from "@dropby/server";
|
|
42
|
+
|
|
43
|
+
type HostSession = {
|
|
44
|
+
user: { id: string; email?: string; name?: string };
|
|
45
|
+
account?: { id: string; name?: string };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Implement this with the host application's session or request authentication.
|
|
49
|
+
declare function authenticate(request: Request): Promise<HostSession | null>;
|
|
50
|
+
|
|
51
|
+
export async function createDropBySessionResponse(request: Request): Promise<Response> {
|
|
52
|
+
const noStore = (body: string, status: number, extraHeaders: Record<string, string> = {}) =>
|
|
53
|
+
new Response(body, {
|
|
54
|
+
status,
|
|
55
|
+
headers: { "cache-control": "no-store", ...extraHeaders },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (request.method !== "POST") {
|
|
59
|
+
return noStore("Method Not Allowed", 405, { allow: "POST" });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const host = await authenticate(request);
|
|
64
|
+
if (!host) return noStore("Unauthorized", 401);
|
|
65
|
+
|
|
66
|
+
const secretKey = process.env.DROPBY_SECRET_KEY;
|
|
67
|
+
if (!secretKey) return noStore("DropBy is not configured", 500);
|
|
68
|
+
|
|
69
|
+
const dropby = createDropByServer({ secretKey });
|
|
70
|
+
|
|
71
|
+
const { session } = await dropby.browserSessions.create({
|
|
72
|
+
user: host.user,
|
|
73
|
+
...(host.account ? { account: host.account } : {}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// `session` is `{ sessionToken, expiresAt }`, what the browser's `auth` returns.
|
|
77
|
+
return Response.json(session, { headers: { "cache-control": "no-store" } });
|
|
78
|
+
} catch {
|
|
79
|
+
return noStore("DropBy session creation failed", 500);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The endpoint is POST-only, returns `Allow: POST` with a 405 for other methods,
|
|
85
|
+
and sets `Cache-Control: no-store` on success and every explicit error response.
|
|
86
|
+
|
|
87
|
+
Omit `account` for a user-only product: the session then has no account.
|
|
88
|
+
Everything the user does without one stays theirs under any account they later
|
|
89
|
+
belong to. Deliver the session only over your authenticated application
|
|
90
|
+
response.
|
|
91
|
+
|
|
92
|
+
The client also exposes `ideas`, `asks`, `chat`, and `config` for server-side
|
|
93
|
+
workflows. Ids are plain strings: one in a URL path is checked before anything is
|
|
94
|
+
sent (`invalid_input`), and one in a request body is checked by the API (a 400
|
|
95
|
+
naming the field).
|
|
96
|
+
|
|
97
|
+
## Feature flags
|
|
98
|
+
|
|
99
|
+
`config.evaluate` resolves every flag for a user and account, as the browser
|
|
100
|
+
does for its session:
|
|
101
|
+
|
|
102
|
+
<!-- snippet: import type { DropByServer } from "@dropby/server"; declare const dropby: DropByServer; -->
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const { config } = await dropby.config.evaluate({
|
|
106
|
+
user: { id: "user_123", plan: "pro" },
|
|
107
|
+
account: { id: "acct_42" },
|
|
108
|
+
});
|
|
109
|
+
if (config.new_exports === true) {
|
|
110
|
+
// show the new exports
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It takes no `context` or `device`, so rules on them see those fields as missing:
|
|
115
|
+
a condition that needs a value (`eq`, `in`, `exists`) never matches, and a
|
|
116
|
+
negative one (`neq`, or `not` around another) always does. The server and the
|
|
117
|
+
browser can therefore disagree for the same user. It needs a secret key with
|
|
118
|
+
`config:read`.
|
|
119
|
+
|
|
120
|
+
## Retries
|
|
121
|
+
|
|
122
|
+
Every call retries by itself after a network error, a timeout, a 408, a 429 or
|
|
123
|
+
a 5xx: twice by default, waiting up to half a second, then up to a second (set
|
|
124
|
+
`maxRetries`, 0 to 10). A `Retry-After` of up to 10 seconds is waited out; a
|
|
125
|
+
longer one ends the call at once, and the error's `retryAfter` says how long the
|
|
126
|
+
API asked for. `timeoutMs` (30 seconds by default) bounds each attempt.
|
|
127
|
+
|
|
128
|
+
Retrying is safe for every call. Updates and merges change nothing when
|
|
129
|
+
repeated (though an update retried after a lost answer can overwrite a change
|
|
130
|
+
a teammate made in between; updates are last-writer-wins), and `asks.create`, `chat.create`, `chat.reply` and `ideas.create` send
|
|
131
|
+
an idempotency key, one per call, generated when you give none: a repeat returns
|
|
132
|
+
what was created instead of posting again. If your backend retries a whole call
|
|
133
|
+
itself, for example from a job queue, pass your own key so those attempts are
|
|
134
|
+
one too:
|
|
135
|
+
|
|
136
|
+
<!-- snippet: import type { DropByServer } from "@dropby/server"; declare const dropby: DropByServer; declare const threadId: string; declare const job: { replyId: string }; -->
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// One key per write, stored with the job that retries it.
|
|
140
|
+
await dropby.chat.reply(threadId, { text: "we're on it" }, { idempotencyKey: job.replyId });
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- A key is 1 to 128 visible ASCII characters; use a UUID. Reusing one for a
|
|
144
|
+
different request is a 409 `idempotency_key_reused`.
|
|
145
|
+
- An ask's and an idea's key are shared across your product and environment,
|
|
146
|
+
every secret key and the dashboard included. A reply's key is shared across
|
|
147
|
+
every secret key of the product and environment (the dashboard's replies keep
|
|
148
|
+
their own). A new thread's key is its first message's, scoped to the user and
|
|
149
|
+
account, and shared with that user's widget sends; the user's widget can see
|
|
150
|
+
it.
|
|
151
|
+
- A repeat returns what was created as it is now: an ask may since be closed.
|
|
152
|
+
An ask is compared by what it asks and whom, so compute its window once and
|
|
153
|
+
keep it with the job; a recomputed `expiresAt` is a different request.
|
|
154
|
+
- Keys never expire.
|
|
155
|
+
|
|
156
|
+
## Errors
|
|
157
|
+
|
|
158
|
+
Every error the client throws is a `DropByError` with a `code`, a `message`,
|
|
159
|
+
the HTTP `status` (undefined when no response arrived), the `retryAfter` seconds
|
|
160
|
+
when the API asked for a wait, and the underlying `cause`. An error that can pass
|
|
161
|
+
is thrown once the retries are spent; any other at once:
|
|
162
|
+
|
|
163
|
+
<!-- snippet: import type { DropByServer } from "@dropby/server"; declare const dropby: DropByServer; declare const threadId: string; declare const text: string; -->
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { DropByError } from "@dropby/server";
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await dropby.chat.reply(threadId, { text });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (error instanceof DropByError && error.code === "rate_limited") {
|
|
172
|
+
// error.retryAfter says how long the API asked to wait
|
|
173
|
+
}
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
API codes are `bad_request`, `unauthorized`, `forbidden`, `not_found`,
|
|
179
|
+
`conflict`, `idea_archived`, `idea_merged`, `idempotency_key_reused`,
|
|
180
|
+
`rate_limited`, `unavailable` and `internal_error`; the API may add more. The
|
|
181
|
+
client adds `invalid_input` (a bad argument or option, raised before anything
|
|
182
|
+
is sent), `network_error`, `timeout` (30 seconds per attempt by default;
|
|
183
|
+
set `timeoutMs`), `invalid_response` and `request_failed` (an error response without
|
|
184
|
+
a DropBy error body). No message includes your keys.
|
|
185
|
+
|
|
186
|
+
Responses are returned as the API sends them. New fields and values can appear
|
|
187
|
+
as the API grows, so treat enums such as a status as open and keep a default
|
|
188
|
+
branch when you switch over one.
|
|
189
|
+
|
|
190
|
+
## Identity proofs
|
|
191
|
+
|
|
192
|
+
`createIdentityProof` is an alternative server-side credential flow for a
|
|
193
|
+
browser session endpoint. Sign it locally with your browser key's identity
|
|
194
|
+
secret and return only the short-lived proof to the browser. Never use your
|
|
195
|
+
secret key for this, and never send the identity secret to the browser.
|
|
196
|
+
|
|
197
|
+
<!-- snippet: declare const user: { id: string; email?: string }; declare const account: { id: string; name?: string } | undefined; -->
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
import { createIdentityProof, identityProofOptionsFromEnv } from "@dropby/server";
|
|
201
|
+
|
|
202
|
+
// Reads DROPBY_PRODUCT_ID, DROPBY_PUBLIC_KEY_ID, DROPBY_ENV and
|
|
203
|
+
// DROPBY_IDENTITY_SECRET; throws at startup naming the first that is missing or invalid.
|
|
204
|
+
const dropby = identityProofOptionsFromEnv(process.env);
|
|
205
|
+
|
|
206
|
+
const identityProof = await createIdentityProof({
|
|
207
|
+
...dropby,
|
|
208
|
+
user: { id: user.id, email: user.email },
|
|
209
|
+
...(account ? { account: { id: account.id, name: account.name } } : {}),
|
|
210
|
+
});
|
|
211
|
+
// return { identityProof } from your authenticated session endpoint
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
On Cloudflare Workers, pass the request's `env` instead of `process.env`. Setting
|
|
215
|
+
the four variables per environment lets one codebase serve test and live.
|
|
216
|
+
|
|
217
|
+
Get these values from the dashboard: **settings → api keys**, then **reveal
|
|
218
|
+
identity secret** on the browser key's actions menu (or from the identity proof
|
|
219
|
+
option under "connect your app", whose server setup lists them). The dialog shows
|
|
220
|
+
`DROPBY_PRODUCT_ID`, `DROPBY_PUBLIC_KEY_ID`, `DROPBY_ENV` and
|
|
221
|
+
`DROPBY_IDENTITY_SECRET`. Only the secret is shown
|
|
222
|
+
once. If you lose it, create a new browser key, update your backend and browser
|
|
223
|
+
configuration, then revoke the old key.
|
|
224
|
+
|
|
225
|
+
The proof carries only the user's `id`, `name`, `email` and `attributes` and the
|
|
226
|
+
account's `id`, `name`, `domain` and `attributes`; other properties of the objects
|
|
227
|
+
you pass are left out, since the browser can read the proof. Proofs expire within
|
|
228
|
+
the supported short lifetime (900 seconds at most). The host backend remains the
|
|
229
|
+
source of identity truth; request-body fields from the browser are not an
|
|
230
|
+
authorization boundary.
|
|
231
|
+
|
|
232
|
+
`createIdentityProof` checks the secret, ids and lifetime before signing. The API
|
|
233
|
+
checks the rest (email, names, domain, attributes) when the browser exchanges
|
|
234
|
+
the proof: a proof that breaks one of those rules is answered with a 400 naming
|
|
235
|
+
the claim, such as `Invalid identity proof claims: user.email: …`.
|