@flow-industries/id 0.26.3 → 0.26.5

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 (39) hide show
  1. package/README.md +38 -0
  2. package/contracts/v1/sdk-exports.json +95 -0
  3. package/dist/sdk/caller.d.ts +29 -0
  4. package/dist/sdk/caller.js +37 -0
  5. package/dist/sdk/client/atproto-resolver.d.ts +8 -0
  6. package/dist/sdk/client/atproto-resolver.js +87 -0
  7. package/dist/sdk/client/atproto.js +9 -1
  8. package/dist/sdk/client/create-flow.js +10 -0
  9. package/dist/sdk/client/index.d.ts +6 -1
  10. package/dist/sdk/client/index.js +4 -0
  11. package/dist/sdk/client/note-transport.d.ts +15 -0
  12. package/dist/sdk/client/note-transport.js +104 -0
  13. package/dist/sdk/client/passkey-prf.d.ts +8 -0
  14. package/dist/sdk/client/passkey-prf.js +67 -0
  15. package/dist/sdk/client/session.d.ts +2 -0
  16. package/dist/sdk/client/session.js +23 -0
  17. package/dist/sdk/client/static-flow.js +2 -0
  18. package/dist/sdk/db/invitations.d.ts +310 -0
  19. package/dist/sdk/db/invitations.js +42 -0
  20. package/dist/sdk/db/schema.d.ts +248 -0
  21. package/dist/sdk/db/schema.js +25 -0
  22. package/dist/sdk/session-view.d.ts +15 -0
  23. package/dist/sdk/session-view.js +32 -0
  24. package/dist/sdk/types/atproto-resolver.d.ts +46 -0
  25. package/dist/sdk/types/atproto-resolver.js +0 -0
  26. package/dist/sdk/types/atproto.d.ts +24 -0
  27. package/dist/sdk/types/auth.d.ts +4 -0
  28. package/dist/sdk/types/dialog.d.ts +3 -0
  29. package/dist/sdk/types/hosted-oauth.d.ts +23 -0
  30. package/dist/sdk/types/hosted-oauth.js +0 -0
  31. package/dist/sdk/types/hosting-quota.d.ts +28 -0
  32. package/dist/sdk/types/hosting-quota.js +0 -0
  33. package/dist/sdk/types/index.d.ts +7 -2
  34. package/dist/sdk/types/managed-provisioning.d.ts +51 -0
  35. package/dist/sdk/types/managed-provisioning.js +0 -0
  36. package/dist/sdk/types/note-transport.d.ts +140 -0
  37. package/dist/sdk/types/note-transport.js +0 -0
  38. package/dist/sdk/types/sdk.d.ts +7 -0
  39. package/package.json +4 -2
package/README.md CHANGED
@@ -23,6 +23,33 @@ partially migrated schema.
23
23
 
24
24
  ### AT Protocol identity
25
25
 
26
+ Consumers discover the signed-in user's current PDS through
27
+ `await flow.atproto.resolve({ freshness: "cached" })`. Use `"refresh"` after an
28
+ endpoint or authorization failure, and call `flow.atproto.invalidate()` after
29
+ identity or migration events. Flow account switches, logout and the first-party
30
+ linking helpers invalidate automatically. Invalidation forces the next request
31
+ to refresh. Discovery uses the configured Auth host, cookie or Flow bearer
32
+ session, and canonical user ID; it never provisions an account.
33
+
34
+ The result includes the DID, verified display handle (or null), current PDS
35
+ origin, hosting ownership, lifecycle and authorization status. DID documents
36
+ are authoritative for the endpoint. No OAuth tokens or private keys are
37
+ returned. `connected` indicates an unexpired stored grant whose audience and
38
+ issuer match public resource metadata; it does not prove that a remote grant
39
+ has not since been revoked. Writes must obtain fresh AT OAuth authorization,
40
+ and must not forward old credentials to a migrated host or replay mutations
41
+ with unknown outcomes. `AtprotoRequestError.code` distinguishes sign-in,
42
+ missing identity, pending or failed provisioning, account changes, unsupported
43
+ DID methods, invalid identity and unavailable resolution.
44
+
45
+ `ATPROTO_MANAGED_PDS_ORIGINS` is an optional comma-separated list of public
46
+ HTTPS PDS origins owned by Flow, used only to classify current hosting.
47
+ `ATPROTO_RESOLUTION_TTL_MS` enables a server metadata cache keyed by DID and
48
+ mapping version, bounded to five minutes; unset or invalid values disable
49
+ caching. Authorization status is checked separately on each request. A cached
50
+ authorization failure forces one fresh DID resolution. Public discovery works
51
+ without OAuth credential configuration and reports `reconnect_required`.
52
+
26
53
  Bluesky and other AT Protocol accounts can sign in to Flow ID or connect to an
27
54
  existing account. Flow user IDs remain stable; the provider DID identifies the
28
55
  connection. New accounts do not need an email, passkey, or wallet to enroll.
@@ -223,3 +250,14 @@ still need complete request/response descriptions and generated client coverage
223
250
  before the full HTTP contract work is complete.
224
251
 
225
252
  Server deployments may set `FLOW_ID_API_URL` to a trusted internal HTTP(S) origin for API and JWKS requests, or pass `apiUrl` to server session/verification helpers. `issuerUrl` remains the public JWT issuer and browser navigation origin. The transport URL must contain no credentials, path, query, or fragment.
253
+
254
+ PDS discovery requires explicit nonnegative integer budgets in
255
+ `ATPROTO_RESOLUTION_USER_HOURLY_CAP` and
256
+ `ATPROTO_RESOLUTION_GLOBAL_HOURLY_CAP`. Unset, invalid or zero budgets refuse
257
+ outbound discovery with HTTP 429 and retryable `RESOLUTION_UNAVAILABLE`. Every discovery
258
+ request, including cached reads and forced refreshes, consumes durable
259
+ per-user and fleet-wide hourly admission before any provider request. Failed
260
+ outbound attempts retain admission consumption; counters use the existing
261
+ bounded verification retention and canonical account erasure mechanism.
262
+ These caps bound request admission across replicas; operators must choose
263
+ budgets appropriate to replica count, outbound latency and capacity.
@@ -92,6 +92,31 @@
92
92
  "type": false,
93
93
  "value": true
94
94
  },
95
+ {
96
+ "name": "AtprotoApi",
97
+ "type": true,
98
+ "value": false
99
+ },
100
+ {
101
+ "name": "AtprotoRequestError",
102
+ "type": true,
103
+ "value": true
104
+ },
105
+ {
106
+ "name": "AtprotoResolution",
107
+ "type": true,
108
+ "value": false
109
+ },
110
+ {
111
+ "name": "AtprotoResolutionFailure",
112
+ "type": true,
113
+ "value": false
114
+ },
115
+ {
116
+ "name": "AtprotoResolveOptions",
117
+ "type": true,
118
+ "value": false
119
+ },
95
120
  {
96
121
  "name": "BrowserAccount",
97
122
  "type": true,
@@ -142,6 +167,11 @@
142
167
  "type": false,
143
168
  "value": true
144
169
  },
170
+ {
171
+ "name": "createAtprotoApi",
172
+ "type": false,
173
+ "value": true
174
+ },
145
175
  {
146
176
  "name": "createDialogHost",
147
177
  "type": false,
@@ -167,6 +197,11 @@
167
197
  "type": false,
168
198
  "value": true
169
199
  },
200
+ {
201
+ "name": "createNoteTransport",
202
+ "type": false,
203
+ "value": true
204
+ },
170
205
  {
171
206
  "name": "createProfileButton",
172
207
  "type": false,
@@ -222,6 +257,11 @@
222
257
  "type": false,
223
258
  "value": true
224
259
  },
260
+ {
261
+ "name": "evaluatePasskeyPrf",
262
+ "type": false,
263
+ "value": true
264
+ },
225
265
  {
226
266
  "name": "fieldsCoveredBySubjects",
227
267
  "type": false,
@@ -317,6 +357,11 @@
317
357
  "type": false,
318
358
  "value": true
319
359
  },
360
+ {
361
+ "name": "getConnectedPasskey",
362
+ "type": false,
363
+ "value": true
364
+ },
320
365
  {
321
366
  "name": "getFlow",
322
367
  "type": false,
@@ -397,6 +442,46 @@
397
442
  "type": false,
398
443
  "value": true
399
444
  },
445
+ {
446
+ "name": "NoteBlob",
447
+ "type": true,
448
+ "value": false
449
+ },
450
+ {
451
+ "name": "NoteCollection",
452
+ "type": true,
453
+ "value": false
454
+ },
455
+ {
456
+ "name": "NoteOperation",
457
+ "type": true,
458
+ "value": false
459
+ },
460
+ {
461
+ "name": "NoteRecord",
462
+ "type": true,
463
+ "value": false
464
+ },
465
+ {
466
+ "name": "NoteResponses",
467
+ "type": true,
468
+ "value": false
469
+ },
470
+ {
471
+ "name": "NoteStoredRecord",
472
+ "type": true,
473
+ "value": false
474
+ },
475
+ {
476
+ "name": "NoteTransportError",
477
+ "type": true,
478
+ "value": true
479
+ },
480
+ {
481
+ "name": "NoteVaultHeader",
482
+ "type": true,
483
+ "value": false
484
+ },
400
485
  {
401
486
  "name": "NotificationCounts",
402
487
  "type": true,
@@ -432,6 +517,16 @@
432
517
  "type": true,
433
518
  "value": false
434
519
  },
520
+ {
521
+ "name": "PasskeyPrfError",
522
+ "type": true,
523
+ "value": true
524
+ },
525
+ {
526
+ "name": "PasskeyPrfOptions",
527
+ "type": true,
528
+ "value": false
529
+ },
435
530
  {
436
531
  "name": "ProfileButtonHandle",
437
532
  "type": true,
@@ -0,0 +1,29 @@
1
+ import type { AppDb } from "./db/handle";
2
+ import type { SessionUser } from "./types";
3
+ /**
4
+ * Caller-identity core, shared by the dialog-facing routes and the tests. Takes
5
+ * the drizzle handle as a parameter (postgres-js in the server, PGlite in
6
+ * tests) instead of importing the singleton, which requires DATABASE_URL at
7
+ * import time. The singleton-bound entry point is `resolveCaller` in
8
+ * `routes/caller.ts`.
9
+ */
10
+ /**
11
+ * The user behind a dialog request, and the better-auth session row that
12
+ * authenticated it — null when the caller presented an access JWT instead,
13
+ * since a bearer holds no session row.
14
+ */
15
+ /** The session row as callers read it: opaque beyond the fields session-view names. */
16
+ export type CallerSession = {
17
+ id: string;
18
+ audience?: string | null;
19
+ };
20
+ export type Caller = {
21
+ user: SessionUser;
22
+ session: CallerSession | null;
23
+ };
24
+ /**
25
+ * Reads the user a verified bearer's `sub` names, in the shape better-auth puts
26
+ * on `session.user`. Null when the subject no longer exists — a token for a
27
+ * deleted account is unauthorized, not a caller with an empty profile.
28
+ */
29
+ export declare function loadCallerUser(dbi: AppDb, userId: string): Promise<SessionUser | null>;
@@ -0,0 +1,37 @@
1
+ import { eq } from "drizzle-orm";
2
+ import * as schema from "./db/schema";
3
+ import { sessionUserOf } from "./session-view";
4
+ /**
5
+ * The columns better-auth puts on `session.user` (base fields plus the
6
+ * configured `additionalFields`), so a bearer-authenticated caller reads back
7
+ * the same user shape a cookie-authenticated one does. `sessionEpoch` is
8
+ * deliberately absent: it is an internal revocation lever, not profile data.
9
+ */
10
+ const USER_COLUMNS = {
11
+ id: schema.user.id,
12
+ name: schema.user.name,
13
+ username: schema.user.username,
14
+ email: schema.user.email,
15
+ emailVerified: schema.user.emailVerified,
16
+ image: schema.user.image,
17
+ bio: schema.user.bio,
18
+ isGuest: schema.user.isGuest,
19
+ role: schema.user.role,
20
+ walletAddress: schema.user.walletAddress,
21
+ subscribedToNews: schema.user.subscribedToNews,
22
+ createdAt: schema.user.createdAt,
23
+ updatedAt: schema.user.updatedAt,
24
+ };
25
+ /**
26
+ * Reads the user a verified bearer's `sub` names, in the shape better-auth puts
27
+ * on `session.user`. Null when the subject no longer exists — a token for a
28
+ * deleted account is unauthorized, not a caller with an empty profile.
29
+ */
30
+ export async function loadCallerUser(dbi, userId) {
31
+ const [user] = await dbi
32
+ .select(USER_COLUMNS)
33
+ .from(schema.user)
34
+ .where(eq(schema.user.id, userId))
35
+ .limit(1);
36
+ return user ? sessionUserOf(user) : null;
37
+ }
@@ -0,0 +1,8 @@
1
+ import type { AtprotoApi, AtprotoResolutionFailure } from "../types/atproto";
2
+ export declare class AtprotoRequestError extends Error {
3
+ readonly code: AtprotoResolutionFailure;
4
+ readonly retryable: boolean;
5
+ constructor(code: AtprotoResolutionFailure, retryable?: boolean);
6
+ }
7
+ /** Identity changes and migration completion should invalidate before another discovery. */
8
+ export declare function createAtprotoApi(host: string, getToken: () => Promise<string | null>, getUserId: () => string | null): AtprotoApi;
@@ -0,0 +1,87 @@
1
+ import { z } from "zod";
2
+ const failureSchema = z.enum([
3
+ "SIGN_IN_REQUIRED",
4
+ "NO_AT_IDENTITY",
5
+ "PROVISIONING_PENDING",
6
+ "PROVISIONING_FAILED",
7
+ "ACCOUNT_CHANGED",
8
+ "UNSUPPORTED_DID_METHOD",
9
+ "IDENTITY_INVALID",
10
+ "RESOLUTION_UNAVAILABLE",
11
+ ]);
12
+ const resolutionSchema = z.object({
13
+ flowUserId: z.string(),
14
+ did: z.string(),
15
+ handle: z.string().nullable(),
16
+ pdsUrl: z.url(),
17
+ hosting: z.enum(["flow", "external"]),
18
+ lifecycle: z.enum(["active", "suspended", "migrated", "deletion_pending"]),
19
+ authorization: z.enum(["connected", "reconnect_required"]),
20
+ resolvedAt: z.iso.datetime(),
21
+ expiresAt: z.iso.datetime(),
22
+ identityVersion: z.string(),
23
+ });
24
+ export class AtprotoRequestError extends Error {
25
+ code;
26
+ retryable;
27
+ constructor(code, retryable = false) {
28
+ super(code);
29
+ this.code = code;
30
+ this.retryable = retryable;
31
+ this.name = "AtprotoRequestError";
32
+ }
33
+ }
34
+ /** Identity changes and migration completion should invalidate before another discovery. */
35
+ export function createAtprotoApi(host, getToken, getUserId) {
36
+ let generation = 0;
37
+ let dirty = false;
38
+ return {
39
+ invalidate() {
40
+ generation++;
41
+ dirty = true;
42
+ },
43
+ async resolve(options = {}) {
44
+ options.signal?.throwIfAborted();
45
+ const userId = getUserId();
46
+ if (!userId)
47
+ throw new AtprotoRequestError("SIGN_IN_REQUIRED");
48
+ const requestGeneration = generation;
49
+ const unchanged = () => {
50
+ if (generation !== requestGeneration || getUserId() !== userId)
51
+ throw new AtprotoRequestError("ACCOUNT_CHANGED");
52
+ };
53
+ const token = await getToken();
54
+ unchanged();
55
+ const headers = new Headers({ "X-Flow-User-Id": userId });
56
+ if (token)
57
+ headers.set("Authorization", `Bearer ${token}`);
58
+ let response;
59
+ try {
60
+ response = await fetch(`${host}/api/atproto/resolve?freshness=${dirty ? "refresh" : (options.freshness ?? "cached")}`, { headers, credentials: "include", signal: options.signal });
61
+ }
62
+ catch {
63
+ options.signal?.throwIfAborted();
64
+ unchanged();
65
+ throw new AtprotoRequestError("RESOLUTION_UNAVAILABLE", true);
66
+ }
67
+ unchanged();
68
+ const body = await response.json().catch(() => null);
69
+ options.signal?.throwIfAborted();
70
+ unchanged();
71
+ if (!response.ok) {
72
+ const parsed = z.object({ code: failureSchema }).safeParse(body);
73
+ const code = parsed.success
74
+ ? parsed.data.code
75
+ : "RESOLUTION_UNAVAILABLE";
76
+ throw new AtprotoRequestError(code, ["RESOLUTION_UNAVAILABLE", "PROVISIONING_PENDING"].includes(code));
77
+ }
78
+ const parsed = resolutionSchema.safeParse(body);
79
+ if (!parsed.success)
80
+ throw new AtprotoRequestError("IDENTITY_INVALID");
81
+ if (parsed.data.flowUserId !== userId)
82
+ throw new AtprotoRequestError("ACCOUNT_CHANGED");
83
+ dirty = false;
84
+ return parsed.data;
85
+ },
86
+ };
87
+ }
@@ -1,3 +1,4 @@
1
+ import { getFlow } from "./create-flow";
1
2
  async function readResponse(response) {
2
3
  const body = await response.json();
3
4
  if (!response.ok)
@@ -17,6 +18,7 @@ export async function unlinkAtproto(expectedUserId) {
17
18
  headers: { "Content-Type": "application/json" },
18
19
  body: JSON.stringify({ expectedUserId }),
19
20
  }));
21
+ getFlow()?.atproto.invalidate();
20
22
  }
21
23
  const completionKey = "flow.atproto.completion";
22
24
  export async function connectAtproto(request, signal) {
@@ -30,6 +32,7 @@ export async function connectAtproto(request, signal) {
30
32
  }));
31
33
  signal.throwIfAborted();
32
34
  sessionStorage.setItem(completionKey, result.completionToken);
35
+ getFlow()?.atproto.invalidate();
33
36
  window.location.assign(result.url);
34
37
  }
35
38
  let completion = null;
@@ -44,6 +47,11 @@ export function completeAtproto() {
44
47
  credentials: "include",
45
48
  headers: { "Content-Type": "application/json" },
46
49
  body: JSON.stringify({ completionToken }),
47
- }).then((readResponse));
50
+ })
51
+ .then((readResponse))
52
+ .then((result) => {
53
+ getFlow()?.atproto.invalidate();
54
+ return result;
55
+ });
48
56
  return completion;
49
57
  }
@@ -1,6 +1,7 @@
1
1
  import { BROWSER_FLOW_TTL_MS } from "../browser-contract";
2
2
  import { DEFAULT_SESSION_PATH, isLocalHostname, resolveIdHost, } from "../id-host";
3
3
  import { isExpiring } from "../token-expiry";
4
+ import { createAtprotoApi } from "./atproto-resolver";
4
5
  import { createDialogHost } from "./dialog-host";
5
6
  import { idb } from "./idb";
6
7
  import { createRoomsApi } from "./rooms";
@@ -510,6 +511,14 @@ export function createFlow(options = {}) {
510
511
  };
511
512
  window.addEventListener("focus", refreshOnFocus);
512
513
  document.addEventListener("visibilitychange", refreshOnFocus);
514
+ const atproto = createAtprotoApi(host, getToken, () => store.getSnapshot().user?.id ?? null);
515
+ let resolverUserId = store.getSnapshot().user?.id ?? null;
516
+ store.subscribe((state) => {
517
+ const userId = state.user?.id ?? null;
518
+ if (userId !== resolverUserId)
519
+ atproto.invalidate();
520
+ resolverUserId = userId;
521
+ });
513
522
  const flow = {
514
523
  get user() {
515
524
  return store.getSnapshot().user;
@@ -541,6 +550,7 @@ export function createFlow(options = {}) {
541
550
  getToken,
542
551
  mintAudience,
543
552
  rooms: createRoomsApi(host, getToken),
553
+ atproto,
544
554
  signMessage: async (args) => {
545
555
  const mod = await import("./signing");
546
556
  return mod.signMessage(buildSigningContext(), args);
@@ -1,16 +1,21 @@
1
1
  export { defaultIdHost, isLocalHostname } from "../id-host";
2
- export type { AccessKeyOptions, ActionCatalogEntry, ActionCatalogResponse, ActionCurve, ActionEventRequest, ActionEventResponse, ActionSubjectRef, ActionSurface, ActionsApi, ActionTransitionKind, ActiveAction, ActiveActionResponse, ActiveSessionConflict, AdditionalSession, Address, BrowserAccount, BrowserAccounts, CatalogFieldRef, ClockAnchor, ConnectCapabilities, ConnectResponse, CreateFlowOptions, CreateStudySubjectInput, CurrentAction, DialogHost, FinishActionRequest, FinishActionResponse, FinishedSummary, Flow, FlowCredential, FlowSessionState, FlowState, FlowUser, FlowWidgetHandle, FlowWidgetName, FocusApi, FocusPhase, FocusSessionEvent, FocusSessionState, ListStudySessionsOptions, ListStudySubjectsOptions, LoginOptions, MarkNotificationsReadInput, MethodName, MountFlowWidgetOptions, MountProfileOptions, NotificationCounts, NotificationEntry, NotificationPage, NotificationReadResult, NotificationRefusal, OpenProfileOptions, ProfileButtonHandle, ProfilePosition, RoomMemberEntry, Session, StartActionInput, StartActionResponse, StartActionResult, StartFocusRequest, StudyApi, StudyCatalog, StudyCategory, StudyCategoryTotals, StudyField, StudyRange, StudySearchHit, StudySessionEntry, StudySessionPage, StudySubject, StudySubjectList, StudySubjectRefusal, StudySubjectRemovalResponse, StudySubjectResponse, StudySubjectTotals, StudySummary, SwitchAccountOptions, UpdateStudySubjectInput, UserActionSurface, XpEstimateContext, XpMode, } from "../types";
2
+ export type { AccessKeyOptions, ActionCatalogEntry, ActionCatalogResponse, ActionCurve, ActionEventRequest, ActionEventResponse, ActionSubjectRef, ActionSurface, ActionsApi, ActionTransitionKind, ActiveAction, ActiveActionResponse, ActiveSessionConflict, AdditionalSession, Address, AtprotoApi, AtprotoResolution, AtprotoResolutionFailure, AtprotoResolveOptions, BrowserAccount, BrowserAccounts, CatalogFieldRef, ClockAnchor, ConnectCapabilities, ConnectResponse, CreateFlowOptions, CreateStudySubjectInput, CurrentAction, DialogHost, FinishActionRequest, FinishActionResponse, FinishedSummary, Flow, FlowCredential, FlowSessionState, FlowState, FlowUser, FlowWidgetHandle, FlowWidgetName, FocusApi, FocusPhase, FocusSessionEvent, FocusSessionState, ListStudySessionsOptions, ListStudySubjectsOptions, LoginOptions, MarkNotificationsReadInput, MethodName, MountFlowWidgetOptions, MountProfileOptions, NotificationCounts, NotificationEntry, NotificationPage, NotificationReadResult, NotificationRefusal, OpenProfileOptions, PasskeyPrfOptions, ProfileButtonHandle, ProfilePosition, RoomMemberEntry, Session, StartActionInput, StartActionResponse, StartActionResult, StartFocusRequest, StudyApi, StudyCatalog, StudyCategory, StudyCategoryTotals, StudyField, StudyRange, StudySearchHit, StudySessionEntry, StudySessionPage, StudySubject, StudySubjectList, StudySubjectRefusal, StudySubjectRemovalResponse, StudySubjectResponse, StudySubjectTotals, StudySummary, SwitchAccountOptions, UpdateStudySubjectInput, UserActionSurface, XpEstimateContext, XpMode, } from "../types";
3
+ export type { NoteBlob, NoteCollection, NoteOperation, NoteRecord, NoteResponses, NoteStoredRecord, NoteVaultHeader, } from "../types/note-transport";
3
4
  export { consistencyCurve, estimateActionXp, FLOW_SCORE_MAX, flowMultiplier, } from "../xp/curve";
4
5
  export { ActionsRequestError, createActionsApi, isActiveSessionConflict, } from "./actions";
6
+ export { AtprotoRequestError, createAtprotoApi } from "./atproto-resolver";
5
7
  export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
6
8
  export { createDialogHost } from "./dialog-host";
7
9
  export { createFlowWidget } from "./flow-widget";
8
10
  export { actionHeadline, anchorFor, catalogFieldIndex, createFocusApi, elapsedSeconds, fieldsCoveredBySubjects, focusSessionReducer, formatElapsed, hitLabel, INITIAL_FOCUS_STATE, MEDITATION_SOURCE, matchScore, normalizeSearchText, REANCHOR_TOLERANCE_SECONDS, recentSubjects, STUDY_SOURCE, searchStudy, shouldReanchor, } from "./focus";
9
11
  export { METHODS } from "./methods";
12
+ export { createNoteTransport, NoteTransportError } from "./note-transport";
10
13
  export { closeProfile, openProfile } from "./open-profile";
14
+ export { evaluatePasskeyPrf, PasskeyPrfError } from "./passkey-prf";
11
15
  export { createProfileButton } from "./profile-button";
12
16
  export type { ReportOperation, ReportRequest, ReportResponse } from "./reports";
13
17
  export { createReportsApi, ReportRequestError } from "./reports";
14
18
  export { createRoomsApi, RoomsRequestError } from "./rooms";
19
+ export { getConnectedPasskey } from "./session";
15
20
  export { createStaticFlow } from "./static-flow";
16
21
  export { createStudyApi, StudyRequestError } from "./study";
@@ -1,14 +1,18 @@
1
1
  export { defaultIdHost, isLocalHostname } from "../id-host";
2
2
  export { consistencyCurve, estimateActionXp, FLOW_SCORE_MAX, flowMultiplier, } from "../xp/curve";
3
3
  export { ActionsRequestError, createActionsApi, isActiveSessionConflict, } from "./actions";
4
+ export { AtprotoRequestError, createAtprotoApi } from "./atproto-resolver";
4
5
  export { createFlow, getFlow, requireFlow, resetFlow } from "./create-flow";
5
6
  export { createDialogHost } from "./dialog-host";
6
7
  export { createFlowWidget } from "./flow-widget";
7
8
  export { actionHeadline, anchorFor, catalogFieldIndex, createFocusApi, elapsedSeconds, fieldsCoveredBySubjects, focusSessionReducer, formatElapsed, hitLabel, INITIAL_FOCUS_STATE, MEDITATION_SOURCE, matchScore, normalizeSearchText, REANCHOR_TOLERANCE_SECONDS, recentSubjects, STUDY_SOURCE, searchStudy, shouldReanchor, } from "./focus";
8
9
  export { METHODS } from "./methods";
10
+ export { createNoteTransport, NoteTransportError } from "./note-transport";
9
11
  export { closeProfile, openProfile } from "./open-profile";
12
+ export { evaluatePasskeyPrf, PasskeyPrfError } from "./passkey-prf";
10
13
  export { createProfileButton } from "./profile-button";
11
14
  export { createReportsApi, ReportRequestError } from "./reports";
12
15
  export { createRoomsApi, RoomsRequestError } from "./rooms";
16
+ export { getConnectedPasskey } from "./session";
13
17
  export { createStaticFlow } from "./static-flow";
14
18
  export { createStudyApi, StudyRequestError } from "./study";
@@ -0,0 +1,15 @@
1
+ import type { AtprotoApi, AtprotoResolution } from "../types/atproto";
2
+ import type { NoteBlob, NoteOperation, NoteResponses } from "../types/note-transport";
3
+ export declare class NoteTransportError extends Error {
4
+ readonly code: string;
5
+ readonly status: number;
6
+ constructor(code: string, status: number);
7
+ }
8
+ /** Ciphertext-only Auth transport; the browser never receives provider tokens or DPoP keys. */
9
+ export declare function createNoteTransport(host: string, getToken: () => Promise<string | null>, getUserId: () => string | null, getSessionVersion: () => number, resolver: AtprotoApi, expectedIdentity: Pick<AtprotoResolution, "did" | "identityVersion">): {
10
+ records<T extends NoteOperation>(operation: T, signal?: AbortSignal): Promise<NoteResponses[T["method"]]>;
11
+ download(cid: string, signal?: AbortSignal): Promise<Uint8Array<ArrayBuffer>>;
12
+ upload(bytes: Uint8Array<ArrayBuffer>, signal?: AbortSignal): Promise<{
13
+ blob: NoteBlob;
14
+ }>;
15
+ };
@@ -0,0 +1,104 @@
1
+ import { z } from "zod";
2
+ export class NoteTransportError extends Error {
3
+ code;
4
+ status;
5
+ constructor(code, status) {
6
+ super(code);
7
+ this.code = code;
8
+ this.status = status;
9
+ this.name = "NoteTransportError";
10
+ }
11
+ }
12
+ /** Ciphertext-only Auth transport; the browser never receives provider tokens or DPoP keys. */
13
+ export function createNoteTransport(host, getToken, getUserId, getSessionVersion, resolver, expectedIdentity) {
14
+ const pinnedIdentity = { ...expectedIdentity };
15
+ async function request(action, body, signal) {
16
+ signal?.throwIfAborted();
17
+ const userId = getUserId();
18
+ const version = getSessionVersion();
19
+ if (!userId)
20
+ throw new NoteTransportError("SIGN_IN_REQUIRED", 401);
21
+ const unchanged = () => {
22
+ if (getUserId() !== userId || getSessionVersion() !== version)
23
+ throw new NoteTransportError("ACCOUNT_CHANGED", 409);
24
+ };
25
+ const resolution = await resolver.resolve({ freshness: "refresh", signal });
26
+ unchanged();
27
+ if (resolution.did !== pinnedIdentity.did ||
28
+ resolution.identityVersion !== pinnedIdentity.identityVersion) {
29
+ resolver.invalidate();
30
+ throw new NoteTransportError("ACCOUNT_CHANGED", 409);
31
+ }
32
+ if (resolution.lifecycle !== "active" ||
33
+ resolution.authorization !== "connected")
34
+ throw new NoteTransportError("RECONNECT_REQUIRED", 403);
35
+ const token = await getToken();
36
+ unchanged();
37
+ if (!token)
38
+ throw new NoteTransportError("SIGN_IN_REQUIRED", 401);
39
+ let response;
40
+ try {
41
+ response = await fetch(`${host}/api/atproto/note/${action}`, {
42
+ method: "POST",
43
+ headers: {
44
+ Authorization: `Bearer ${token}`,
45
+ "X-Flow-User-Id": userId,
46
+ "X-Flow-At-Did": pinnedIdentity.did,
47
+ "X-Flow-Identity-Version": pinnedIdentity.identityVersion,
48
+ "Content-Type": action === "upload"
49
+ ? "application/octet-stream"
50
+ : "application/json",
51
+ },
52
+ body,
53
+ signal,
54
+ });
55
+ }
56
+ catch {
57
+ signal?.throwIfAborted();
58
+ unchanged();
59
+ throw new NoteTransportError("PDS_UNAVAILABLE", 503);
60
+ }
61
+ const data = action === "download" && response.ok
62
+ ? new Uint8Array(await response.arrayBuffer())
63
+ : await response.json().catch(() => null);
64
+ signal?.throwIfAborted();
65
+ unchanged();
66
+ if (!response.ok) {
67
+ const parsed = z.object({ code: z.string() }).safeParse(data);
68
+ const code = parsed.success ? parsed.data.code : "PDS_UNAVAILABLE";
69
+ if (code === "ACCOUNT_CHANGED" || code === "RECONNECT_REQUIRED")
70
+ resolver.invalidate();
71
+ throw new NoteTransportError(code, response.status);
72
+ }
73
+ const after = await resolver.resolve({ freshness: "refresh", signal });
74
+ unchanged();
75
+ if (after.did !== pinnedIdentity.did ||
76
+ after.identityVersion !== pinnedIdentity.identityVersion) {
77
+ resolver.invalidate();
78
+ throw new NoteTransportError("ACCOUNT_CHANGED", 409);
79
+ }
80
+ if (data instanceof Uint8Array)
81
+ return new Uint8Array(data);
82
+ const parsed = z.record(z.string(), z.unknown()).safeParse(data);
83
+ if (!parsed.success)
84
+ throw new NoteTransportError("INVALID_RESPONSE", 502);
85
+ // SAFETY: Auth validates method-specific ciphertext schemas and strips provider secrets; the SDK preserves those named contracts.
86
+ return parsed.data;
87
+ }
88
+ return {
89
+ async records(operation, signal) {
90
+ // SAFETY: Auth validates the selected operation's named response schema.
91
+ return (await request("records", JSON.stringify(operation), signal));
92
+ },
93
+ async download(cid, signal) {
94
+ const result = await request("download", JSON.stringify({ cid }), signal);
95
+ if (!(result instanceof Uint8Array))
96
+ throw new NoteTransportError("INVALID_RESPONSE", 502);
97
+ return result;
98
+ },
99
+ async upload(bytes, signal) {
100
+ // SAFETY: Auth validates the upload response as a single standard octet-stream blob reference.
101
+ return (await request("upload", bytes, signal));
102
+ },
103
+ };
104
+ }
@@ -0,0 +1,8 @@
1
+ import type { FlowCredential } from "../types/auth";
2
+ import type { PasskeyPrfOptions } from "../types/sdk";
3
+ export declare class PasskeyPrfError extends Error {
4
+ readonly code: "PASSKEY_REQUIRED" | "PRF_UNAVAILABLE" | "ACCOUNT_CHANGED" | "INVALID_INPUT";
5
+ constructor(code: "PASSKEY_REQUIRED" | "PRF_UNAVAILABLE" | "ACCOUNT_CHANGED" | "INVALID_INPUT");
6
+ }
7
+ /** Evaluate only the connected passkey. Output is client-only key material: never serialize or log it. */
8
+ export declare function evaluatePasskeyPrf(getCredential: () => FlowCredential | null, getUserId: () => string | null, getSessionVersion: () => number, options: PasskeyPrfOptions): Promise<Uint8Array<ArrayBuffer>>;
@@ -0,0 +1,67 @@
1
+ export class PasskeyPrfError extends Error {
2
+ code;
3
+ constructor(code) {
4
+ super(code);
5
+ this.code = code;
6
+ this.name = "PasskeyPrfError";
7
+ }
8
+ }
9
+ /** Evaluate only the connected passkey. Output is client-only key material: never serialize or log it. */
10
+ export async function evaluatePasskeyPrf(getCredential, getUserId, getSessionVersion, options) {
11
+ options.signal?.throwIfAborted();
12
+ const credential = getCredential();
13
+ const userId = getUserId();
14
+ const sessionVersion = getSessionVersion();
15
+ if (!credential || !userId)
16
+ throw new PasskeyPrfError("PASSKEY_REQUIRED");
17
+ if (options.input.byteLength !== 32 || !options.rpId)
18
+ throw new PasskeyPrfError("INVALID_INPUT");
19
+ let id;
20
+ try {
21
+ if (!/^[A-Za-z0-9_-]+$/.test(credential.id))
22
+ throw new Error();
23
+ id = Uint8Array.from(atob(credential.id.replace(/-/g, "+").replace(/_/g, "/")), (character) => character.charCodeAt(0));
24
+ }
25
+ catch {
26
+ throw new PasskeyPrfError("INVALID_INPUT");
27
+ }
28
+ const unchanged = () => {
29
+ if (getSessionVersion() !== sessionVersion ||
30
+ getUserId() !== userId ||
31
+ getCredential()?.id !== credential.id)
32
+ throw new PasskeyPrfError("ACCOUNT_CHANGED");
33
+ };
34
+ if (!globalThis.navigator?.credentials || !globalThis.PublicKeyCredential)
35
+ throw new PasskeyPrfError("PRF_UNAVAILABLE");
36
+ let assertion;
37
+ try {
38
+ assertion = await navigator.credentials.get({
39
+ publicKey: {
40
+ challenge: crypto.getRandomValues(new Uint8Array(32)),
41
+ rpId: options.rpId,
42
+ allowCredentials: [{ type: "public-key", id }],
43
+ userVerification: "required",
44
+ timeout: 60_000,
45
+ extensions: { prf: { eval: { first: options.input.slice().buffer } } },
46
+ },
47
+ signal: options.signal,
48
+ });
49
+ }
50
+ catch {
51
+ options.signal?.throwIfAborted();
52
+ unchanged();
53
+ throw new PasskeyPrfError("PRF_UNAVAILABLE");
54
+ }
55
+ options.signal?.throwIfAborted();
56
+ unchanged();
57
+ if (!(assertion instanceof PublicKeyCredential))
58
+ throw new PasskeyPrfError("PRF_UNAVAILABLE");
59
+ const actualId = new Uint8Array(assertion.rawId);
60
+ if (actualId.length !== id.length ||
61
+ actualId.some((byte, i) => byte !== id[i]))
62
+ throw new PasskeyPrfError("PRF_UNAVAILABLE");
63
+ const result = assertion.getClientExtensionResults().prf?.results?.first;
64
+ if (!(result instanceof ArrayBuffer) || result.byteLength !== 32)
65
+ throw new PasskeyPrfError("PRF_UNAVAILABLE");
66
+ return new Uint8Array(result.slice(0));
67
+ }