@decentnetwork/beagle-service 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +720 -2
  3. package/dist/agent.d.ts +18 -0
  4. package/dist/agent.js +448 -0
  5. package/dist/auth.d.ts +28 -0
  6. package/dist/auth.js +340 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +163 -0
  9. package/dist/commonjs.cjs +49 -0
  10. package/dist/commonjs.d.cts +11 -0
  11. package/dist/index.d.ts +23 -0
  12. package/dist/index.js +191 -0
  13. package/dist/outbox.d.ts +63 -0
  14. package/dist/outbox.js +314 -0
  15. package/dist/routes.d.ts +29 -0
  16. package/dist/routes.js +454 -0
  17. package/dist/stores.d.ts +21 -0
  18. package/dist/stores.js +92 -0
  19. package/dist/types.d.ts +641 -0
  20. package/dist/types.js +1 -0
  21. package/dist/users.d.ts +16 -0
  22. package/dist/users.js +429 -0
  23. package/dist/util.d.ts +48 -0
  24. package/dist/util.js +208 -0
  25. package/examples/minimal-site/index.html +91 -0
  26. package/examples/minimal-site/server.mjs +64 -0
  27. package/examples/next-pages-cjs/README.md +30 -0
  28. package/examples/next-pages-cjs/instrumentation.js +16 -0
  29. package/examples/next-pages-cjs/lib/beagle.js +23 -0
  30. package/examples/next-pages-cjs/lib/peer-for-browser.mjs +2 -0
  31. package/examples/next-pages-cjs/next.config.js +11 -0
  32. package/examples/next-pages-cjs/package.json +19 -0
  33. package/examples/next-pages-cjs/pages/api/auth/[...nextauth].js +17 -0
  34. package/examples/next-pages-cjs/pages/api/auth/launch.js +3 -0
  35. package/examples/next-pages-cjs/pages/api/auth/logout.js +3 -0
  36. package/examples/next-pages-cjs/pages/api/auth/me.js +3 -0
  37. package/examples/next-pages-cjs/pages/api/auth/nonce.js +3 -0
  38. package/examples/next-pages-cjs/pages/api/auth/register.js +3 -0
  39. package/examples/next-pages-cjs/pages/api/auth/sign-in.js +3 -0
  40. package/examples/next-pages-cjs/pages/api/notes.js +16 -0
  41. package/examples/next-pages-cjs/pages/index.js +52 -0
  42. package/examples/sign-in-from-script.mjs +37 -0
  43. package/package.json +67 -4
@@ -0,0 +1,641 @@
1
+ import type { NetworkNode, Peer, PeerOptions } from "@decentnetwork/peer";
2
+ export type UserStatus = "visitor" | "registered" | "blocked";
3
+ /** One record per userid. A site may add fields of its own; every update
4
+ * merges into the stored record, so fields this library does not know stay. */
5
+ export interface UserRecord {
6
+ /** The identity: base58 of the X25519 public key that signed. */
7
+ userid: string;
8
+ /** Full Carrier address (key + nospam + checksum), needed to send a friend
9
+ * request. Not signed at sign-in: checked to parse and to name `userid`. */
10
+ address: string | null;
11
+ /** Display hints from the visitor's Beagle. Not signed. */
12
+ name: string | null;
13
+ avatar: string | null;
14
+ punkId: number | null;
15
+ /** Which Beagle answered: the popup's origin, or "launch" for an Apps-tab arrival. */
16
+ door: string | null;
17
+ /** The origin of the site's page the last sign-in was signed for. */
18
+ origin?: string | null;
19
+ status: UserStatus;
20
+ /** The site's registration fields (`register.required` / `register.optional`). */
21
+ profile: Record<string, unknown>;
22
+ /** ISO 8601. null for a userid that was blocked before it ever signed in. */
23
+ firstSeen: string | null;
24
+ lastSeen: string | null;
25
+ /** Bursts of activity, separated by `visitor.visitGapMinutes` of silence. */
26
+ visits: number;
27
+ /** Successful sign-ins and Apps-tab launches. */
28
+ signIns: number;
29
+ registeredAt?: string | null;
30
+ blocked?: {
31
+ at: string;
32
+ why: string;
33
+ was: "visitor" | "registered";
34
+ } | null;
35
+ /** ms since the epoch; a session issued before this moment is refused. */
36
+ sessionsValidFrom?: number;
37
+ /** What the userid published under a beagles.eth name (`importProfile`). */
38
+ published?: PublishedProfile | null;
39
+ [key: string]: unknown;
40
+ }
41
+ export interface PublishedProfile {
42
+ name?: string;
43
+ displayName?: string;
44
+ description?: string;
45
+ avatar?: string;
46
+ url?: string;
47
+ /** The record's owner. When it equals the userid, that key published it. */
48
+ owner?: string;
49
+ fetchedAt: string;
50
+ }
51
+ /** What one write changed, as the second argument of `UserStore.put`. */
52
+ export interface UserChange {
53
+ /** The record as it was read for this write; null when there was none. */
54
+ prev: UserRecord | null;
55
+ /** Top-level fields that were added or got a new value, with that value. */
56
+ set: Record<string, unknown>;
57
+ /** Top-level fields that were removed. */
58
+ unset: string[];
59
+ }
60
+ /** Where users are kept. Implement these three calls for Postgres, sqlite or
61
+ * anything else.
62
+ *
63
+ * `get` may return any object that names the userid; fields this library
64
+ * needs and does not find are filled in when it is read (a record that
65
+ * predates the library is fine).
66
+ *
67
+ * `put` receives the whole record, including fields the store does not
68
+ * recognise, and as a second argument what this write changed. A store that
69
+ * other code writes to as well should store only `change.set` and remove
70
+ * `change.unset`: then a field somebody else wrote since the read is not
71
+ * written back from an old copy. A store that ignores the second argument
72
+ * and stores the whole record (an adapter written for 0.1.0) keeps working.
73
+ *
74
+ * When `get` and `put` are synchronous, the library calls them in the same
75
+ * turn of the event loop with nothing in between. */
76
+ export interface UserStore {
77
+ get(userid: string): Promise<UserRecord | null> | UserRecord | null;
78
+ put(user: UserRecord, change?: UserChange): Promise<void> | void;
79
+ list(): Promise<UserRecord[]> | UserRecord[];
80
+ }
81
+ /** Small durable sets with expiry: used nonces, used launch assertions and
82
+ * revoked sessions. With several server processes, back this with a shared
83
+ * database; `addOnce` must then be atomic there (an insert that fails on a
84
+ * duplicate key). */
85
+ export interface StateStore {
86
+ /** Remember `key` in `ns` until `expiresAt` (ms). true when it was not
87
+ * there, or had expired; false when it was. */
88
+ addOnce(ns: string, key: string, expiresAt: number): Promise<boolean> | boolean;
89
+ has(ns: string, key: string): Promise<boolean> | boolean;
90
+ }
91
+ export interface RegisterConfig {
92
+ /** Fields a visitor must give to become a registered user. Empty (the
93
+ * default): every visitor is a registered user at once. */
94
+ required?: string[];
95
+ /** Fields the register route also accepts and stores. */
96
+ optional?: string[];
97
+ /** Confirm one submitted field. Return true to accept, false or a string
98
+ * (the reason, shown to the page) to refuse, or `{ value }` to store a
99
+ * normalised value. `submitted` is everything the page posted, so a site
100
+ * can read a confirmation code it asked for. Sending the email or the SMS
101
+ * is the site's part. */
102
+ verify?: (field: string, value: unknown, user: UserRecord, submitted: Record<string, unknown>) => boolean | string | {
103
+ value: unknown;
104
+ } | Promise<boolean | string | {
105
+ value: unknown;
106
+ }>;
107
+ }
108
+ export interface VisitorConfig {
109
+ /** Days after `firstSeen` a visitor may stay unregistered. */
110
+ maxDays?: number;
111
+ /** Visits a visitor may make unregistered. */
112
+ maxVisits?: number;
113
+ /** After the limit: "ask" keeps answering and marks registration as due;
114
+ * "refuse" answers 403 `register-required` until they register. Default "ask". */
115
+ then?: "ask" | "refuse";
116
+ /** Silence that separates two visits. Default 30. */
117
+ visitGapMinutes?: number;
118
+ }
119
+ /** What the page is told about registration. */
120
+ export interface RegistrationState {
121
+ status: UserStatus;
122
+ required: string[];
123
+ optional: string[];
124
+ /** Required fields the user has not given yet. */
125
+ missing: string[];
126
+ /** The visitor limit has passed and fields are missing. */
127
+ due: boolean;
128
+ /** `due`, and the site answers 403 until they register. */
129
+ refused: boolean;
130
+ /** When `visitor.maxDays` runs out (ISO 8601), if it is set. */
131
+ deadline?: string;
132
+ /** Visits left before `visitor.maxVisits` is reached, if it is set. */
133
+ visitsLeft?: number;
134
+ }
135
+ export type RegisterResult = {
136
+ ok: true;
137
+ user: UserRecord;
138
+ } | {
139
+ ok: false;
140
+ error: "invalid-fields";
141
+ fields: Record<string, string>;
142
+ } | {
143
+ ok: false;
144
+ error: "blocked" | "not-found";
145
+ };
146
+ export interface UsersEvents {
147
+ /** After every successful sign-in or launch. */
148
+ "sign-in": [user: UserRecord, info: {
149
+ first: boolean;
150
+ door: string | null;
151
+ origin: string | null;
152
+ }];
153
+ /** A visitor became a registered user. */
154
+ registered: [user: UserRecord];
155
+ blocked: [user: UserRecord];
156
+ unblocked: [user: UserRecord];
157
+ [event: string]: unknown[];
158
+ }
159
+ export interface Users {
160
+ get(userid: string): Promise<UserRecord | null>;
161
+ /** All users, or those with one status. */
162
+ list(filter?: {
163
+ status?: UserStatus;
164
+ }): Promise<UserRecord[]>;
165
+ /** Merge `patch` into the record (one level deep for `profile`). Fields
166
+ * the patch does not name are kept, known to this library or not. */
167
+ update(userid: string, patch: Partial<UserRecord>): Promise<UserRecord>;
168
+ /** Refuse this userid: sign-in answers 403 `blocked`, its sessions stop
169
+ * working, and the agent refuses and removes it as a friend. Works for a
170
+ * userid that has never signed in. */
171
+ block(userid: string, why?: string): Promise<UserRecord>;
172
+ unblock(userid: string): Promise<UserRecord | null>;
173
+ /** End every session of this user (they sign in again). */
174
+ revokeSessions(userid: string): Promise<void>;
175
+ /** Check and store registration fields; a visitor with every required
176
+ * field becomes `registered`. */
177
+ register(userid: string, submitted: Record<string, unknown>): Promise<RegisterResult>;
178
+ registration(user: UserRecord): RegistrationState;
179
+ /** Record a sign-in the site verified by a door of its own. The two built-in
180
+ * doors call this themselves. */
181
+ recordSignIn(userid: string, hints?: {
182
+ address?: unknown;
183
+ name?: unknown;
184
+ avatar?: unknown;
185
+ punkId?: unknown;
186
+ }, info?: {
187
+ door?: string | null;
188
+ origin?: string | null;
189
+ }): Promise<UserRecord>;
190
+ /** Note activity: `lastSeen`, and `visits` after a gap. Written at most
191
+ * once a minute per user. */
192
+ touch(userid: string): Promise<UserRecord | null>;
193
+ /** Read the userid's beagles.eth record and store it under `published`. */
194
+ importProfile(userid: string): Promise<PublishedProfile | null>;
195
+ on<K extends keyof UsersEvents>(event: K, cb: (...args: UsersEvents[K]) => unknown): () => void;
196
+ }
197
+ export interface SessionConfig {
198
+ /** Days until a session expires. Default 30. */
199
+ ttlDays?: number;
200
+ /** "cookie" (default): HttpOnly cookie. "bearer": the token is returned in
201
+ * the sign-in answer and sent back as `Authorization: Bearer`. "both". */
202
+ transport?: "cookie" | "bearer" | "both";
203
+ /** Default "beagle_session". */
204
+ cookieName?: string;
205
+ /** Default "Lax". */
206
+ sameSite?: "Lax" | "Strict" | "None";
207
+ cookieDomain?: string;
208
+ /** Default "/". */
209
+ cookiePath?: string;
210
+ /** `authenticate()` notes the user's activity (`lastSeen`, `visits`), which
211
+ * is one write to the users store per user per minute at most. false: it
212
+ * only reads. One call can say otherwise with `authenticate(req, { touch })`.
213
+ * Default true. */
214
+ touch?: boolean;
215
+ }
216
+ /** A limit on sign-in and launch attempts, counted in memory per process. */
217
+ export interface RateLimitConfig {
218
+ /** Attempts one client may make per window. Default 30. */
219
+ perClient?: number;
220
+ /** Attempts all clients together may make per window. Default 600. */
221
+ total?: number;
222
+ /** Default 60. */
223
+ windowSeconds?: number;
224
+ /** Read the client's address from `X-Forwarded-For`: true for one proxy in
225
+ * front of the server, or how many. Default false: the connection's own
226
+ * address, and a connection from this machine (a local proxy) counts
227
+ * against `total` only. */
228
+ trustProxy?: boolean | number;
229
+ /** Name the client yourself; null counts against `total` only. */
230
+ key?: (request: ServiceRequest) => string | null | undefined;
231
+ }
232
+ /** What `onSignIn` is told beside the user. */
233
+ export interface SignInInfo {
234
+ /** The origin of the site's page the assertion was signed for. */
235
+ origin: string;
236
+ /** Which Beagle answered: the popup's origin, or "launch". */
237
+ door: string | null;
238
+ /** Which route verified it. */
239
+ route: "sign-in" | "launch";
240
+ /** The first sign-in of this userid. */
241
+ first: boolean;
242
+ /** The request as the route got it (`raw` is the framework's own object,
243
+ * when an adapter made the request). */
244
+ request: ServiceRequest;
245
+ }
246
+ /** Refuse a sign-in from `onSignIn`: no session is issued. */
247
+ export interface SignInRefusal {
248
+ /** true, or the `error` code the page gets. Default "sign-in-refused". */
249
+ refuse: true | string;
250
+ /** Default 403. */
251
+ status?: number;
252
+ message?: string;
253
+ }
254
+ /** Runs after the assertion is verified and the user record is written, and
255
+ * before the session is issued and the answer goes out. Return an object and
256
+ * its fields are added to the sign-in answer (the library's own fields win);
257
+ * return `{ refuse }` to refuse. It is awaited; if it throws, the sign-in
258
+ * answers 500 and no session is issued. */
259
+ export type SignInHook = (user: UserRecord, info: SignInInfo) => void | Record<string, unknown> | SignInRefusal | Promise<void | Record<string, unknown> | SignInRefusal>;
260
+ export interface SessionInfo {
261
+ userid: string;
262
+ /** Session id; logout revokes this. */
263
+ sid: string;
264
+ issuedAt: number;
265
+ expiresAt: number;
266
+ /** How the token arrived. */
267
+ via: "cookie" | "bearer";
268
+ }
269
+ export type ErrorCode = "origin-not-allowed" | "bad-request" | "bad-nonce" | "bad-signature" | "launch-expired" | "launch-replayed" | "blocked" | "register-required" | "sign-in-refused" | "rate-limited" | "not-signed-in" | "invalid-fields" | "not-found" | "method-not-allowed" | "too-large" | "server-error";
270
+ export type VerifyResult = {
271
+ ok: true;
272
+ userid: string;
273
+ hints: {
274
+ address?: unknown;
275
+ name?: unknown;
276
+ avatar?: unknown;
277
+ punkId?: unknown;
278
+ };
279
+ door: string | null;
280
+ } | {
281
+ ok: false;
282
+ status: number;
283
+ error: ErrorCode;
284
+ };
285
+ export type AuthResult = {
286
+ ok: true;
287
+ user: UserRecord;
288
+ session: SessionInfo;
289
+ registration: RegistrationState;
290
+ } | {
291
+ ok: false;
292
+ status: number;
293
+ error: ErrorCode;
294
+ user?: UserRecord;
295
+ session?: SessionInfo;
296
+ registration?: RegistrationState;
297
+ };
298
+ /** Anything with request headers: a node `IncomingMessage`, an Express or
299
+ * Next.js request, a `ServiceRequest`, or a bare `{ headers }`. */
300
+ export interface HeadersCarrier {
301
+ headers: Record<string, string | string[] | undefined> | {
302
+ get(name: string): string | null;
303
+ };
304
+ method?: string;
305
+ }
306
+ /** The exact origins of the site's pages: a list, or a function that returns
307
+ * the list as it is now (called on each request; keep it cheap). */
308
+ export type OriginsSource = string[] | (() => string[]);
309
+ export interface AuthenticateOptions {
310
+ /** Let a visitor past the limit through (the register route does). */
311
+ allowUnregistered?: boolean;
312
+ /** Note the user's activity; see `session.touch`. */
313
+ touch?: boolean;
314
+ /** For the site's own state-changing routes: a session that arrived in a
315
+ * cookie is accepted only when the request's `Origin` header is on the
316
+ * list (403 `origin-not-allowed` otherwise). A bearer token needs no
317
+ * Origin. When the request object has a `method` and it is GET, HEAD or
318
+ * OPTIONS, nothing is checked, so the option can be passed on every call. */
319
+ checkOrigin?: boolean;
320
+ }
321
+ export interface Auth {
322
+ /** A single-use nonce that lives 120 s. */
323
+ issueNonce(): string;
324
+ /** The request's `Origin` header when it is on the exact list, else null. */
325
+ allowedOrigin(req: HeadersCarrier): string | null;
326
+ /** The exact origins accepted now. */
327
+ origins(): string[];
328
+ /** Replace the list while running. Throws on a malformed entry and keeps
329
+ * the list it had. Used nonces, launch assertions and sessions are not
330
+ * touched. */
331
+ setOrigins(origins: OriginsSource): void;
332
+ /** Check a `decent-auth` assertion: origin, nonce, signature. The nonce is
333
+ * spent by the sign-in that verifies; one that fails writes nothing. */
334
+ verifySignIn(origin: string | null | undefined, body: unknown): Promise<VerifyResult>;
335
+ /** Check a `decent-launch` assertion: origin, ±120 s, signature, then replay (consumed last, kept on disk). */
336
+ verifyLaunch(origin: string | null | undefined, body: unknown): Promise<VerifyResult>;
337
+ /** Who is making this request, for the site's own routes. Refuses a blocked
338
+ * user (403 `blocked`) and a visitor past the limit under `then: "refuse"`
339
+ * (403 `register-required`) unless `allowUnregistered` is set. Never
340
+ * rejects: no session, a bad one, or a store that cannot be read all answer
341
+ * `ok: false` (the last with status 500 `server-error`). */
342
+ authenticate(req: HeadersCarrier, opts?: AuthenticateOptions): Promise<AuthResult>;
343
+ /** A session token for `userid`. */
344
+ mintSession(userid: string): {
345
+ token: string;
346
+ sid: string;
347
+ expiresAt: number;
348
+ };
349
+ /** The token's content when it is ours, untampered and unexpired. Does not
350
+ * look at revocation or the user; `authenticate` does. */
351
+ readToken(token: string): {
352
+ userid: string;
353
+ sid: string;
354
+ issuedAt: number;
355
+ expiresAt: number;
356
+ } | null;
357
+ /** The session token the request carries (bearer first, then the cookie), or null. */
358
+ tokenOf(req: HeadersCarrier): string | null;
359
+ /** Revoke one session token (logout). */
360
+ revoke(token: string): Promise<void>;
361
+ /** The `Set-Cookie` value carrying `token`, or clearing it when `token` is null. */
362
+ cookie(token: string | null, origin?: string | null): string;
363
+ }
364
+ export interface ServiceRequest {
365
+ method: string;
366
+ /** Path below the prefix: "/nonce", "/sign-in", "/launch", "/me", "/logout", "/register". */
367
+ path?: string;
368
+ headers: Record<string, string | string[] | undefined>;
369
+ /** The parsed JSON body of a POST. */
370
+ body?: unknown;
371
+ /** The client's address, for the rate limit. The adapters fill it in. */
372
+ ip?: string | null;
373
+ /** The framework's own request object, when an adapter made this one. */
374
+ raw?: unknown;
375
+ }
376
+ export interface ServiceResponse {
377
+ status: number;
378
+ headers: Record<string, string | string[]>;
379
+ /** A JSON-serialisable object, or null for an empty answer. */
380
+ body: Record<string, unknown> | null;
381
+ }
382
+ export type RouteHandler = (req: ServiceRequest) => Promise<ServiceResponse>;
383
+ export interface Handlers {
384
+ nonce: RouteHandler;
385
+ signIn: RouteHandler;
386
+ launch: RouteHandler;
387
+ me: RouteHandler;
388
+ logout: RouteHandler;
389
+ register: RouteHandler;
390
+ }
391
+ export type RouteName = keyof Handlers;
392
+ export interface AdapterOptions {
393
+ /** Where the routes are mounted. Default "/api/auth". */
394
+ prefix?: string;
395
+ }
396
+ /** `(req, res, next?)`: a `node:http` listener, an Express middleware and a
397
+ * Next.js API route all have this shape. */
398
+ export type NodeHandler = (req: any, res: any, next?: (err?: unknown) => void) => Promise<void>;
399
+ /** A friend request as the policy sees it. */
400
+ export interface FriendRequestInfo {
401
+ userid: string;
402
+ address?: string;
403
+ name?: string;
404
+ description?: string;
405
+ hello?: string;
406
+ punkId?: number;
407
+ avatarUrl?: string;
408
+ /** ISO 8601. */
409
+ arrivedAt: string;
410
+ }
411
+ /** true accepts, false holds the request (it is looked at again when the
412
+ * sender signs in, registers or is unblocked), "reject" refuses it. */
413
+ export type FriendPolicyFn = (request: FriendRequestInfo, user: UserRecord | null) => boolean | "reject" | Promise<boolean | "reject">;
414
+ export type FriendPolicy = "all" | "signed-in" | "registered" | "manual" | FriendPolicyFn;
415
+ export interface AgentServers {
416
+ bootstrapNodes: NetworkNode[];
417
+ expressNodes?: NetworkNode[];
418
+ }
419
+ export interface AgentConfig {
420
+ /** The identity's key file; created on first start. Default `<dataDir>/agent/peer.save`. */
421
+ keyFile?: string;
422
+ nickname?: string;
423
+ statusMessage?: string;
424
+ punkId?: number;
425
+ avatarUrl?: string;
426
+ /** Who may become a friend. Default "signed-in": the userid has a user record. */
427
+ accept?: FriendPolicy;
428
+ /** Send a friend request from the agent to the user's stored address. Default "never". */
429
+ befriendOn?: "sign-in" | "register" | "never";
430
+ /** The greeting in friend requests the agent sends. Default: the nickname. */
431
+ hello?: string;
432
+ /** Days an unanswered request is kept. Default 7. */
433
+ holdDays?: number;
434
+ /** Bootstrap and Express nodes, or the URL of a list in the Beagle apps'
435
+ * `bgservers.json` format. Default `DEFAULT_SERVERS_URL`. */
436
+ servers?: AgentServers | string;
437
+ /** Hours a message may wait in the outbox before it is dead-lettered. Default 72. */
438
+ maxQueueHours?: number;
439
+ /** Extra options passed to `Peer.create` (for example `udpPort`). */
440
+ peerOptions?: Partial<PeerOptions>;
441
+ }
442
+ /** The part of `Peer` the agent uses. */
443
+ export type AgentPeer = Pick<Peer, "userid" | "address" | "start" | "stop" | "joinNetwork" | "friends" | "sessionStatus" | "onFriendRequest" | "acceptFriendRequest" | "rejectFriendRequest" | "removeFriend" | "sendFriendRequest" | "onFriendConnection" | "onText" | "sendText" | "sendTextUntilAck" | "sendTextViaExpress">;
444
+ export interface IncomingMessage {
445
+ /** The sender's key. Act on this, never on a userid written in the text. */
446
+ userid: string;
447
+ /** The sender's user record, or null when they never signed in on the site. */
448
+ user: UserRecord | null;
449
+ text: string;
450
+ deliveryId?: string;
451
+ via?: "online" | "offline";
452
+ /** Queue an answer to the sender. */
453
+ reply(text: string): Promise<SendReceipt>;
454
+ }
455
+ export interface SendReceipt {
456
+ /** Outbox id of the message. */
457
+ id: string;
458
+ /** One per part; a long text is split. Stable across retries and restarts. */
459
+ deliveryIds: string[];
460
+ state: "queued" | "dead";
461
+ /** Why it was dead-lettered at once. */
462
+ why?: DeadLetterReason;
463
+ }
464
+ export type DeadLetterReason = "not-a-friend" | "bad-address" | "blocked" | "expired";
465
+ export interface OutboxItem {
466
+ id: string;
467
+ /** Part of the message `id` (0-based) and how many parts it has. */
468
+ part: number;
469
+ parts: number;
470
+ to: string;
471
+ text: string;
472
+ deliveryId: string;
473
+ createdAt: string;
474
+ attempts: number;
475
+ /** ms; not tried again before this. */
476
+ nextAt: number;
477
+ lastError?: string;
478
+ }
479
+ export interface DeliveryEvent {
480
+ id: string;
481
+ deliveryId: string;
482
+ to: string;
483
+ /** "online": a live session took it. "offline": posted to an Express node. */
484
+ via: "online" | "offline";
485
+ /** The friend's side confirmed receipt (only possible on the live road). */
486
+ confirmed: boolean;
487
+ }
488
+ export interface DeadLetter {
489
+ at: string;
490
+ why: DeadLetterReason;
491
+ error?: string;
492
+ item: OutboxItem;
493
+ }
494
+ export type BefriendResult = {
495
+ ok: true;
496
+ how: "requested" | "accepted" | "already-friends" | "already-asked";
497
+ } | {
498
+ ok: false;
499
+ reason: "blocked" | "no-address" | "unknown-user" | "send-failed";
500
+ error?: string;
501
+ };
502
+ export interface AgentEvents {
503
+ /** A request the policy did not accept yet; it is kept on disk. */
504
+ "request-held": [request: FriendRequestInfo];
505
+ "friend-accepted": [info: {
506
+ userid: string;
507
+ }];
508
+ "friend-refused": [info: {
509
+ userid: string;
510
+ why: "blocked" | "policy" | "manual" | "expired";
511
+ }];
512
+ "friend-removed": [info: {
513
+ userid: string;
514
+ }];
515
+ delivered: [event: DeliveryEvent];
516
+ "dead-letter": [letter: DeadLetter];
517
+ [event: string]: unknown[];
518
+ }
519
+ export interface Agent {
520
+ /** The agent's own identity. Print `address` where people can add it. */
521
+ readonly userid: string;
522
+ readonly address: string;
523
+ readonly nickname: string | undefined;
524
+ /** Resolves when the peer has joined the network. Sign-in routes do not wait for it. */
525
+ readonly ready: Promise<void>;
526
+ /** The underlying `@decentnetwork/peer` instance, for what this library leaves out (files, calls, groups). */
527
+ readonly peer: AgentPeer;
528
+ /** Queue a text for a friend. Resolves once the message is on disk;
529
+ * delivery starts on the next turn of the event loop. */
530
+ send(userid: string, text: string): Promise<SendReceipt>;
531
+ onMessage(cb: (message: IncomingMessage) => unknown): () => void;
532
+ /** Send a friend request to the user's stored address (once; `force` asks again). */
533
+ befriend(userid: string, opts?: {
534
+ hello?: string;
535
+ force?: boolean;
536
+ }): Promise<BefriendResult>;
537
+ isFriend(userid: string): boolean;
538
+ /** Requests waiting on disk. */
539
+ pending(): FriendRequestInfo[];
540
+ /** Accept or refuse a held request by hand. */
541
+ accept(userid: string): Promise<boolean>;
542
+ reject(userid: string): Promise<boolean>;
543
+ /** Drop a friend. */
544
+ unfriend(userid: string): boolean;
545
+ /** Messages not delivered yet. */
546
+ outbox(): OutboxItem[];
547
+ deadLetters(): DeadLetter[];
548
+ on<K extends keyof AgentEvents>(event: K, cb: (...args: AgentEvents[K]) => unknown): () => void;
549
+ stop(): Promise<void>;
550
+ }
551
+ export interface BeagleServiceConfig {
552
+ /** Exact origins the site's pages are served from, e.g. "https://your.app"
553
+ * (scheme, host, port). No suffix matching. `http://localhost:3000` only
554
+ * when it is listed. A function is called on each request, for a list that
555
+ * changes while the server runs; `auth.setOrigins()` does the same from
556
+ * outside. A malformed entry throws when the service is created. */
557
+ origins: OriginsSource;
558
+ /** Where the library keeps its files. Default "./data". */
559
+ dataDir?: string;
560
+ /** Users backend. Default: `<dataDir>/users.json`. */
561
+ store?: UserStore;
562
+ /** Replay and revocation backend. Default: `<dataDir>/state.json`. */
563
+ state?: StateStore;
564
+ /** Signs sessions and nonces. Default: generated into `<dataDir>/session-secret`. */
565
+ sessionSecret?: string;
566
+ /** A name for this server among several that have one `sessionSecret` and
567
+ * cannot share a `state` store. A nonce is then accepted only by the server
568
+ * that issued it; sessions stay valid on all of them. */
569
+ serverId?: string;
570
+ session?: SessionConfig;
571
+ /** Limit on sign-in and launch attempts. false turns it off. */
572
+ rateLimit?: RateLimitConfig | false;
573
+ /** The site's part of a sign-in, before the answer goes out. */
574
+ onSignIn?: SignInHook;
575
+ register?: RegisterConfig;
576
+ visitor?: VisitorConfig;
577
+ /** Import the userid's beagles.eth record after sign-in. true, or the gateway and cache time. */
578
+ importProfile?: boolean | {
579
+ gateway?: string;
580
+ ttlHours?: number;
581
+ };
582
+ /** What the routes return as `user`. Default: the library's own fields, none of the site's. */
583
+ publicUser?: (user: UserRecord) => Record<string, unknown>;
584
+ /** The site's own Carrier identity. Omit when the site only needs sign-in. */
585
+ agent?: AgentConfig;
586
+ /** Warnings and errors. Default: `console.warn`. */
587
+ log?: (message: string) => void;
588
+ }
589
+ export interface BeagleService {
590
+ users: Users;
591
+ auth: Auth;
592
+ /** Present when `agent` was configured. */
593
+ agent?: Agent;
594
+ /** Framework-neutral: `({ method, path, headers, body }) → { status, headers, body }`. */
595
+ handle: RouteHandler;
596
+ /** The same, one function per route. */
597
+ handlers: Handlers;
598
+ /** For `http.createServer(...)`. Paths outside the prefix answer 404. */
599
+ nodeHandler(opts?: AdapterOptions): NodeHandler;
600
+ /** For `app.use(...)` in Express. Paths outside the prefix go to `next()`. */
601
+ expressHandler(opts?: AdapterOptions): NodeHandler;
602
+ /** For `pages/api/auth/[...beagle].js` in Next.js: `export default svc.nextApiHandler()`. */
603
+ nextApiHandler(opts?: AdapterOptions): NodeHandler;
604
+ /** One route as a `(req, res)` handler, whatever path it is mounted on: for
605
+ * a site that gives each route its own file (beside NextAuth's catch-all,
606
+ * or under names of its own). */
607
+ route(name: RouteName): NodeHandler;
608
+ /** For Web `Request`/`Response` hosts, such as Next.js App Router route handlers. */
609
+ fetchHandler(opts?: AdapterOptions): (request: Request) => Promise<Response>;
610
+ /** Stop the agent and timers. */
611
+ close(): Promise<void>;
612
+ }
613
+ /** A service that is created on first use and kept for the life of the
614
+ * process, whoever asks for it and however often the module that asks is
615
+ * evaluated. Nothing here throws at the caller for a service that cannot
616
+ * start, except `ready()`. */
617
+ export interface SharedBeagleService {
618
+ /** The service. Rejects when it cannot be created (a malformed `origins`
619
+ * entry, a users file that does not parse); the failure is not kept, the
620
+ * next call tries again. Await it at start to find a bad configuration
621
+ * before the first request. */
622
+ ready(): Promise<BeagleService>;
623
+ /** `auth.authenticate` of the service. Never rejects: when the service
624
+ * cannot start it answers `ok: false`, status 500. */
625
+ authenticate(req: HeadersCarrier, opts?: AuthenticateOptions): Promise<AuthResult>;
626
+ /** As on the service; they answer 500 `server-error` while it cannot start. */
627
+ handle: RouteHandler;
628
+ route(name: RouteName): NodeHandler;
629
+ nodeHandler(opts?: AdapterOptions): NodeHandler;
630
+ expressHandler(opts?: AdapterOptions): NodeHandler;
631
+ nextApiHandler(opts?: AdapterOptions): NodeHandler;
632
+ fetchHandler(opts?: AdapterOptions): (request: Request) => Promise<Response>;
633
+ /** Close the service if it was created, and forget it. */
634
+ close(): Promise<void>;
635
+ }
636
+ export interface SharedOptions {
637
+ /** A process may hold several services under different keys. Default "default". */
638
+ key?: string;
639
+ /** After a failed start, wait this long before trying again. Default 5000. */
640
+ retryMs?: number;
641
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { RegisterConfig, Users, UserStore, VisitorConfig } from "./types.js";
2
+ import { type Logger } from "./util.js";
3
+ export declare const DEFAULT_PROFILE_GATEWAY = "https://ens-gateway.beaglechat.workers.dev";
4
+ export interface UsersOptions {
5
+ store: UserStore;
6
+ register?: RegisterConfig;
7
+ visitor?: VisitorConfig;
8
+ importProfile?: boolean | {
9
+ gateway?: string;
10
+ ttlHours?: number;
11
+ };
12
+ now?: () => number;
13
+ log?: Logger;
14
+ fetch?: typeof fetch;
15
+ }
16
+ export declare function createUsers(opts: UsersOptions): Users;