@flow-industries/id 0.23.4 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/sdk/browser-contract.d.ts +1 -0
- package/dist/sdk/browser-contract.js +1 -0
- package/dist/sdk/browser-session-route.js +3 -3
- package/dist/sdk/client/atproto.d.ts +5 -0
- package/dist/sdk/client/atproto.js +49 -0
- package/dist/sdk/client/create-flow.js +2 -1
- package/dist/sdk/db/handle.d.ts +11 -0
- package/dist/sdk/db/handle.js +0 -0
- package/dist/sdk/db/schema.d.ts +4626 -0
- package/dist/sdk/db/schema.js +782 -0
- package/dist/sdk/oauth/encryption.d.ts +4 -0
- package/dist/sdk/oauth/encryption.js +29 -0
- package/dist/sdk/oauth/stores.d.ts +16 -0
- package/dist/sdk/oauth/stores.js +159 -0
- package/dist/sdk/settings/appearance.d.ts +3 -0
- package/dist/sdk/settings/appearance.js +167 -0
- package/dist/sdk/settings/cosmetics.d.ts +149 -0
- package/dist/sdk/settings/cosmetics.js +408 -0
- package/dist/sdk/settings/hair-items.json +312 -0
- package/dist/sdk/settings/registry.d.ts +11 -0
- package/dist/sdk/settings/registry.js +136 -0
- package/dist/sdk/settings/surfaces.d.ts +4 -0
- package/dist/sdk/settings/surfaces.js +37 -0
- package/dist/sdk/types/account-data.d.ts +11 -0
- package/dist/sdk/types/atproto-storage.d.ts +9 -0
- package/dist/sdk/types/atproto-storage.js +0 -0
- package/dist/sdk/types/atproto.d.ts +25 -0
- package/dist/sdk/types/atproto.js +0 -0
- package/dist/sdk/types/auth.d.ts +1 -0
- package/dist/sdk/types/cosmetics.d.ts +2 -1
- package/dist/sdk/types/index.d.ts +1 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -21,6 +21,82 @@ rotated so local session creation keeps working after environment changes.
|
|
|
21
21
|
It also refuses to start when migrations fail instead of serving against a
|
|
22
22
|
partially migrated schema.
|
|
23
23
|
|
|
24
|
+
### AT Protocol identity
|
|
25
|
+
|
|
26
|
+
Bluesky and other AT Protocol accounts can sign in to Flow ID or connect to an
|
|
27
|
+
existing account. Flow user IDs remain stable; the provider DID identifies the
|
|
28
|
+
connection. New accounts do not need an email, passkey, or wallet to enroll.
|
|
29
|
+
|
|
30
|
+
Set `ATPROTO_PUBLIC_URL`, `ATPROTO_PRIVATE_KEY` (an ES256 private JWK with a stable
|
|
31
|
+
`kid`), and `ATPROTO_STORAGE_KEY` (a base64-encoded 32-byte encryption key) to
|
|
32
|
+
enable the integration. Leave all three unset to disable it. Production uses
|
|
33
|
+
HTTPS client metadata at `/api/auth/atproto/client-metadata.json` and public keys
|
|
34
|
+
at `/api/auth/atproto/jwks.json`. Provider tokens and DPoP keys are encrypted in
|
|
35
|
+
Postgres and stay on the server. Back up the storage key with the database;
|
|
36
|
+
changing it requires re-encrypting stored credentials.
|
|
37
|
+
|
|
38
|
+
For local OAuth, set `ATPROTO_PUBLIC_URL=http://127.0.0.1:8183` and launch
|
|
39
|
+
`./dev.sh 8180`. The hosted sign-in page uses `http://localhost:8183` and the
|
|
40
|
+
launcher sets the WebAuthn relying party to `localhost`. Use the printed URLs;
|
|
41
|
+
existing passkeys remain bound to their original relying party.
|
|
42
|
+
|
|
43
|
+
Without an email provider configured, the local launcher also prints sign-in
|
|
44
|
+
codes in the browser console. This requires development mode, the launcher's
|
|
45
|
+
`FLOW_DEV_OTP_CONSOLE=1` opt-in, and a loopback issuer; the API then binds only to
|
|
46
|
+
`127.0.0.1`. Production responses never include these codes.
|
|
47
|
+
|
|
48
|
+
AT Protocol requires an IP loopback callback URI. The development landing server
|
|
49
|
+
redirects loopback GET requests to its fixed localhost origin before handling
|
|
50
|
+
them, retaining the callback query so OAuth resumes with the same browser cookies
|
|
51
|
+
and session storage as sign-in. The SDK still exchanges the code using the
|
|
52
|
+
registered IP callback URI. If that port block is occupied, update
|
|
53
|
+
`ATPROTO_PUBLIC_URL` to the selected landing port and restart. Production clients
|
|
54
|
+
use HTTPS metadata and authenticate with the configured signing key.
|
|
55
|
+
|
|
56
|
+
Provider sign-in resumes the bound `/authorize` transaction. Flow installs a
|
|
57
|
+
first-party session, then returns the existing one-use PKCE code to the app.
|
|
58
|
+
Account linking returns to `/account`; no provider or Flow tokens travel in
|
|
59
|
+
redirect URLs or window messages.
|
|
60
|
+
|
|
61
|
+
The account connection view reads saved identities and credentials locally. It
|
|
62
|
+
does not refresh provider tokens or resolve handles on page load; reconnect
|
|
63
|
+
performs a new provider authorization when access needs to be restored.
|
|
64
|
+
|
|
65
|
+
### Connected accounts
|
|
66
|
+
|
|
67
|
+
Better Auth owns the shared `account` model. External identities are keyed by
|
|
68
|
+
`providerId` and `accountId`, and link to the canonical Flow `userId`. For AT
|
|
69
|
+
Protocol these values are `atproto` and the verified DID. Handles are display
|
|
70
|
+
metadata, never an identity key. A Flow account can connect different providers
|
|
71
|
+
without adding provider-specific identity tables.
|
|
72
|
+
Provider IDs identify a trusted provider configuration: separate OIDC issuers
|
|
73
|
+
must use separate IDs, even if both run the same identity-server software.
|
|
74
|
+
|
|
75
|
+
Provider credentials and temporary authorization state live in the encrypted,
|
|
76
|
+
provider-scoped `oauth_session` and `oauth_state` stores. The AT SDK uses an
|
|
77
|
+
adapter over those stores; its DPoP and discovery behavior stays within the AT
|
|
78
|
+
integration. Provider tokens never become Flow session tokens.
|
|
79
|
+
|
|
80
|
+
Better Auth supports built-in social providers and its Generic OAuth plugin.
|
|
81
|
+
Flow currently uses version 1.4.17, whose default OAuth enrollment requires an
|
|
82
|
+
email and creates its own sessions. A new provider must supply a verified stable
|
|
83
|
+
subject to Flow's shared account enrollment, then use Flow's audience-bound
|
|
84
|
+
session issuance. Register usable sign-in providers explicitly; arbitrary
|
|
85
|
+
account rows do not count as recovery methods. No new identity table is needed.
|
|
86
|
+
|
|
87
|
+
Automatic email-based account linking is disabled. Disconnect through Flow's
|
|
88
|
+
account controls so credential cleanup and last-sign-in-method checks run in
|
|
89
|
+
one transaction; the stock Better Auth unlink endpoint is blocked. Its account
|
|
90
|
+
listing remains available and excludes credentials.
|
|
91
|
+
|
|
92
|
+
Migration `0040` preserves existing AT links and encrypted sessions, then removes
|
|
93
|
+
the earlier AT-only tables. It invalidates in-flight authorization state, so
|
|
94
|
+
unfinished attempts must restart. Deploy the complete revised Auth stack before
|
|
95
|
+
enabling AT production configuration. After this migration, the intermediate
|
|
96
|
+
AT-only builds from PRs #170–#172 are not rollback targets; use the pre-AT main
|
|
97
|
+
build or a compatible forward fix. Existing pre-AT Flow user/session writes
|
|
98
|
+
remain supported.
|
|
99
|
+
|
|
24
100
|
**npm:** [`@flow-industries/id`](https://www.npmjs.com/package/@flow-industries/id)
|
|
25
101
|
|
|
26
102
|
## Installation
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { browserSigningSchema } from "./browser-contract";
|
|
2
|
+
import { BROWSER_FLOW_TTL_MS, browserSigningSchema } from "./browser-contract";
|
|
3
3
|
import { clearCookieString, cookieNamesFor, parseCookieHeader, serializeCookie, } from "./cookies";
|
|
4
4
|
import { fetchIssuerApi, resolveIssuerApiUrl } from "./issuer-api";
|
|
5
5
|
import { claimsToUser, sessionCookies } from "./session-core";
|
|
@@ -110,14 +110,14 @@ export async function startBrowserSession(request, audience, issuerUrl, body, ap
|
|
|
110
110
|
state,
|
|
111
111
|
verifier,
|
|
112
112
|
returnTo,
|
|
113
|
-
expires: Date.now() +
|
|
113
|
+
expires: Date.now() + BROWSER_FLOW_TTL_MS,
|
|
114
114
|
})));
|
|
115
115
|
return reply({
|
|
116
116
|
loginId: state,
|
|
117
117
|
authorizeUrl: `${issuerUrl}/authorize?transaction=${transaction}`,
|
|
118
118
|
}, 200, [
|
|
119
119
|
serializeCookie(cookieName(audience, state), pending, {
|
|
120
|
-
maxAge:
|
|
120
|
+
maxAge: BROWSER_FLOW_TTL_MS / 1000,
|
|
121
121
|
secure: audience.startsWith("https:"),
|
|
122
122
|
}),
|
|
123
123
|
]);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AtprotoCompletion, AtprotoStartRequest, AtprotoStatus } from "../types";
|
|
2
|
+
export declare function getAtprotoStatus(signal?: AbortSignal): Promise<AtprotoStatus>;
|
|
3
|
+
export declare function unlinkAtproto(expectedUserId: string): Promise<void>;
|
|
4
|
+
export declare function connectAtproto(request: AtprotoStartRequest, signal: AbortSignal): Promise<void>;
|
|
5
|
+
export declare function completeAtproto(): Promise<AtprotoCompletion>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
async function readResponse(response) {
|
|
2
|
+
const body = await response.json();
|
|
3
|
+
if (!response.ok)
|
|
4
|
+
throw new Error(body.message ??
|
|
5
|
+
body.error ??
|
|
6
|
+
"Could not connect your AT Protocol account.");
|
|
7
|
+
// SAFETY: the caller names the payload returned by the same-origin Flow endpoint.
|
|
8
|
+
return body;
|
|
9
|
+
}
|
|
10
|
+
export async function getAtprotoStatus(signal) {
|
|
11
|
+
return readResponse(await fetch("/api/auth/atproto/status", { credentials: "include", signal }));
|
|
12
|
+
}
|
|
13
|
+
export async function unlinkAtproto(expectedUserId) {
|
|
14
|
+
await readResponse(await fetch("/api/auth/atproto/unlink", {
|
|
15
|
+
method: "POST",
|
|
16
|
+
credentials: "include",
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
body: JSON.stringify({ expectedUserId }),
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
const completionKey = "flow.atproto.completion";
|
|
22
|
+
export async function connectAtproto(request, signal) {
|
|
23
|
+
signal.throwIfAborted();
|
|
24
|
+
const result = await readResponse(await fetch("/api/auth/atproto/start", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
credentials: "include",
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
body: JSON.stringify(request),
|
|
29
|
+
signal,
|
|
30
|
+
}));
|
|
31
|
+
signal.throwIfAborted();
|
|
32
|
+
sessionStorage.setItem(completionKey, result.completionToken);
|
|
33
|
+
window.location.assign(result.url);
|
|
34
|
+
}
|
|
35
|
+
let completion = null;
|
|
36
|
+
export function completeAtproto() {
|
|
37
|
+
if (completion)
|
|
38
|
+
return completion;
|
|
39
|
+
const completionToken = sessionStorage.getItem(completionKey);
|
|
40
|
+
if (!completionToken)
|
|
41
|
+
return Promise.reject(new Error("Sign-in expired. Please try again."));
|
|
42
|
+
completion = fetch("/api/auth/atproto/complete", {
|
|
43
|
+
method: "POST",
|
|
44
|
+
credentials: "include",
|
|
45
|
+
headers: { "Content-Type": "application/json" },
|
|
46
|
+
body: JSON.stringify({ completionToken }),
|
|
47
|
+
}).then((readResponse));
|
|
48
|
+
return completion;
|
|
49
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BROWSER_FLOW_TTL_MS } from "../browser-contract";
|
|
1
2
|
import { DEFAULT_SESSION_PATH, isLocalHostname, resolveIdHost, } from "../id-host";
|
|
2
3
|
import { isExpiring } from "../token-expiry";
|
|
3
4
|
import { createDialogHost } from "./dialog-host";
|
|
@@ -385,7 +386,7 @@ export function createFlow(options = {}) {
|
|
|
385
386
|
if (preparation)
|
|
386
387
|
await idb.set(`flow.pendingAccessKey.${result.loginId}`, {
|
|
387
388
|
preparation,
|
|
388
|
-
expires: Date.now() +
|
|
389
|
+
expires: Date.now() + BROWSER_FLOW_TTL_MS,
|
|
389
390
|
});
|
|
390
391
|
window.location.assign(result.authorizeUrl);
|
|
391
392
|
return new Promise((_resolve, reject) => {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
2
|
+
import type * as schema from "./schema";
|
|
3
|
+
/**
|
|
4
|
+
* Any pg-dialect drizzle handle over the app schema — postgres-js in the
|
|
5
|
+
* server, PGlite in the tests.
|
|
6
|
+
*
|
|
7
|
+
* Core logic takes this as a parameter instead of importing the `db` singleton,
|
|
8
|
+
* which requires DATABASE_URL at import time and so cannot be exercised without
|
|
9
|
+
* a Postgres server. Type-only, so importing it never pulls the singleton in.
|
|
10
|
+
*/
|
|
11
|
+
export type AppDb = PgDatabase<PgQueryResultHKT, typeof schema>;
|
|
File without changes
|