@flow-industries/id 0.24.5 → 0.26.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/README.md +3 -1
- package/contracts/v1/sdk-exports.json +15 -0
- package/dist/sdk/browser-session-route.d.ts +3 -1
- package/dist/sdk/browser-session-route.js +88 -54
- package/dist/sdk/client/create-flow.js +99 -21
- package/dist/sdk/client/index.d.ts +1 -1
- package/dist/sdk/client/profile-button.d.ts +1 -1
- package/dist/sdk/client/profile-button.js +29 -174
- package/dist/sdk/client/session.js +1 -1
- package/dist/sdk/client/static-flow.js +3 -0
- package/dist/sdk/db/schema.d.ts +167 -0
- package/dist/sdk/db/schema.js +19 -0
- package/dist/sdk/profile.css +2 -0
- package/dist/sdk/react/account-menu.d.ts +4 -0
- package/dist/sdk/react/account-menu.js +82 -0
- package/dist/sdk/react/profile-button.d.ts +5 -3
- package/dist/sdk/react/profile-button.js +51 -25
- package/dist/sdk/react/profile-renderer.d.ts +2 -0
- package/dist/sdk/react/profile-renderer.js +19 -0
- package/dist/sdk/react/profile-theme.d.ts +3 -0
- package/dist/sdk/react/profile-theme.js +19 -0
- package/dist/sdk/session-core.d.ts +1 -1
- package/dist/sdk/session-core.js +2 -1
- package/dist/sdk/session-route.js +50 -3
- package/dist/sdk/types/auth.d.ts +2 -0
- package/dist/sdk/types/browser-accounts.d.ts +9 -0
- package/dist/sdk/types/browser-accounts.js +0 -0
- package/dist/sdk/types/index.d.ts +2 -1
- package/dist/sdk/types/sdk.d.ts +25 -1
- package/package.json +15 -6
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { resolveIdHost } from "../id-host";
|
|
2
1
|
import { getFlow, requireFlow } from "./create-flow";
|
|
3
2
|
const POSITION_STYLE = {
|
|
4
3
|
"top-right": { top: "0", right: "0" },
|
|
@@ -6,194 +5,50 @@ const POSITION_STYLE = {
|
|
|
6
5
|
"bottom-right": { bottom: "0", right: "0" },
|
|
7
6
|
"bottom-left": { bottom: "0", left: "0" },
|
|
8
7
|
};
|
|
9
|
-
/** Mount
|
|
8
|
+
/** Mount the shared profile menu. Import @flow-industries/id/profile.css in non-Tailwind apps. */
|
|
10
9
|
export function createProfileButton(options) {
|
|
11
10
|
if (!("document" in globalThis))
|
|
12
11
|
return { setTheme() { }, destroy() { } };
|
|
13
12
|
const flow = options.flow ?? getFlow() ?? requireFlow();
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
: document.createElement("div");
|
|
18
|
-
const container = options.container ?? createdContainer;
|
|
19
|
-
if (!container)
|
|
20
|
-
return { setTheme() { }, destroy() { } };
|
|
21
|
-
if (createdContainer) {
|
|
22
|
-
Object.assign(createdContainer.style, {
|
|
13
|
+
const container = options.container ?? document.createElement("div");
|
|
14
|
+
if (!options.container) {
|
|
15
|
+
Object.assign(container.style, {
|
|
23
16
|
position: "fixed",
|
|
24
17
|
zIndex: "2147483000",
|
|
25
18
|
padding: options.padding ?? "1.5rem",
|
|
26
19
|
...POSITION_STYLE[options.position ?? "top-right"],
|
|
27
20
|
});
|
|
28
|
-
document.body.appendChild(
|
|
29
|
-
}
|
|
30
|
-
const button = document.createElement("button");
|
|
31
|
-
button.type = "button";
|
|
32
|
-
button.dataset.flowProfile = "";
|
|
33
|
-
Object.assign(button.style, {
|
|
34
|
-
display: "flex",
|
|
35
|
-
boxSizing: "border-box",
|
|
36
|
-
height: "var(--flow-profile-height, 2.25rem)",
|
|
37
|
-
flexShrink: "0",
|
|
38
|
-
alignItems: "center",
|
|
39
|
-
gap: "8px",
|
|
40
|
-
border: "0",
|
|
41
|
-
borderRadius: "0",
|
|
42
|
-
padding: "0.25rem 0.75rem",
|
|
43
|
-
background: "transparent",
|
|
44
|
-
color: "CanvasText",
|
|
45
|
-
font: "inherit",
|
|
46
|
-
fontSize: "0.875rem",
|
|
47
|
-
lineHeight: "1.25rem",
|
|
48
|
-
cursor: "pointer",
|
|
49
|
-
pointerEvents: "auto",
|
|
50
|
-
maxWidth: "240px",
|
|
51
|
-
overflow: "hidden",
|
|
52
|
-
textOverflow: "ellipsis",
|
|
53
|
-
whiteSpace: "nowrap",
|
|
54
|
-
});
|
|
55
|
-
const notice = document.createElement("span");
|
|
56
|
-
notice.setAttribute("role", "status");
|
|
57
|
-
Object.assign(notice.style, {
|
|
58
|
-
display: "block",
|
|
59
|
-
font: "12px sans-serif",
|
|
60
|
-
maxWidth: "240px",
|
|
61
|
-
});
|
|
62
|
-
const avatar = document.createElement("span");
|
|
63
|
-
Object.assign(avatar.style, {
|
|
64
|
-
display: "flex",
|
|
65
|
-
alignItems: "center",
|
|
66
|
-
justifyContent: "center",
|
|
67
|
-
width: "calc(var(--flow-profile-height, 2.25rem) - 0.5rem)",
|
|
68
|
-
height: "calc(var(--flow-profile-height, 2.25rem) - 0.5rem)",
|
|
69
|
-
borderRadius: "var(--squircle-radius, 35%)",
|
|
70
|
-
// biome-ignore lint/plugin: CSSStyleDeclaration requires the browser's property name.
|
|
71
|
-
cornerShape: "superellipse(1.6)",
|
|
72
|
-
overflow: "hidden",
|
|
73
|
-
background: "color-mix(in srgb, currentColor 10%, transparent)",
|
|
74
|
-
flexShrink: "0",
|
|
75
|
-
});
|
|
76
|
-
const name = document.createElement("span");
|
|
77
|
-
Object.assign(name.style, {
|
|
78
|
-
minWidth: "0",
|
|
79
|
-
overflow: "hidden",
|
|
80
|
-
textOverflow: "ellipsis",
|
|
81
|
-
});
|
|
82
|
-
button.append(avatar, name);
|
|
83
|
-
const fallback = document.createElement("a");
|
|
84
|
-
fallback.href = `${host}/account`;
|
|
85
|
-
fallback.target = "_blank";
|
|
86
|
-
fallback.rel = "noopener noreferrer";
|
|
87
|
-
fallback.textContent = "Open Flow ID";
|
|
88
|
-
fallback.hidden = true;
|
|
89
|
-
let lastUser = null;
|
|
90
|
-
let imageUrl = null;
|
|
91
|
-
avatar.textContent = "F";
|
|
92
|
-
let profileRequest = null;
|
|
93
|
-
function update(refresh = false) {
|
|
94
|
-
const user = flow.getState().user;
|
|
95
|
-
name.textContent = user?.username ?? "Flow ID";
|
|
96
|
-
button.setAttribute("aria-label", !user || user.isGuest
|
|
97
|
-
? "Sign in to Flow ID"
|
|
98
|
-
: `Open ${user.username} Flow ID account in a new tab`);
|
|
99
|
-
const identityChanged = user?.id !== lastUser?.id ||
|
|
100
|
-
user?.username !== lastUser?.username ||
|
|
101
|
-
user?.isGuest !== lastUser?.isGuest;
|
|
102
|
-
if (identityChanged) {
|
|
103
|
-
profileRequest?.abort();
|
|
104
|
-
profileRequest = null;
|
|
105
|
-
imageUrl = null;
|
|
106
|
-
avatar.textContent = user?.username.slice(0, 1).toUpperCase() ?? "F";
|
|
107
|
-
lastUser = user;
|
|
108
|
-
}
|
|
109
|
-
else if (!refresh || profileRequest) {
|
|
110
|
-
// Same account: only an explicit refresh refetches, and never on top of
|
|
111
|
-
// a request already in flight.
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (!user || user.isGuest)
|
|
115
|
-
return;
|
|
116
|
-
const controller = new AbortController();
|
|
117
|
-
profileRequest = controller;
|
|
118
|
-
void fetch(`${host}/api/users/${encodeURIComponent(user.username)}`, {
|
|
119
|
-
signal: controller.signal,
|
|
120
|
-
credentials: "omit",
|
|
121
|
-
})
|
|
122
|
-
.then(async (response) => {
|
|
123
|
-
if (!response.ok)
|
|
124
|
-
return;
|
|
125
|
-
// SAFETY: Flow ID owns the public profile endpoint and its optional image field.
|
|
126
|
-
const profile = (await response.json());
|
|
127
|
-
if (controller.signal.aborted)
|
|
128
|
-
return;
|
|
129
|
-
if (profile.image === null) {
|
|
130
|
-
imageUrl = null;
|
|
131
|
-
avatar.textContent = user.username.slice(0, 1).toUpperCase();
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (!profile.image || profile.image === imageUrl)
|
|
135
|
-
return;
|
|
136
|
-
const image = document.createElement("img");
|
|
137
|
-
image.src = profile.image;
|
|
138
|
-
image.alt = "";
|
|
139
|
-
Object.assign(image.style, {
|
|
140
|
-
width: "100%",
|
|
141
|
-
height: "100%",
|
|
142
|
-
objectFit: "cover",
|
|
143
|
-
});
|
|
144
|
-
await image.decode();
|
|
145
|
-
if (controller.signal.aborted)
|
|
146
|
-
return;
|
|
147
|
-
imageUrl = profile.image;
|
|
148
|
-
avatar.replaceChildren(image);
|
|
149
|
-
})
|
|
150
|
-
.catch(() => { })
|
|
151
|
-
.finally(() => {
|
|
152
|
-
if (profileRequest === controller)
|
|
153
|
-
profileRequest = null;
|
|
154
|
-
});
|
|
21
|
+
document.body.appendChild(container);
|
|
155
22
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
void flow.login().catch(() => {
|
|
165
|
-
button.disabled = false;
|
|
166
|
-
notice.textContent = "Could not start sign-in. Try again.";
|
|
167
|
-
});
|
|
23
|
+
const mount = document.createElement("div");
|
|
24
|
+
container.appendChild(mount);
|
|
25
|
+
let destroyed = false;
|
|
26
|
+
let theme = options.theme;
|
|
27
|
+
let renderer = null;
|
|
28
|
+
void import("../react/profile-renderer")
|
|
29
|
+
.then(({ mountProfileMenu }) => {
|
|
30
|
+
if (destroyed)
|
|
168
31
|
return;
|
|
32
|
+
renderer = mountProfileMenu(mount, { flow, host: options.host, theme });
|
|
33
|
+
})
|
|
34
|
+
.catch(() => {
|
|
35
|
+
if (!destroyed) {
|
|
36
|
+
mount.setAttribute("role", "alert");
|
|
37
|
+
mount.textContent =
|
|
38
|
+
"Could not load the account menu. Reload and try again.";
|
|
169
39
|
}
|
|
170
|
-
const opened = flow.openAccount();
|
|
171
|
-
notice.textContent = opened
|
|
172
|
-
? ""
|
|
173
|
-
: "Your browser blocked the new tab. Open Flow ID below to sign in.";
|
|
174
|
-
fallback.hidden = opened;
|
|
175
40
|
});
|
|
176
|
-
function refresh() {
|
|
177
|
-
update(true);
|
|
178
|
-
}
|
|
179
|
-
window.addEventListener("focus", refresh);
|
|
180
|
-
function setTheme(theme) {
|
|
181
|
-
button.style.colorScheme = theme;
|
|
182
|
-
}
|
|
183
|
-
setTheme(options.theme ?? "light dark");
|
|
184
|
-
container.append(button, notice, fallback);
|
|
185
|
-
update();
|
|
186
|
-
const unsubscribe = flow.subscribe(() => update());
|
|
187
41
|
return {
|
|
188
|
-
setTheme
|
|
42
|
+
setTheme(value) {
|
|
43
|
+
theme = value;
|
|
44
|
+
renderer?.setTheme(value);
|
|
45
|
+
},
|
|
189
46
|
destroy() {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
notice.remove();
|
|
196
|
-
createdContainer?.remove();
|
|
47
|
+
destroyed = true;
|
|
48
|
+
renderer?.destroy();
|
|
49
|
+
mount.remove();
|
|
50
|
+
if (!options.container)
|
|
51
|
+
container.remove();
|
|
197
52
|
},
|
|
198
53
|
};
|
|
199
54
|
}
|
|
@@ -28,12 +28,12 @@ export async function restoreCredential(store) {
|
|
|
28
28
|
*/
|
|
29
29
|
export async function runLogout(store) {
|
|
30
30
|
const state = store.getSnapshot();
|
|
31
|
+
store.setState(initialFlowState);
|
|
31
32
|
if (state.address) {
|
|
32
33
|
const { clearAccessKey } = await import("./access-key");
|
|
33
34
|
await clearAccessKey(state.address);
|
|
34
35
|
}
|
|
35
36
|
await idb.delete(CREDENTIAL_KEY);
|
|
36
|
-
store.setState(initialFlowState);
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* Derives the user's wallet address from their passkey public key by
|
|
@@ -45,6 +45,9 @@ export function createStaticFlow(state, options = {}) {
|
|
|
45
45
|
},
|
|
46
46
|
login: () => unavailable("login"),
|
|
47
47
|
openAccount: () => unavailable("openAccount"),
|
|
48
|
+
switchAccounts: () => unavailable("switchAccounts"),
|
|
49
|
+
addAccount: () => unavailable("addAccount"),
|
|
50
|
+
listAccounts: () => unavailable("listAccounts"),
|
|
48
51
|
logout: () => unavailable("logout"),
|
|
49
52
|
restore: async () => false,
|
|
50
53
|
ensureGuest: () => unavailable("ensureGuest"),
|
package/dist/sdk/db/schema.d.ts
CHANGED
|
@@ -651,6 +651,173 @@ export declare const session: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
|
651
651
|
};
|
|
652
652
|
dialect: "pg";
|
|
653
653
|
}>;
|
|
654
|
+
export declare const browserAccountSet: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
655
|
+
name: "browser_account_set";
|
|
656
|
+
schema: undefined;
|
|
657
|
+
columns: {
|
|
658
|
+
initialized: import("drizzle-orm/pg-core").PgColumn<{
|
|
659
|
+
name: "initialized";
|
|
660
|
+
tableName: "browser_account_set";
|
|
661
|
+
dataType: "boolean";
|
|
662
|
+
columnType: "PgBoolean";
|
|
663
|
+
data: boolean;
|
|
664
|
+
driverParam: boolean;
|
|
665
|
+
notNull: true;
|
|
666
|
+
hasDefault: true;
|
|
667
|
+
isPrimaryKey: false;
|
|
668
|
+
isAutoincrement: false;
|
|
669
|
+
hasRuntimeDefault: false;
|
|
670
|
+
enumValues: undefined;
|
|
671
|
+
baseColumn: never;
|
|
672
|
+
identity: undefined;
|
|
673
|
+
generated: undefined;
|
|
674
|
+
}, {}, {}>;
|
|
675
|
+
id: import("drizzle-orm/pg-core").PgColumn<{
|
|
676
|
+
name: "id";
|
|
677
|
+
tableName: "browser_account_set";
|
|
678
|
+
dataType: "string";
|
|
679
|
+
columnType: "PgText";
|
|
680
|
+
data: string;
|
|
681
|
+
driverParam: string;
|
|
682
|
+
notNull: true;
|
|
683
|
+
hasDefault: false;
|
|
684
|
+
isPrimaryKey: true;
|
|
685
|
+
isAutoincrement: false;
|
|
686
|
+
hasRuntimeDefault: false;
|
|
687
|
+
enumValues: [string, ...string[]];
|
|
688
|
+
baseColumn: never;
|
|
689
|
+
identity: undefined;
|
|
690
|
+
generated: undefined;
|
|
691
|
+
}, {}, {}>;
|
|
692
|
+
activeUserId: import("drizzle-orm/pg-core").PgColumn<{
|
|
693
|
+
name: "active_user_id";
|
|
694
|
+
tableName: "browser_account_set";
|
|
695
|
+
dataType: "string";
|
|
696
|
+
columnType: "PgText";
|
|
697
|
+
data: string;
|
|
698
|
+
driverParam: string;
|
|
699
|
+
notNull: false;
|
|
700
|
+
hasDefault: false;
|
|
701
|
+
isPrimaryKey: false;
|
|
702
|
+
isAutoincrement: false;
|
|
703
|
+
hasRuntimeDefault: false;
|
|
704
|
+
enumValues: [string, ...string[]];
|
|
705
|
+
baseColumn: never;
|
|
706
|
+
identity: undefined;
|
|
707
|
+
generated: undefined;
|
|
708
|
+
}, {}, {}>;
|
|
709
|
+
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
710
|
+
name: "updated_at";
|
|
711
|
+
tableName: "browser_account_set";
|
|
712
|
+
dataType: "date";
|
|
713
|
+
columnType: "PgTimestamp";
|
|
714
|
+
data: Date;
|
|
715
|
+
driverParam: string;
|
|
716
|
+
notNull: true;
|
|
717
|
+
hasDefault: true;
|
|
718
|
+
isPrimaryKey: false;
|
|
719
|
+
isAutoincrement: false;
|
|
720
|
+
hasRuntimeDefault: false;
|
|
721
|
+
enumValues: undefined;
|
|
722
|
+
baseColumn: never;
|
|
723
|
+
identity: undefined;
|
|
724
|
+
generated: undefined;
|
|
725
|
+
}, {}, {}>;
|
|
726
|
+
};
|
|
727
|
+
dialect: "pg";
|
|
728
|
+
}>;
|
|
729
|
+
export declare const browserAccountMember: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
730
|
+
name: "browser_account_member";
|
|
731
|
+
schema: undefined;
|
|
732
|
+
columns: {
|
|
733
|
+
setId: import("drizzle-orm/pg-core").PgColumn<{
|
|
734
|
+
name: "set_id";
|
|
735
|
+
tableName: "browser_account_member";
|
|
736
|
+
dataType: "string";
|
|
737
|
+
columnType: "PgText";
|
|
738
|
+
data: string;
|
|
739
|
+
driverParam: string;
|
|
740
|
+
notNull: true;
|
|
741
|
+
hasDefault: false;
|
|
742
|
+
isPrimaryKey: false;
|
|
743
|
+
isAutoincrement: false;
|
|
744
|
+
hasRuntimeDefault: false;
|
|
745
|
+
enumValues: [string, ...string[]];
|
|
746
|
+
baseColumn: never;
|
|
747
|
+
identity: undefined;
|
|
748
|
+
generated: undefined;
|
|
749
|
+
}, {}, {}>;
|
|
750
|
+
userId: import("drizzle-orm/pg-core").PgColumn<{
|
|
751
|
+
name: "user_id";
|
|
752
|
+
tableName: "browser_account_member";
|
|
753
|
+
dataType: "string";
|
|
754
|
+
columnType: "PgText";
|
|
755
|
+
data: string;
|
|
756
|
+
driverParam: string;
|
|
757
|
+
notNull: true;
|
|
758
|
+
hasDefault: false;
|
|
759
|
+
isPrimaryKey: false;
|
|
760
|
+
isAutoincrement: false;
|
|
761
|
+
hasRuntimeDefault: false;
|
|
762
|
+
enumValues: [string, ...string[]];
|
|
763
|
+
baseColumn: never;
|
|
764
|
+
identity: undefined;
|
|
765
|
+
generated: undefined;
|
|
766
|
+
}, {}, {}>;
|
|
767
|
+
sessionId: import("drizzle-orm/pg-core").PgColumn<{
|
|
768
|
+
name: "session_id";
|
|
769
|
+
tableName: "browser_account_member";
|
|
770
|
+
dataType: "string";
|
|
771
|
+
columnType: "PgText";
|
|
772
|
+
data: string;
|
|
773
|
+
driverParam: string;
|
|
774
|
+
notNull: true;
|
|
775
|
+
hasDefault: false;
|
|
776
|
+
isPrimaryKey: false;
|
|
777
|
+
isAutoincrement: false;
|
|
778
|
+
hasRuntimeDefault: false;
|
|
779
|
+
enumValues: [string, ...string[]];
|
|
780
|
+
baseColumn: never;
|
|
781
|
+
identity: undefined;
|
|
782
|
+
generated: undefined;
|
|
783
|
+
}, {}, {}>;
|
|
784
|
+
userEpoch: import("drizzle-orm/pg-core").PgColumn<{
|
|
785
|
+
name: "user_epoch";
|
|
786
|
+
tableName: "browser_account_member";
|
|
787
|
+
dataType: "number";
|
|
788
|
+
columnType: "PgInteger";
|
|
789
|
+
data: number;
|
|
790
|
+
driverParam: string | number;
|
|
791
|
+
notNull: true;
|
|
792
|
+
hasDefault: false;
|
|
793
|
+
isPrimaryKey: false;
|
|
794
|
+
isAutoincrement: false;
|
|
795
|
+
hasRuntimeDefault: false;
|
|
796
|
+
enumValues: undefined;
|
|
797
|
+
baseColumn: never;
|
|
798
|
+
identity: undefined;
|
|
799
|
+
generated: undefined;
|
|
800
|
+
}, {}, {}>;
|
|
801
|
+
lastUsedAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
802
|
+
name: "last_used_at";
|
|
803
|
+
tableName: "browser_account_member";
|
|
804
|
+
dataType: "date";
|
|
805
|
+
columnType: "PgTimestamp";
|
|
806
|
+
data: Date;
|
|
807
|
+
driverParam: string;
|
|
808
|
+
notNull: true;
|
|
809
|
+
hasDefault: true;
|
|
810
|
+
isPrimaryKey: false;
|
|
811
|
+
isAutoincrement: false;
|
|
812
|
+
hasRuntimeDefault: false;
|
|
813
|
+
enumValues: undefined;
|
|
814
|
+
baseColumn: never;
|
|
815
|
+
identity: undefined;
|
|
816
|
+
generated: undefined;
|
|
817
|
+
}, {}, {}>;
|
|
818
|
+
};
|
|
819
|
+
dialect: "pg";
|
|
820
|
+
}>;
|
|
654
821
|
export declare const refreshToken: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
655
822
|
name: "refresh_token";
|
|
656
823
|
schema: undefined;
|
package/dist/sdk/db/schema.js
CHANGED
|
@@ -79,6 +79,25 @@ export const session = pgTable("session", {
|
|
|
79
79
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
80
80
|
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
81
81
|
}, (table) => [index("session_userId_idx").on(table.userId)]);
|
|
82
|
+
export const browserAccountSet = pgTable("browser_account_set", {
|
|
83
|
+
initialized: boolean("initialized").notNull().default(false),
|
|
84
|
+
id: text("id").primaryKey(),
|
|
85
|
+
activeUserId: text("active_user_id"),
|
|
86
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
87
|
+
});
|
|
88
|
+
export const browserAccountMember = pgTable("browser_account_member", {
|
|
89
|
+
setId: text("set_id")
|
|
90
|
+
.notNull()
|
|
91
|
+
.references(() => browserAccountSet.id, { onDelete: "cascade" }),
|
|
92
|
+
userId: text("user_id")
|
|
93
|
+
.notNull()
|
|
94
|
+
.references(() => user.id, { onDelete: "cascade" }),
|
|
95
|
+
sessionId: text("session_id")
|
|
96
|
+
.notNull()
|
|
97
|
+
.references(() => session.id, { onDelete: "cascade" }),
|
|
98
|
+
userEpoch: integer("user_epoch").notNull(),
|
|
99
|
+
lastUsedAt: timestamp("last_used_at").notNull().defaultNow(),
|
|
100
|
+
}, (table) => [primaryKey({ columns: [table.setId, table.userId] })]);
|
|
82
101
|
// First-party session persistence: consumer apps store the opaque token in their
|
|
83
102
|
// OWN origin and mint access JWTs via POST /api/session/refresh — no third-party
|
|
84
103
|
// id.flow.industries cookie, so it survives iOS ITP. Rotated on every use.
|