@everystack/auth 0.1.0 → 0.2.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 +1 -7
- package/package.json +33 -22
- package/src/api.ts +42 -8
- package/src/client/index.tsx +39 -7
- package/src/cookies.ts +3 -0
- package/src/csrf.ts +57 -0
- package/src/index.ts +276 -46
- package/src/jwt/index.ts +4 -3
- package/src/passwords.ts +1 -1
- package/src/plugin.ts +264 -0
- package/src/postgres.ts +310 -59
- package/src/refresh.ts +121 -57
- package/src/schema.ts +222 -130
- package/src/sql.ts +715 -73
- package/src/testing.ts +232 -0
package/src/schema.ts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auth schema —
|
|
2
|
+
* Auth schema — default tables for everystack auth.
|
|
3
3
|
*
|
|
4
4
|
* Tables:
|
|
5
|
-
*
|
|
6
|
-
* identities
|
|
7
|
-
*
|
|
8
|
-
* devices
|
|
5
|
+
* users — Supabase-compatible user table (batteries included, replaceable)
|
|
6
|
+
* identities — OAuth provider links (Supabase-compatible)
|
|
7
|
+
* refreshTokens — RFC 6819 family-based token rotation with reuse detection
|
|
8
|
+
* devices — Device registry (persistent ID, push tokens) + attestation
|
|
9
|
+
* mfaFactors — TOTP + WebAuthn multi-factor authentication
|
|
10
|
+
* mfaChallenges — Ephemeral MFA challenge state
|
|
11
|
+
* oneTimeTokens — Email confirmation, password reset, magic links
|
|
9
12
|
*
|
|
10
|
-
*
|
|
13
|
+
* The default schema works out of the box. Apps that need a different user model
|
|
14
|
+
* replace the postgres functions (sign_in, sign_up, oauth_sign_in) — the function
|
|
15
|
+
* signatures are the stable API, not the schema.
|
|
16
|
+
*
|
|
17
|
+
* Column names match Supabase for zero-friction migration.
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
20
|
import {
|
|
14
21
|
pgSchema,
|
|
22
|
+
boolean,
|
|
15
23
|
text,
|
|
16
24
|
timestamp,
|
|
17
25
|
uuid,
|
|
26
|
+
varchar,
|
|
18
27
|
integer,
|
|
19
28
|
index,
|
|
20
29
|
uniqueIndex,
|
|
@@ -26,159 +35,121 @@ const authSchema = pgSchema('auth');
|
|
|
26
35
|
export { authSchema };
|
|
27
36
|
|
|
28
37
|
// =============================================================================
|
|
29
|
-
//
|
|
38
|
+
// USERS
|
|
30
39
|
// =============================================================================
|
|
31
40
|
|
|
32
41
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* domain='auth' data: { biometricAt, attestationAt, authorizedBy, authorizedAt,
|
|
36
|
-
* revokedBy, revokedReason, challenge, challengeExpiresAt, browserName,
|
|
37
|
-
* browserOs, location, locationHint, familyId }
|
|
38
|
-
*
|
|
39
|
-
* domain='work' data: { goals, taskIds, summary, outcome }
|
|
42
|
+
* Default user table. Supabase-compatible column names for zero-friction migration.
|
|
40
43
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* domain='ws' data: { connectionId, channels }
|
|
44
|
+
* Apps that need a different user model can replace this table entirely —
|
|
45
|
+
* the postgres auth functions (sign_in, sign_up, oauth_sign_in) are the real API.
|
|
46
|
+
* Replace the functions, keep the primitives (hash_password, sign_jwt, etc.).
|
|
45
47
|
*/
|
|
46
|
-
export const
|
|
47
|
-
'
|
|
48
|
+
export const users = authSchema.table(
|
|
49
|
+
'users',
|
|
48
50
|
{
|
|
49
51
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
tokenHash: text('token_hash').unique(),
|
|
65
|
-
|
|
66
|
-
// Status (shared across domains, different values per domain)
|
|
67
|
-
status: text('status').notNull().default('active'),
|
|
68
|
-
|
|
69
|
-
// Auth: lifecycle timestamps
|
|
70
|
-
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
|
71
|
-
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
|
|
72
|
-
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
|
73
|
-
|
|
74
|
-
// Auth: security context
|
|
75
|
-
ipAddress: text('ip_address'),
|
|
76
|
-
userAgent: text('user_agent'),
|
|
77
|
-
|
|
78
|
-
// Work/agent: lifecycle
|
|
79
|
-
startedAt: timestamp('started_at', { withTimezone: true }),
|
|
80
|
-
endedAt: timestamp('ended_at', { withTimezone: true }),
|
|
81
|
-
|
|
82
|
-
// Agent: counters
|
|
83
|
-
toolCallCount: integer('tool_call_count'),
|
|
84
|
-
errorCount: integer('error_count'),
|
|
85
|
-
|
|
86
|
-
// Domain-specific data
|
|
87
|
-
data: jsonb('data'),
|
|
88
|
-
|
|
89
|
-
// Timestamps
|
|
52
|
+
instanceId: uuid('instance_id'), // Supabase compat
|
|
53
|
+
aud: varchar('aud', { length: 255 }).default('authenticated'),
|
|
54
|
+
role: varchar('role', { length: 255 }).default('authenticated'),
|
|
55
|
+
email: varchar('email', { length: 255 }).unique(),
|
|
56
|
+
encryptedPassword: varchar('encrypted_password', { length: 255 }),
|
|
57
|
+
emailConfirmedAt: timestamp('email_confirmed_at', { withTimezone: true }),
|
|
58
|
+
phone: varchar('phone', { length: 255 }),
|
|
59
|
+
phoneConfirmedAt: timestamp('phone_confirmed_at', { withTimezone: true }),
|
|
60
|
+
rawAppMetaData: jsonb('raw_app_meta_data'),
|
|
61
|
+
rawUserMetaData: jsonb('raw_user_meta_data'),
|
|
62
|
+
lastSignInAt: timestamp('last_sign_in_at', { withTimezone: true }),
|
|
63
|
+
bannedUntil: timestamp('banned_until', { withTimezone: true }),
|
|
64
|
+
deletedAt: timestamp('deleted_at', { withTimezone: true }),
|
|
65
|
+
isAnonymous: boolean('is_anonymous').notNull().default(false),
|
|
90
66
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
67
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
91
68
|
},
|
|
92
69
|
(table) => ({
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
domainAgentStatusIdx: index('sessions_domain_agent_status_idx').on(table.domain, table.agentId, table.status),
|
|
96
|
-
typeIdx: index('sessions_type_idx').on(table.type),
|
|
97
|
-
userIdx: index('sessions_user_idx').on(table.userId),
|
|
98
|
-
deviceIdx: index('sessions_device_idx').on(table.deviceId),
|
|
99
|
-
tokenIdx: uniqueIndex('sessions_token_idx').on(table.tokenHash),
|
|
100
|
-
statusIdx: index('sessions_status_idx').on(table.status),
|
|
101
|
-
expiresIdx: index('sessions_expires_idx').on(table.expiresAt),
|
|
102
|
-
repoIdx: index('sessions_repo_idx').on(table.repositoryId),
|
|
103
|
-
createdIdx: index('sessions_created_idx').on(table.createdAt),
|
|
70
|
+
emailIdx: uniqueIndex('users_email_idx').on(table.email),
|
|
71
|
+
instanceIdx: index('users_instance_id_idx').on(table.instanceId),
|
|
104
72
|
})
|
|
105
73
|
);
|
|
106
74
|
|
|
107
75
|
// =============================================================================
|
|
108
|
-
// IDENTITIES
|
|
76
|
+
// IDENTITIES
|
|
109
77
|
// =============================================================================
|
|
110
78
|
|
|
111
79
|
/**
|
|
112
|
-
*
|
|
113
|
-
* from HOW they prove it (credentials).
|
|
80
|
+
* OAuth provider links. Supabase-compatible.
|
|
114
81
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
82
|
+
* One row per (provider, provider_id) pair. A user can have multiple identities
|
|
83
|
+
* (email + google + apple). The identity_data jsonb stores provider-specific claims.
|
|
117
84
|
*/
|
|
118
85
|
export const identities = authSchema.table(
|
|
119
86
|
'identities',
|
|
120
87
|
{
|
|
121
88
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
blockedAt: timestamp('blocked_at', { withTimezone: true }),
|
|
131
|
-
blockReason: text('block_reason'),
|
|
132
|
-
|
|
89
|
+
userId: uuid('user_id')
|
|
90
|
+
.notNull()
|
|
91
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
92
|
+
provider: text('provider').notNull(),
|
|
93
|
+
providerId: text('provider_id').notNull(),
|
|
94
|
+
identityData: jsonb('identity_data'),
|
|
95
|
+
email: text('email'),
|
|
96
|
+
lastSignInAt: timestamp('last_sign_in_at', { withTimezone: true }),
|
|
133
97
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
98
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
134
99
|
},
|
|
135
100
|
(table) => ({
|
|
136
|
-
|
|
101
|
+
providerUniqueIdx: uniqueIndex('identities_provider_id_provider_idx').on(
|
|
102
|
+
table.providerId,
|
|
103
|
+
table.provider
|
|
104
|
+
),
|
|
137
105
|
userIdx: index('identities_user_idx').on(table.userId),
|
|
138
|
-
externalIdx: index('identities_external_idx').on(table.type, table.externalId),
|
|
139
|
-
blockedIdx: index('identities_blocked_idx').on(table.blockedAt),
|
|
140
106
|
})
|
|
141
107
|
);
|
|
142
108
|
|
|
143
109
|
// =============================================================================
|
|
144
|
-
//
|
|
110
|
+
// REFRESH TOKENS
|
|
145
111
|
// =============================================================================
|
|
146
112
|
|
|
147
113
|
/**
|
|
148
|
-
*
|
|
114
|
+
* Dedicated refresh token table.
|
|
115
|
+
*
|
|
116
|
+
* Security properties:
|
|
117
|
+
* - Only SHA-256 hashes stored (never plaintext)
|
|
118
|
+
* - familyId groups a rotation chain for theft detection
|
|
119
|
+
* - status='consumed' tokens are kept; replay triggers family revocation (RFC 6819)
|
|
120
|
+
* - ip_address/user_agent enable "active sessions" display
|
|
121
|
+
* - device_id enables token-to-device binding
|
|
149
122
|
*
|
|
150
|
-
*
|
|
151
|
-
* -
|
|
152
|
-
* - password: secretHash (bcrypt/argon2/scram)
|
|
153
|
-
* - api_key: credentialId (key prefix), secretHash (argon2)
|
|
154
|
-
* - oauth: credentialId (providerUid), name (provider), metadata ({email})
|
|
155
|
-
* - challenge: credentialId (nonce), expiresAt (5min), revokedAt (consumed)
|
|
123
|
+
* PostgreSQL path uses SECURITY DEFINER functions with SELECT FOR UPDATE
|
|
124
|
+
* for full atomicity. JS path uses consumed-token reuse detection as mitigation.
|
|
156
125
|
*/
|
|
157
|
-
export const
|
|
158
|
-
'
|
|
126
|
+
export const refreshTokens = authSchema.table(
|
|
127
|
+
'refresh_tokens',
|
|
159
128
|
{
|
|
160
129
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
130
|
+
repositoryId: uuid('repository_id'),
|
|
131
|
+
userId: uuid('user_id')
|
|
132
|
+
.notNull()
|
|
133
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
134
|
+
tokenHash: text('token_hash').notNull().unique(),
|
|
135
|
+
familyId: uuid('family_id').notNull().defaultRandom(),
|
|
136
|
+
status: text('status').notNull().default('active'), // 'active' | 'consumed'
|
|
137
|
+
ipAddress: text('ip_address'),
|
|
138
|
+
userAgent: text('user_agent'),
|
|
139
|
+
deviceId: uuid('device_id'),
|
|
140
|
+
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
|
167
141
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
168
|
-
expiresAt: timestamp('expires_at', { withTimezone: true }),
|
|
169
|
-
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
|
|
170
|
-
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
|
171
|
-
revokedBy: uuid('revoked_by'),
|
|
172
|
-
metadata: jsonb('metadata'),
|
|
173
142
|
},
|
|
174
143
|
(table) => ({
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
144
|
+
tokenIdx: uniqueIndex('refresh_tokens_token_idx').on(table.tokenHash),
|
|
145
|
+
userIdx: index('refresh_tokens_user_idx').on(table.userId),
|
|
146
|
+
repoIdx: index('refresh_tokens_repo_idx').on(table.repositoryId),
|
|
147
|
+
repoUserIdx: index('refresh_tokens_repo_user_idx').on(table.repositoryId, table.userId),
|
|
148
|
+
familyIdx: index('refresh_tokens_family_idx').on(table.familyId),
|
|
149
|
+
expiresIdx: index('refresh_tokens_expires_idx').on(table.expiresAt),
|
|
150
|
+
activeFamilyIdx: index('refresh_tokens_active_family_idx')
|
|
151
|
+
.on(table.familyId)
|
|
152
|
+
.where(sql`${table.status} = 'active'`),
|
|
182
153
|
})
|
|
183
154
|
);
|
|
184
155
|
|
|
@@ -187,38 +158,159 @@ export const credentials = authSchema.table(
|
|
|
187
158
|
// =============================================================================
|
|
188
159
|
|
|
189
160
|
/**
|
|
190
|
-
*
|
|
161
|
+
* Unified device registry + attestation.
|
|
162
|
+
*
|
|
163
|
+
* Auth-owned fields (device registry):
|
|
164
|
+
* deviceId — persistent UUID from SecureStorage, survives reinstalls
|
|
165
|
+
* userId — nullable, set when user signs in (anonymous on first launch)
|
|
166
|
+
* pushToken — nullable, set when app enables push notifications
|
|
167
|
+
* platform — 'ios' | 'android' | 'web'
|
|
168
|
+
* name, model, osVersion, appVersion, userAgent — device metadata
|
|
191
169
|
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
170
|
+
* Security-consumed fields (attestation):
|
|
171
|
+
* attestationType — 'apple_app_attest' | 'play_integrity' | null
|
|
172
|
+
* keyId, publicKey, attestationData — hardware-backed keys
|
|
173
|
+
* trustScore, attestationCount, lastAttestation — attestation state
|
|
174
|
+
* revokedAt, revokedReason — device revocation
|
|
195
175
|
*/
|
|
196
176
|
export const devices = authSchema.table(
|
|
197
177
|
'devices',
|
|
198
178
|
{
|
|
199
179
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
200
|
-
|
|
180
|
+
repositoryId: uuid('repository_id'),
|
|
181
|
+
userId: uuid('user_id').references(() => users.id, { onDelete: 'set null' }),
|
|
201
182
|
deviceId: text('device_id').notNull().unique(),
|
|
202
|
-
platform: text('platform').notNull(),
|
|
203
|
-
|
|
204
|
-
keyId: text('key_id'),
|
|
205
|
-
publicKey: text('public_key'),
|
|
206
|
-
attestationData: jsonb('attestation_data'),
|
|
183
|
+
platform: text('platform').notNull(), // 'ios' | 'android' | 'web'
|
|
184
|
+
pushToken: text('push_token'),
|
|
207
185
|
name: text('name'),
|
|
208
186
|
model: text('model'),
|
|
209
187
|
osVersion: text('os_version'),
|
|
210
188
|
appVersion: text('app_version'),
|
|
211
|
-
|
|
212
|
-
|
|
189
|
+
userAgent: text('user_agent'),
|
|
190
|
+
|
|
191
|
+
// Attestation (consumed by @everystack/security)
|
|
192
|
+
attestationType: text('attestation_type'),
|
|
193
|
+
keyId: text('key_id'),
|
|
194
|
+
publicKey: text('public_key'),
|
|
195
|
+
attestationData: jsonb('attestation_data'),
|
|
213
196
|
trustScore: integer('trust_score').notNull().default(100),
|
|
197
|
+
attestationCount: integer('attestation_count').notNull().default(0),
|
|
198
|
+
lastAttestation: timestamp('last_attestation', { withTimezone: true }),
|
|
199
|
+
|
|
200
|
+
// Lifecycle
|
|
214
201
|
registeredAt: timestamp('registered_at', { withTimezone: true }).defaultNow().notNull(),
|
|
215
202
|
lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).defaultNow().notNull(),
|
|
216
203
|
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
|
217
204
|
revokedReason: text('revoked_reason'),
|
|
218
205
|
},
|
|
219
206
|
(table) => ({
|
|
207
|
+
repoIdx: index('devices_repo_idx').on(table.repositoryId),
|
|
220
208
|
userIdx: index('devices_user_idx').on(table.userId),
|
|
209
|
+
repoUserIdx: index('devices_repo_user_idx').on(table.repositoryId, table.userId),
|
|
221
210
|
deviceIdIdx: uniqueIndex('devices_device_id_idx').on(table.deviceId),
|
|
222
211
|
platformIdx: index('devices_platform_idx').on(table.platform),
|
|
223
212
|
})
|
|
224
213
|
);
|
|
214
|
+
|
|
215
|
+
// =============================================================================
|
|
216
|
+
// MFA FACTORS
|
|
217
|
+
// =============================================================================
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Multi-factor authentication factors. TOTP + WebAuthn.
|
|
221
|
+
*
|
|
222
|
+
* Users enroll factors (e.g. Google Authenticator) which start as 'unverified'.
|
|
223
|
+
* After confirming a challenge, the factor becomes 'verified' and is required
|
|
224
|
+
* for subsequent sign-ins.
|
|
225
|
+
*
|
|
226
|
+
* MFA handlers are a follow-up — this schema establishes the tables so apps
|
|
227
|
+
* can implement their own MFA flows via postgres functions.
|
|
228
|
+
*/
|
|
229
|
+
export const mfaFactors = authSchema.table(
|
|
230
|
+
'mfa_factors',
|
|
231
|
+
{
|
|
232
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
233
|
+
userId: uuid('user_id')
|
|
234
|
+
.notNull()
|
|
235
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
236
|
+
friendlyName: text('friendly_name'),
|
|
237
|
+
factorType: text('factor_type').notNull(), // 'totp' | 'webauthn'
|
|
238
|
+
status: text('status').notNull().default('unverified'), // 'unverified' | 'verified'
|
|
239
|
+
secret: text('secret'), // encrypted TOTP secret
|
|
240
|
+
webAuthnCredential: jsonb('web_authn_credential'),
|
|
241
|
+
phone: text('phone'), // future: phone MFA
|
|
242
|
+
lastChallengedAt: timestamp('last_challenged_at', { withTimezone: true }),
|
|
243
|
+
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
244
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
245
|
+
},
|
|
246
|
+
(table) => ({
|
|
247
|
+
userIdx: index('mfa_factors_user_idx').on(table.userId),
|
|
248
|
+
userVerifiedIdx: index('mfa_factors_user_verified_idx')
|
|
249
|
+
.on(table.userId)
|
|
250
|
+
.where(sql`${table.status} = 'verified'`),
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// =============================================================================
|
|
255
|
+
// MFA CHALLENGES
|
|
256
|
+
// =============================================================================
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Ephemeral MFA challenge state.
|
|
260
|
+
*
|
|
261
|
+
* Created when a user initiates MFA verification. Verified within a timeout
|
|
262
|
+
* window. Can be cleaned up periodically.
|
|
263
|
+
*/
|
|
264
|
+
export const mfaChallenges = authSchema.table(
|
|
265
|
+
'mfa_challenges',
|
|
266
|
+
{
|
|
267
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
268
|
+
factorId: uuid('factor_id')
|
|
269
|
+
.notNull()
|
|
270
|
+
.references(() => mfaFactors.id, { onDelete: 'cascade' }),
|
|
271
|
+
ipAddress: text('ip_address'),
|
|
272
|
+
otpCode: text('otp_code'),
|
|
273
|
+
webAuthnSessionData: jsonb('web_authn_session_data'),
|
|
274
|
+
verifiedAt: timestamp('verified_at', { withTimezone: true }),
|
|
275
|
+
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
276
|
+
},
|
|
277
|
+
(table) => ({
|
|
278
|
+
factorIdx: index('mfa_challenges_factor_idx').on(table.factorId),
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// =============================================================================
|
|
283
|
+
// ONE-TIME TOKENS
|
|
284
|
+
// =============================================================================
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* One-time tokens for email confirmation, password reset, magic links.
|
|
288
|
+
*
|
|
289
|
+
* Each (user_id, token_type) pair is unique — creating a new token of the same
|
|
290
|
+
* type replaces the previous one. Tokens are stored as SHA-256 hashes.
|
|
291
|
+
*
|
|
292
|
+
* Token types: confirmation, recovery, reauthentication,
|
|
293
|
+
* email_change_new, email_change_current, phone_change
|
|
294
|
+
*/
|
|
295
|
+
export const oneTimeTokens = authSchema.table(
|
|
296
|
+
'one_time_tokens',
|
|
297
|
+
{
|
|
298
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
299
|
+
userId: uuid('user_id')
|
|
300
|
+
.notNull()
|
|
301
|
+
.references(() => users.id, { onDelete: 'cascade' }),
|
|
302
|
+
tokenType: text('token_type').notNull(),
|
|
303
|
+
tokenHash: text('token_hash').notNull(),
|
|
304
|
+
relatesTo: text('relates_to').notNull(), // email or phone
|
|
305
|
+
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
|
306
|
+
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
|
|
307
|
+
},
|
|
308
|
+
(table) => ({
|
|
309
|
+
userTypeUniqueIdx: uniqueIndex('one_time_tokens_user_type_idx').on(
|
|
310
|
+
table.userId,
|
|
311
|
+
table.tokenType
|
|
312
|
+
),
|
|
313
|
+
tokenHashIdx: index('one_time_tokens_token_hash_idx').on(table.tokenHash),
|
|
314
|
+
relatesToIdx: index('one_time_tokens_relates_to_idx').on(table.relatesTo),
|
|
315
|
+
})
|
|
316
|
+
);
|