@decentnetwork/beagle-service 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/stores.js ADDED
@@ -0,0 +1,69 @@
1
+ import { atomicWriteJson, readJsonFile } from "./util.js";
2
+ // ── users ───────────────────────────────────────────────────────────────────
3
+ /** Users in one JSON file, `{ [userid]: record }`, rewritten atomically on
4
+ * every change. The whole table is held in memory; good for a site with up to
5
+ * some tens of thousands of users and one server process. */
6
+ export function jsonFileUserStore(file) {
7
+ const users = new Map(Object.entries(readJsonFile(file, {})));
8
+ const flush = () => atomicWriteJson(file, Object.fromEntries(users));
9
+ return {
10
+ get: (userid) => {
11
+ const u = users.get(userid);
12
+ return u ? structuredClone(u) : null;
13
+ },
14
+ put: (user) => {
15
+ users.set(user.userid, structuredClone(user));
16
+ flush();
17
+ },
18
+ list: () => [...users.values()].map((u) => structuredClone(u)),
19
+ };
20
+ }
21
+ /** Users in memory; gone when the process ends. For tests. */
22
+ export function memoryUserStore() {
23
+ const users = new Map();
24
+ return {
25
+ get: (userid) => {
26
+ const u = users.get(userid);
27
+ return u ? structuredClone(u) : null;
28
+ },
29
+ put: (user) => void users.set(user.userid, structuredClone(user)),
30
+ list: () => [...users.values()].map((u) => structuredClone(u)),
31
+ };
32
+ }
33
+ function setStore(load, save, now) {
34
+ const sets = load();
35
+ const prune = () => {
36
+ const t = now();
37
+ for (const ns of Object.keys(sets)) {
38
+ for (const [k, exp] of Object.entries(sets[ns]))
39
+ if (exp < t)
40
+ delete sets[ns][k];
41
+ }
42
+ };
43
+ return {
44
+ addOnce(ns, key, expiresAt) {
45
+ prune();
46
+ const set = (sets[ns] ??= {});
47
+ if (Object.prototype.hasOwnProperty.call(set, key))
48
+ return false;
49
+ set[key] = expiresAt;
50
+ // Written before the answer goes out: a restart must not forget it.
51
+ save(sets);
52
+ return true;
53
+ },
54
+ has(ns, key) {
55
+ const exp = sets[ns]?.[key];
56
+ return exp !== undefined && exp >= now();
57
+ },
58
+ };
59
+ }
60
+ /** Used nonces, used launch assertions and revoked sessions in one JSON file,
61
+ * written atomically before each answer. One server process. */
62
+ export function jsonFileStateStore(file, now = Date.now) {
63
+ return setStore(() => readJsonFile(file, {}), (s) => atomicWriteJson(file, s), now);
64
+ }
65
+ /** The same in memory. A restart forgets it, which reopens launch replay for
66
+ * two minutes; for tests. */
67
+ export function memoryStateStore(now = Date.now) {
68
+ return setStore(() => ({}), () => { }, now);
69
+ }
@@ -0,0 +1,496 @@
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
+ status: UserStatus;
18
+ /** The site's registration fields (`register.required` / `register.optional`). */
19
+ profile: Record<string, unknown>;
20
+ /** ISO 8601. null for a userid that was blocked before it ever signed in. */
21
+ firstSeen: string | null;
22
+ lastSeen: string | null;
23
+ /** Bursts of activity, separated by `visitor.visitGapMinutes` of silence. */
24
+ visits: number;
25
+ /** Successful sign-ins and Apps-tab launches. */
26
+ signIns: number;
27
+ registeredAt?: string | null;
28
+ blocked?: {
29
+ at: string;
30
+ why: string;
31
+ was: "visitor" | "registered";
32
+ } | null;
33
+ /** ms since the epoch; a session issued before this moment is refused. */
34
+ sessionsValidFrom?: number;
35
+ /** What the userid published under a beagles.eth name (`importProfile`). */
36
+ published?: PublishedProfile | null;
37
+ [key: string]: unknown;
38
+ }
39
+ export interface PublishedProfile {
40
+ name?: string;
41
+ displayName?: string;
42
+ description?: string;
43
+ avatar?: string;
44
+ url?: string;
45
+ /** The record's owner. When it equals the userid, that key published it. */
46
+ owner?: string;
47
+ fetchedAt: string;
48
+ }
49
+ /** Where users are kept. Implement these three calls for Postgres, sqlite or
50
+ * anything else. `put` receives the whole record and must store all of it,
51
+ * including fields it does not recognise. */
52
+ export interface UserStore {
53
+ get(userid: string): Promise<UserRecord | null> | UserRecord | null;
54
+ put(user: UserRecord): Promise<void> | void;
55
+ list(): Promise<UserRecord[]> | UserRecord[];
56
+ }
57
+ /** Small durable sets with expiry: used nonces, used launch assertions and
58
+ * revoked sessions. With several server processes, back this with a shared
59
+ * database; `addOnce` must then be atomic there (an insert that fails on a
60
+ * duplicate key). */
61
+ export interface StateStore {
62
+ /** Remember `key` in `ns` until `expiresAt` (ms). true when it was not
63
+ * there, or had expired; false when it was. */
64
+ addOnce(ns: string, key: string, expiresAt: number): Promise<boolean> | boolean;
65
+ has(ns: string, key: string): Promise<boolean> | boolean;
66
+ }
67
+ export interface RegisterConfig {
68
+ /** Fields a visitor must give to become a registered user. Empty (the
69
+ * default): every visitor is a registered user at once. */
70
+ required?: string[];
71
+ /** Fields the register route also accepts and stores. */
72
+ optional?: string[];
73
+ /** Confirm one submitted field. Return true to accept, false or a string
74
+ * (the reason, shown to the page) to refuse, or `{ value }` to store a
75
+ * normalised value. `submitted` is everything the page posted, so a site
76
+ * can read a confirmation code it asked for. Sending the email or the SMS
77
+ * is the site's part. */
78
+ verify?: (field: string, value: unknown, user: UserRecord, submitted: Record<string, unknown>) => boolean | string | {
79
+ value: unknown;
80
+ } | Promise<boolean | string | {
81
+ value: unknown;
82
+ }>;
83
+ }
84
+ export interface VisitorConfig {
85
+ /** Days after `firstSeen` a visitor may stay unregistered. */
86
+ maxDays?: number;
87
+ /** Visits a visitor may make unregistered. */
88
+ maxVisits?: number;
89
+ /** After the limit: "ask" keeps answering and marks registration as due;
90
+ * "refuse" answers 403 `register-required` until they register. Default "ask". */
91
+ then?: "ask" | "refuse";
92
+ /** Silence that separates two visits. Default 30. */
93
+ visitGapMinutes?: number;
94
+ }
95
+ /** What the page is told about registration. */
96
+ export interface RegistrationState {
97
+ status: UserStatus;
98
+ required: string[];
99
+ optional: string[];
100
+ /** Required fields the user has not given yet. */
101
+ missing: string[];
102
+ /** The visitor limit has passed and fields are missing. */
103
+ due: boolean;
104
+ /** `due`, and the site answers 403 until they register. */
105
+ refused: boolean;
106
+ /** When `visitor.maxDays` runs out (ISO 8601), if it is set. */
107
+ deadline?: string;
108
+ /** Visits left before `visitor.maxVisits` is reached, if it is set. */
109
+ visitsLeft?: number;
110
+ }
111
+ export type RegisterResult = {
112
+ ok: true;
113
+ user: UserRecord;
114
+ } | {
115
+ ok: false;
116
+ error: "invalid-fields";
117
+ fields: Record<string, string>;
118
+ } | {
119
+ ok: false;
120
+ error: "blocked" | "not-found";
121
+ };
122
+ export interface UsersEvents {
123
+ /** After every successful sign-in or launch. */
124
+ "sign-in": [user: UserRecord, info: {
125
+ first: boolean;
126
+ door: string | null;
127
+ }];
128
+ /** A visitor became a registered user. */
129
+ registered: [user: UserRecord];
130
+ blocked: [user: UserRecord];
131
+ unblocked: [user: UserRecord];
132
+ [event: string]: unknown[];
133
+ }
134
+ export interface Users {
135
+ get(userid: string): Promise<UserRecord | null>;
136
+ /** All users, or those with one status. */
137
+ list(filter?: {
138
+ status?: UserStatus;
139
+ }): Promise<UserRecord[]>;
140
+ /** Merge `patch` into the record (one level deep for `profile`). Fields
141
+ * the patch does not name are kept, known to this library or not. */
142
+ update(userid: string, patch: Partial<UserRecord>): Promise<UserRecord>;
143
+ /** Refuse this userid: sign-in answers 403 `blocked`, its sessions stop
144
+ * working, and the agent refuses and removes it as a friend. Works for a
145
+ * userid that has never signed in. */
146
+ block(userid: string, why?: string): Promise<UserRecord>;
147
+ unblock(userid: string): Promise<UserRecord | null>;
148
+ /** End every session of this user (they sign in again). */
149
+ revokeSessions(userid: string): Promise<void>;
150
+ /** Check and store registration fields; a visitor with every required
151
+ * field becomes `registered`. */
152
+ register(userid: string, submitted: Record<string, unknown>): Promise<RegisterResult>;
153
+ registration(user: UserRecord): RegistrationState;
154
+ /** Record a sign-in the site verified by a door of its own. The two built-in
155
+ * doors call this themselves. */
156
+ recordSignIn(userid: string, hints?: {
157
+ address?: unknown;
158
+ name?: unknown;
159
+ avatar?: unknown;
160
+ punkId?: unknown;
161
+ }, info?: {
162
+ door?: string | null;
163
+ }): Promise<UserRecord>;
164
+ /** Note activity: `lastSeen`, and `visits` after a gap. Written at most
165
+ * once a minute per user. */
166
+ touch(userid: string): Promise<UserRecord | null>;
167
+ /** Read the userid's beagles.eth record and store it under `published`. */
168
+ importProfile(userid: string): Promise<PublishedProfile | null>;
169
+ on<K extends keyof UsersEvents>(event: K, cb: (...args: UsersEvents[K]) => unknown): () => void;
170
+ }
171
+ export interface SessionConfig {
172
+ /** Days until a session expires. Default 30. */
173
+ ttlDays?: number;
174
+ /** "cookie" (default): HttpOnly cookie. "bearer": the token is returned in
175
+ * the sign-in answer and sent back as `Authorization: Bearer`. "both". */
176
+ transport?: "cookie" | "bearer" | "both";
177
+ /** Default "beagle_session". */
178
+ cookieName?: string;
179
+ /** Default "Lax". */
180
+ sameSite?: "Lax" | "Strict" | "None";
181
+ cookieDomain?: string;
182
+ /** Default "/". */
183
+ cookiePath?: string;
184
+ }
185
+ export interface SessionInfo {
186
+ userid: string;
187
+ /** Session id; logout revokes this. */
188
+ sid: string;
189
+ issuedAt: number;
190
+ expiresAt: number;
191
+ /** How the token arrived. */
192
+ via: "cookie" | "bearer";
193
+ }
194
+ export type ErrorCode = "origin-not-allowed" | "bad-request" | "bad-nonce" | "bad-signature" | "launch-expired" | "launch-replayed" | "blocked" | "register-required" | "not-signed-in" | "invalid-fields" | "not-found" | "method-not-allowed" | "too-large" | "server-error";
195
+ export type VerifyResult = {
196
+ ok: true;
197
+ userid: string;
198
+ hints: {
199
+ address?: unknown;
200
+ name?: unknown;
201
+ avatar?: unknown;
202
+ punkId?: unknown;
203
+ };
204
+ door: string | null;
205
+ } | {
206
+ ok: false;
207
+ status: number;
208
+ error: ErrorCode;
209
+ };
210
+ export type AuthResult = {
211
+ ok: true;
212
+ user: UserRecord;
213
+ session: SessionInfo;
214
+ registration: RegistrationState;
215
+ } | {
216
+ ok: false;
217
+ status: number;
218
+ error: ErrorCode;
219
+ user?: UserRecord;
220
+ session?: SessionInfo;
221
+ registration?: RegistrationState;
222
+ };
223
+ /** Anything with request headers: a node `IncomingMessage`, an Express or
224
+ * Next.js request, a `ServiceRequest`, or a bare `{ headers }`. */
225
+ export interface HeadersCarrier {
226
+ headers: Record<string, string | string[] | undefined> | {
227
+ get(name: string): string | null;
228
+ };
229
+ }
230
+ export interface Auth {
231
+ /** A single-use nonce that lives 120 s. */
232
+ issueNonce(): string;
233
+ /** The request's `Origin` header when it is on the exact list, else null. */
234
+ allowedOrigin(req: HeadersCarrier): string | null;
235
+ /** Check a `decent-auth` assertion: origin, nonce (consumed either way), signature. */
236
+ verifySignIn(origin: string | null | undefined, body: unknown): Promise<VerifyResult>;
237
+ /** Check a `decent-launch` assertion: origin, ±120 s, signature, then replay (consumed last, kept on disk). */
238
+ verifyLaunch(origin: string | null | undefined, body: unknown): Promise<VerifyResult>;
239
+ /** Who is making this request, for the site's own routes. Refuses a blocked
240
+ * user (403 `blocked`) and a visitor past the limit under `then: "refuse"`
241
+ * (403 `register-required`) unless `allowUnregistered` is set. */
242
+ authenticate(req: HeadersCarrier, opts?: {
243
+ allowUnregistered?: boolean;
244
+ touch?: boolean;
245
+ }): Promise<AuthResult>;
246
+ /** A session token for `userid`. */
247
+ mintSession(userid: string): {
248
+ token: string;
249
+ sid: string;
250
+ expiresAt: number;
251
+ };
252
+ /** The token's content when it is ours, untampered and unexpired. Does not
253
+ * look at revocation or the user; `authenticate` does. */
254
+ readToken(token: string): {
255
+ userid: string;
256
+ sid: string;
257
+ issuedAt: number;
258
+ expiresAt: number;
259
+ } | null;
260
+ /** The session token the request carries (bearer first, then the cookie), or null. */
261
+ tokenOf(req: HeadersCarrier): string | null;
262
+ /** Revoke one session token (logout). */
263
+ revoke(token: string): Promise<void>;
264
+ /** The `Set-Cookie` value carrying `token`, or clearing it when `token` is null. */
265
+ cookie(token: string | null, origin?: string | null): string;
266
+ }
267
+ export interface ServiceRequest {
268
+ method: string;
269
+ /** Path below the prefix: "/nonce", "/sign-in", "/launch", "/me", "/logout", "/register". */
270
+ path?: string;
271
+ headers: Record<string, string | string[] | undefined>;
272
+ /** The parsed JSON body of a POST. */
273
+ body?: unknown;
274
+ }
275
+ export interface ServiceResponse {
276
+ status: number;
277
+ headers: Record<string, string | string[]>;
278
+ /** A JSON-serialisable object, or null for an empty answer. */
279
+ body: Record<string, unknown> | null;
280
+ }
281
+ export type RouteHandler = (req: ServiceRequest) => Promise<ServiceResponse>;
282
+ export interface Handlers {
283
+ nonce: RouteHandler;
284
+ signIn: RouteHandler;
285
+ launch: RouteHandler;
286
+ me: RouteHandler;
287
+ logout: RouteHandler;
288
+ register: RouteHandler;
289
+ }
290
+ export interface AdapterOptions {
291
+ /** Where the routes are mounted. Default "/api/auth". */
292
+ prefix?: string;
293
+ }
294
+ /** `(req, res, next?)`: a `node:http` listener, an Express middleware and a
295
+ * Next.js API route all have this shape. */
296
+ export type NodeHandler = (req: any, res: any, next?: (err?: unknown) => void) => Promise<void>;
297
+ /** A friend request as the policy sees it. */
298
+ export interface FriendRequestInfo {
299
+ userid: string;
300
+ address?: string;
301
+ name?: string;
302
+ description?: string;
303
+ hello?: string;
304
+ punkId?: number;
305
+ avatarUrl?: string;
306
+ /** ISO 8601. */
307
+ arrivedAt: string;
308
+ }
309
+ /** true accepts, false holds the request (it is looked at again when the
310
+ * sender signs in, registers or is unblocked), "reject" refuses it. */
311
+ export type FriendPolicyFn = (request: FriendRequestInfo, user: UserRecord | null) => boolean | "reject" | Promise<boolean | "reject">;
312
+ export type FriendPolicy = "all" | "signed-in" | "registered" | "manual" | FriendPolicyFn;
313
+ export interface AgentServers {
314
+ bootstrapNodes: NetworkNode[];
315
+ expressNodes?: NetworkNode[];
316
+ }
317
+ export interface AgentConfig {
318
+ /** The identity's key file; created on first start. Default `<dataDir>/agent/peer.save`. */
319
+ keyFile?: string;
320
+ nickname?: string;
321
+ statusMessage?: string;
322
+ punkId?: number;
323
+ avatarUrl?: string;
324
+ /** Who may become a friend. Default "signed-in": the userid has a user record. */
325
+ accept?: FriendPolicy;
326
+ /** Send a friend request from the agent to the user's stored address. Default "never". */
327
+ befriendOn?: "sign-in" | "register" | "never";
328
+ /** The greeting in friend requests the agent sends. Default: the nickname. */
329
+ hello?: string;
330
+ /** Days an unanswered request is kept. Default 7. */
331
+ holdDays?: number;
332
+ /** Bootstrap and Express nodes, or the URL of a list in the Beagle apps'
333
+ * `bgservers.json` format. Default `DEFAULT_SERVERS_URL`. */
334
+ servers?: AgentServers | string;
335
+ /** Hours a message may wait in the outbox before it is dead-lettered. Default 72. */
336
+ maxQueueHours?: number;
337
+ /** Extra options passed to `Peer.create` (for example `udpPort`). */
338
+ peerOptions?: Partial<PeerOptions>;
339
+ }
340
+ /** The part of `Peer` the agent uses. */
341
+ export type AgentPeer = Pick<Peer, "userid" | "address" | "start" | "stop" | "joinNetwork" | "friends" | "sessionStatus" | "onFriendRequest" | "acceptFriendRequest" | "rejectFriendRequest" | "removeFriend" | "sendFriendRequest" | "onFriendConnection" | "onText" | "sendText" | "sendTextUntilAck" | "sendTextViaExpress">;
342
+ export interface IncomingMessage {
343
+ /** The sender's key. Act on this, never on a userid written in the text. */
344
+ userid: string;
345
+ /** The sender's user record, or null when they never signed in on the site. */
346
+ user: UserRecord | null;
347
+ text: string;
348
+ deliveryId?: string;
349
+ via?: "online" | "offline";
350
+ /** Queue an answer to the sender. */
351
+ reply(text: string): Promise<SendReceipt>;
352
+ }
353
+ export interface SendReceipt {
354
+ /** Outbox id of the message. */
355
+ id: string;
356
+ /** One per part; a long text is split. Stable across retries and restarts. */
357
+ deliveryIds: string[];
358
+ state: "queued" | "dead";
359
+ /** Why it was dead-lettered at once. */
360
+ why?: DeadLetterReason;
361
+ }
362
+ export type DeadLetterReason = "not-a-friend" | "bad-address" | "blocked" | "expired";
363
+ export interface OutboxItem {
364
+ id: string;
365
+ /** Part of the message `id` (0-based) and how many parts it has. */
366
+ part: number;
367
+ parts: number;
368
+ to: string;
369
+ text: string;
370
+ deliveryId: string;
371
+ createdAt: string;
372
+ attempts: number;
373
+ /** ms; not tried again before this. */
374
+ nextAt: number;
375
+ lastError?: string;
376
+ }
377
+ export interface DeliveryEvent {
378
+ id: string;
379
+ deliveryId: string;
380
+ to: string;
381
+ /** "online": a live session took it. "offline": posted to an Express node. */
382
+ via: "online" | "offline";
383
+ /** The friend's side confirmed receipt (only possible on the live road). */
384
+ confirmed: boolean;
385
+ }
386
+ export interface DeadLetter {
387
+ at: string;
388
+ why: DeadLetterReason;
389
+ error?: string;
390
+ item: OutboxItem;
391
+ }
392
+ export type BefriendResult = {
393
+ ok: true;
394
+ how: "requested" | "accepted" | "already-friends" | "already-asked";
395
+ } | {
396
+ ok: false;
397
+ reason: "blocked" | "no-address" | "unknown-user" | "send-failed";
398
+ error?: string;
399
+ };
400
+ export interface AgentEvents {
401
+ /** A request the policy did not accept yet; it is kept on disk. */
402
+ "request-held": [request: FriendRequestInfo];
403
+ "friend-accepted": [info: {
404
+ userid: string;
405
+ }];
406
+ "friend-refused": [info: {
407
+ userid: string;
408
+ why: "blocked" | "policy" | "manual" | "expired";
409
+ }];
410
+ "friend-removed": [info: {
411
+ userid: string;
412
+ }];
413
+ delivered: [event: DeliveryEvent];
414
+ "dead-letter": [letter: DeadLetter];
415
+ [event: string]: unknown[];
416
+ }
417
+ export interface Agent {
418
+ /** The agent's own identity. Print `address` where people can add it. */
419
+ readonly userid: string;
420
+ readonly address: string;
421
+ readonly nickname: string | undefined;
422
+ /** Resolves when the peer has joined the network. Sign-in routes do not wait for it. */
423
+ readonly ready: Promise<void>;
424
+ /** The underlying `@decentnetwork/peer` instance, for what this library leaves out (files, calls, groups). */
425
+ readonly peer: AgentPeer;
426
+ /** Queue a text for a friend. Resolves once the message is on disk;
427
+ * delivery starts on the next turn of the event loop. */
428
+ send(userid: string, text: string): Promise<SendReceipt>;
429
+ onMessage(cb: (message: IncomingMessage) => unknown): () => void;
430
+ /** Send a friend request to the user's stored address (once; `force` asks again). */
431
+ befriend(userid: string, opts?: {
432
+ hello?: string;
433
+ force?: boolean;
434
+ }): Promise<BefriendResult>;
435
+ isFriend(userid: string): boolean;
436
+ /** Requests waiting on disk. */
437
+ pending(): FriendRequestInfo[];
438
+ /** Accept or refuse a held request by hand. */
439
+ accept(userid: string): Promise<boolean>;
440
+ reject(userid: string): Promise<boolean>;
441
+ /** Drop a friend. */
442
+ unfriend(userid: string): boolean;
443
+ /** Messages not delivered yet. */
444
+ outbox(): OutboxItem[];
445
+ deadLetters(): DeadLetter[];
446
+ on<K extends keyof AgentEvents>(event: K, cb: (...args: AgentEvents[K]) => unknown): () => void;
447
+ stop(): Promise<void>;
448
+ }
449
+ export interface BeagleServiceConfig {
450
+ /** Exact origins the site's pages are served from, e.g. "https://your.app"
451
+ * (scheme, host, port). No suffix matching. `http://localhost:3000` only
452
+ * when it is listed. */
453
+ origins: string[];
454
+ /** Where the library keeps its files. Default "./data". */
455
+ dataDir?: string;
456
+ /** Users backend. Default: `<dataDir>/users.json`. */
457
+ store?: UserStore;
458
+ /** Replay and revocation backend. Default: `<dataDir>/state.json`. */
459
+ state?: StateStore;
460
+ /** Signs sessions and nonces. Default: generated into `<dataDir>/session-secret`. */
461
+ sessionSecret?: string;
462
+ session?: SessionConfig;
463
+ register?: RegisterConfig;
464
+ visitor?: VisitorConfig;
465
+ /** Import the userid's beagles.eth record after sign-in. true, or the gateway and cache time. */
466
+ importProfile?: boolean | {
467
+ gateway?: string;
468
+ ttlHours?: number;
469
+ };
470
+ /** What the routes return as `user`. Default: the library's own fields, none of the site's. */
471
+ publicUser?: (user: UserRecord) => Record<string, unknown>;
472
+ /** The site's own Carrier identity. Omit when the site only needs sign-in. */
473
+ agent?: AgentConfig;
474
+ /** Warnings and errors. Default: `console.warn`. */
475
+ log?: (message: string) => void;
476
+ }
477
+ export interface BeagleService {
478
+ users: Users;
479
+ auth: Auth;
480
+ /** Present when `agent` was configured. */
481
+ agent?: Agent;
482
+ /** Framework-neutral: `({ method, path, headers, body }) → { status, headers, body }`. */
483
+ handle: RouteHandler;
484
+ /** The same, one function per route. */
485
+ handlers: Handlers;
486
+ /** For `http.createServer(...)`. Paths outside the prefix answer 404. */
487
+ nodeHandler(opts?: AdapterOptions): NodeHandler;
488
+ /** For `app.use(...)` in Express. Paths outside the prefix go to `next()`. */
489
+ expressHandler(opts?: AdapterOptions): NodeHandler;
490
+ /** For `pages/api/auth/[...beagle].js` in Next.js: `export default svc.nextApiHandler()`. */
491
+ nextApiHandler(opts?: AdapterOptions): NodeHandler;
492
+ /** For Web `Request`/`Response` hosts, such as Next.js App Router route handlers. */
493
+ fetchHandler(opts?: AdapterOptions): (request: Request) => Promise<Response>;
494
+ /** Stop the agent and timers. */
495
+ close(): Promise<void>;
496
+ }
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;