or3-provider-sqlite 0.0.4 → 0.0.7
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 +112 -19
- package/dist/module.json +1 -1
- package/dist/runtime/server/admin/adapters/sync-sqlite.js +90 -16
- package/dist/runtime/server/admin/stores/sqlite-store.js +7 -8
- package/dist/runtime/server/auth/sqlite-auth-workspace-store.d.ts +5 -0
- package/dist/runtime/server/auth/sqlite-auth-workspace-store.js +368 -40
- package/dist/runtime/server/connect/sqlite-connect-store.js +19 -5
- package/dist/runtime/server/db/d1-dialect.d.ts +39 -0
- package/dist/runtime/server/db/d1-dialect.js +70 -0
- package/dist/runtime/server/db/d1.d.ts +13 -0
- package/dist/runtime/server/db/d1.js +30 -0
- package/dist/runtime/server/db/kysely.d.ts +53 -15
- package/dist/runtime/server/db/kysely.js +364 -16
- package/dist/runtime/server/db/migrate.js +3 -1
- package/dist/runtime/server/db/migrations/016_connect_runtime_binding.d.ts +4 -0
- package/dist/runtime/server/db/migrations/016_connect_runtime_binding.js +28 -0
- package/dist/runtime/server/db/schema.d.ts +3 -0
- package/dist/runtime/server/plugins/register.js +33 -13
- package/dist/runtime/server/rate-limit/sqlite-provider.d.ts +2 -0
- package/dist/runtime/server/rate-limit/sqlite-provider.js +70 -1
- package/dist/runtime/server/sync/maintenance-state.d.ts +26 -0
- package/dist/runtime/server/sync/maintenance-state.js +66 -0
- package/dist/runtime/server/sync/sqlite-sync-gateway-adapter.d.ts +15 -4
- package/dist/runtime/server/sync/sqlite-sync-gateway-adapter.js +780 -21
- package/dist/runtime/server/webhooks/sqlite-webhook-store.d.ts +3 -2
- package/dist/runtime/server/webhooks/sqlite-webhook-store.js +90 -39
- package/package.json +10 -6
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getRawDb, getSqliteDb } from "../db/kysely.js";
|
|
2
|
-
import {
|
|
1
|
+
import { getRawDb, getSqliteDb, isD1Driver } from "../db/kysely.js";
|
|
2
|
+
import { d1All, d1Batch, d1Run } from "../db/d1.js";
|
|
3
3
|
function uid() {
|
|
4
|
-
return randomUUID();
|
|
4
|
+
return globalThis.crypto.randomUUID();
|
|
5
5
|
}
|
|
6
6
|
function nowEpoch() {
|
|
7
7
|
return Math.floor(Date.now() / 1e3);
|
|
@@ -16,6 +16,9 @@ export class SqliteAuthWorkspaceStore {
|
|
|
16
16
|
onInviteProvisionStep(_step) {
|
|
17
17
|
}
|
|
18
18
|
async getOrCreateUser(input) {
|
|
19
|
+
if (isD1Driver()) {
|
|
20
|
+
return this.getOrCreateUserInD1(input);
|
|
21
|
+
}
|
|
19
22
|
this.db;
|
|
20
23
|
const raw = getRawDb();
|
|
21
24
|
const userId = raw.transaction(() => {
|
|
@@ -116,25 +119,46 @@ export class SqliteAuthWorkspaceStore {
|
|
|
116
119
|
const memberId = uid();
|
|
117
120
|
const now = nowEpoch();
|
|
118
121
|
const name = "My Workspace";
|
|
119
|
-
|
|
120
|
-
await
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
122
|
+
if (isD1Driver()) {
|
|
123
|
+
await d1Batch([
|
|
124
|
+
{
|
|
125
|
+
sql: `INSERT INTO workspaces (
|
|
126
|
+
id, name, description, owner_user_id, created_at, deleted, deleted_at
|
|
127
|
+
) VALUES (?, ?, NULL, ?, ?, 0, NULL)`,
|
|
128
|
+
parameters: [workspaceId, name, userId, now]
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
sql: `INSERT INTO workspace_members (
|
|
132
|
+
id, workspace_id, user_id, role, created_at
|
|
133
|
+
) VALUES (?, ?, ?, 'owner', ?)`,
|
|
134
|
+
parameters: [memberId, workspaceId, userId, now]
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
sql: "UPDATE users SET active_workspace_id = ? WHERE id = ?",
|
|
138
|
+
parameters: [workspaceId, userId]
|
|
139
|
+
}
|
|
140
|
+
]);
|
|
141
|
+
} else {
|
|
142
|
+
await db.transaction().execute(async (tx) => {
|
|
143
|
+
await tx.insertInto("workspaces").values({
|
|
144
|
+
id: workspaceId,
|
|
145
|
+
name,
|
|
146
|
+
description: null,
|
|
147
|
+
owner_user_id: userId,
|
|
148
|
+
created_at: now,
|
|
149
|
+
deleted: 0,
|
|
150
|
+
deleted_at: null
|
|
151
|
+
}).execute();
|
|
152
|
+
await tx.insertInto("workspace_members").values({
|
|
153
|
+
id: memberId,
|
|
154
|
+
workspace_id: workspaceId,
|
|
155
|
+
user_id: userId,
|
|
156
|
+
role: "owner",
|
|
157
|
+
created_at: now
|
|
158
|
+
}).execute();
|
|
159
|
+
await tx.updateTable("users").set({ active_workspace_id: workspaceId }).where("id", "=", userId).execute();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
138
162
|
return { workspaceId, workspaceName: name, created: true };
|
|
139
163
|
}
|
|
140
164
|
async getWorkspaceRole(input) {
|
|
@@ -168,24 +192,47 @@ export class SqliteAuthWorkspaceStore {
|
|
|
168
192
|
const workspaceId = uid();
|
|
169
193
|
const memberId = uid();
|
|
170
194
|
const now = nowEpoch();
|
|
171
|
-
|
|
172
|
-
await
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
195
|
+
if (isD1Driver()) {
|
|
196
|
+
await d1Batch([
|
|
197
|
+
{
|
|
198
|
+
sql: `INSERT INTO workspaces (
|
|
199
|
+
id, name, description, owner_user_id, created_at, deleted, deleted_at
|
|
200
|
+
) VALUES (?, ?, ?, ?, ?, 0, NULL)`,
|
|
201
|
+
parameters: [
|
|
202
|
+
workspaceId,
|
|
203
|
+
input.name,
|
|
204
|
+
input.description ?? null,
|
|
205
|
+
input.userId,
|
|
206
|
+
now
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
sql: `INSERT INTO workspace_members (
|
|
211
|
+
id, workspace_id, user_id, role, created_at
|
|
212
|
+
) VALUES (?, ?, ?, 'owner', ?)`,
|
|
213
|
+
parameters: [memberId, workspaceId, input.userId, now]
|
|
214
|
+
}
|
|
215
|
+
]);
|
|
216
|
+
} else {
|
|
217
|
+
await db.transaction().execute(async (tx) => {
|
|
218
|
+
await tx.insertInto("workspaces").values({
|
|
219
|
+
id: workspaceId,
|
|
220
|
+
name: input.name,
|
|
221
|
+
description: input.description ?? null,
|
|
222
|
+
owner_user_id: input.userId,
|
|
223
|
+
created_at: now,
|
|
224
|
+
deleted: 0,
|
|
225
|
+
deleted_at: null
|
|
226
|
+
}).execute();
|
|
227
|
+
await tx.insertInto("workspace_members").values({
|
|
228
|
+
id: memberId,
|
|
229
|
+
workspace_id: workspaceId,
|
|
230
|
+
user_id: input.userId,
|
|
231
|
+
role: "owner",
|
|
232
|
+
created_at: now
|
|
233
|
+
}).execute();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
189
236
|
return { workspaceId };
|
|
190
237
|
}
|
|
191
238
|
async updateWorkspace(input) {
|
|
@@ -206,6 +253,31 @@ export class SqliteAuthWorkspaceStore {
|
|
|
206
253
|
throw new Error("Forbidden: only owner can remove workspace");
|
|
207
254
|
}
|
|
208
255
|
const now = nowEpoch();
|
|
256
|
+
if (isD1Driver()) {
|
|
257
|
+
await d1Batch([
|
|
258
|
+
{
|
|
259
|
+
sql: "UPDATE workspaces SET deleted = 1, deleted_at = ? WHERE id = ?",
|
|
260
|
+
parameters: [now, input.workspaceId]
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
sql: `UPDATE users
|
|
264
|
+
SET active_workspace_id = (
|
|
265
|
+
SELECT workspace_members.workspace_id
|
|
266
|
+
FROM workspace_members
|
|
267
|
+
INNER JOIN workspaces
|
|
268
|
+
ON workspaces.id = workspace_members.workspace_id
|
|
269
|
+
WHERE workspace_members.user_id = users.id
|
|
270
|
+
AND workspaces.deleted = 0
|
|
271
|
+
AND workspaces.id <> ?
|
|
272
|
+
ORDER BY workspace_members.created_at ASC
|
|
273
|
+
LIMIT 1
|
|
274
|
+
)
|
|
275
|
+
WHERE active_workspace_id = ?`,
|
|
276
|
+
parameters: [input.workspaceId, input.workspaceId]
|
|
277
|
+
}
|
|
278
|
+
]);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
209
281
|
await db.transaction().execute(async (tx) => {
|
|
210
282
|
await tx.updateTable("workspaces").set({ deleted: 1, deleted_at: now }).where("id", "=", input.workspaceId).execute();
|
|
211
283
|
const affectedUsers = await tx.selectFrom("users").select("id").where("active_workspace_id", "=", input.workspaceId).execute();
|
|
@@ -285,6 +357,9 @@ export class SqliteAuthWorkspaceStore {
|
|
|
285
357
|
void input.revokedByUserId;
|
|
286
358
|
}
|
|
287
359
|
async validateInvite(input) {
|
|
360
|
+
if (isD1Driver()) {
|
|
361
|
+
return this.validateInviteInD1(input);
|
|
362
|
+
}
|
|
288
363
|
this.db;
|
|
289
364
|
const raw = getRawDb();
|
|
290
365
|
const now = nowEpoch();
|
|
@@ -306,6 +381,9 @@ export class SqliteAuthWorkspaceStore {
|
|
|
306
381
|
return { ok: true, role: invite.role };
|
|
307
382
|
}
|
|
308
383
|
async acceptInviteAndProvisionUser(input) {
|
|
384
|
+
if (isD1Driver()) {
|
|
385
|
+
return this.acceptInviteAndProvisionUserInD1(input);
|
|
386
|
+
}
|
|
309
387
|
this.db;
|
|
310
388
|
const raw = getRawDb();
|
|
311
389
|
const now = nowEpoch();
|
|
@@ -389,6 +467,9 @@ export class SqliteAuthWorkspaceStore {
|
|
|
389
467
|
}).immediate();
|
|
390
468
|
}
|
|
391
469
|
async consumeInvite(input) {
|
|
470
|
+
if (isD1Driver()) {
|
|
471
|
+
return this.consumeInviteInD1(input);
|
|
472
|
+
}
|
|
392
473
|
this.db;
|
|
393
474
|
const raw = getRawDb();
|
|
394
475
|
const now = nowEpoch();
|
|
@@ -454,6 +535,253 @@ export class SqliteAuthWorkspaceStore {
|
|
|
454
535
|
return { ok: true, role: invite.role };
|
|
455
536
|
}).immediate();
|
|
456
537
|
}
|
|
538
|
+
async getOrCreateUserInD1(input) {
|
|
539
|
+
const candidateUserId = uid();
|
|
540
|
+
const now = nowEpoch();
|
|
541
|
+
const results = await d1Batch([
|
|
542
|
+
{
|
|
543
|
+
sql: `INSERT OR IGNORE INTO users (
|
|
544
|
+
id, email, display_name, active_workspace_id, created_at
|
|
545
|
+
) VALUES (?, ?, ?, NULL, ?)`,
|
|
546
|
+
parameters: [
|
|
547
|
+
candidateUserId,
|
|
548
|
+
input.email ?? null,
|
|
549
|
+
input.displayName ?? null,
|
|
550
|
+
now
|
|
551
|
+
]
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
sql: `INSERT OR IGNORE INTO auth_accounts (
|
|
555
|
+
id, user_id, provider, provider_user_id, created_at
|
|
556
|
+
) VALUES (?, ?, ?, ?, ?)`,
|
|
557
|
+
parameters: [
|
|
558
|
+
uid(),
|
|
559
|
+
candidateUserId,
|
|
560
|
+
input.provider,
|
|
561
|
+
input.providerUserId,
|
|
562
|
+
now
|
|
563
|
+
]
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
sql: `SELECT user_id
|
|
567
|
+
FROM auth_accounts
|
|
568
|
+
WHERE provider = ? AND provider_user_id = ?`,
|
|
569
|
+
parameters: [input.provider, input.providerUserId]
|
|
570
|
+
}
|
|
571
|
+
]);
|
|
572
|
+
const winner = (results[2]?.results ?? [])[0];
|
|
573
|
+
if (!winner?.user_id) {
|
|
574
|
+
throw new Error("Failed to resolve auth account after get-or-create attempt");
|
|
575
|
+
}
|
|
576
|
+
if (winner.user_id !== candidateUserId) {
|
|
577
|
+
await d1Run(
|
|
578
|
+
`DELETE FROM users
|
|
579
|
+
WHERE id = ?
|
|
580
|
+
AND NOT EXISTS (
|
|
581
|
+
SELECT 1 FROM auth_accounts WHERE user_id = ?
|
|
582
|
+
)`,
|
|
583
|
+
candidateUserId,
|
|
584
|
+
candidateUserId
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
return { userId: winner.user_id };
|
|
588
|
+
}
|
|
589
|
+
async validateInviteInD1(input) {
|
|
590
|
+
const now = nowEpoch();
|
|
591
|
+
const [invite] = await d1All(
|
|
592
|
+
`SELECT role, status, email, expires_at
|
|
593
|
+
FROM auth_invites
|
|
594
|
+
WHERE workspace_id = ? AND token_hash = ?
|
|
595
|
+
LIMIT 1`,
|
|
596
|
+
input.workspaceId,
|
|
597
|
+
input.tokenHash
|
|
598
|
+
);
|
|
599
|
+
if (!invite) return { ok: false, reason: "not_found" };
|
|
600
|
+
if (invite.email !== normalizeEmail(input.email)) {
|
|
601
|
+
return { ok: false, reason: "email_mismatch" };
|
|
602
|
+
}
|
|
603
|
+
if (invite.status === "accepted") return { ok: false, reason: "already_used" };
|
|
604
|
+
if (invite.status === "revoked") return { ok: false, reason: "revoked" };
|
|
605
|
+
if (invite.status === "expired" || invite.expires_at <= now) {
|
|
606
|
+
return { ok: false, reason: "expired" };
|
|
607
|
+
}
|
|
608
|
+
return { ok: true, role: invite.role };
|
|
609
|
+
}
|
|
610
|
+
async acceptInviteAndProvisionUserInD1(input) {
|
|
611
|
+
const now = nowEpoch();
|
|
612
|
+
const normalizedEmail = normalizeEmail(input.email);
|
|
613
|
+
const validation = await this.validateInviteInD1({
|
|
614
|
+
workspaceId: input.workspaceId,
|
|
615
|
+
email: normalizedEmail,
|
|
616
|
+
tokenHash: input.tokenHash
|
|
617
|
+
});
|
|
618
|
+
if (!validation.ok) return validation;
|
|
619
|
+
this.onInviteProvisionStep("invite_validated");
|
|
620
|
+
const [existingAccount] = await d1All(
|
|
621
|
+
`SELECT user_id FROM auth_accounts
|
|
622
|
+
WHERE provider = ? AND provider_user_id = ?
|
|
623
|
+
LIMIT 1`,
|
|
624
|
+
input.provider,
|
|
625
|
+
input.providerUserId
|
|
626
|
+
);
|
|
627
|
+
const { userId } = await this.getOrCreateUserInD1({
|
|
628
|
+
provider: input.provider,
|
|
629
|
+
providerUserId: input.providerUserId,
|
|
630
|
+
email: normalizedEmail,
|
|
631
|
+
displayName: input.displayName
|
|
632
|
+
});
|
|
633
|
+
const createdUser = !existingAccount;
|
|
634
|
+
this.onInviteProvisionStep("user_created");
|
|
635
|
+
const results = await d1Batch([
|
|
636
|
+
{
|
|
637
|
+
sql: `UPDATE auth_invites
|
|
638
|
+
SET status = 'accepted', accepted_at = ?, accepted_user_id = ?, updated_at = ?
|
|
639
|
+
WHERE workspace_id = ?
|
|
640
|
+
AND token_hash = ?
|
|
641
|
+
AND email = ?
|
|
642
|
+
AND status = 'pending'
|
|
643
|
+
AND expires_at > ?`,
|
|
644
|
+
parameters: [
|
|
645
|
+
now,
|
|
646
|
+
userId,
|
|
647
|
+
now,
|
|
648
|
+
input.workspaceId,
|
|
649
|
+
input.tokenHash,
|
|
650
|
+
normalizedEmail,
|
|
651
|
+
now
|
|
652
|
+
]
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
sql: `INSERT INTO workspace_members (
|
|
656
|
+
id, workspace_id, user_id, role, created_at
|
|
657
|
+
)
|
|
658
|
+
SELECT ?, ?, ?, role, ?
|
|
659
|
+
FROM auth_invites
|
|
660
|
+
WHERE workspace_id = ?
|
|
661
|
+
AND token_hash = ?
|
|
662
|
+
AND status = 'accepted'
|
|
663
|
+
AND accepted_user_id = ?
|
|
664
|
+
ON CONFLICT(workspace_id, user_id) DO UPDATE SET role = excluded.role`,
|
|
665
|
+
parameters: [
|
|
666
|
+
uid(),
|
|
667
|
+
input.workspaceId,
|
|
668
|
+
userId,
|
|
669
|
+
now,
|
|
670
|
+
input.workspaceId,
|
|
671
|
+
input.tokenHash,
|
|
672
|
+
userId
|
|
673
|
+
]
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
sql: `UPDATE users SET active_workspace_id = ?
|
|
677
|
+
WHERE id = ?
|
|
678
|
+
AND EXISTS (
|
|
679
|
+
SELECT 1 FROM workspace_members
|
|
680
|
+
WHERE workspace_id = ? AND user_id = ?
|
|
681
|
+
)`,
|
|
682
|
+
parameters: [input.workspaceId, userId, input.workspaceId, userId]
|
|
683
|
+
}
|
|
684
|
+
]);
|
|
685
|
+
if ((results[0]?.meta?.changes ?? 0) !== 1) {
|
|
686
|
+
return this.inviteProvisionFailureInD1(input.workspaceId, normalizedEmail, input.tokenHash);
|
|
687
|
+
}
|
|
688
|
+
this.onInviteProvisionStep("membership_created");
|
|
689
|
+
this.onInviteProvisionStep("invite_consumed");
|
|
690
|
+
return { ok: true, userId, role: validation.role, createdUser };
|
|
691
|
+
}
|
|
692
|
+
async consumeInviteInD1(input) {
|
|
693
|
+
const now = nowEpoch();
|
|
694
|
+
const normalized = normalizeEmail(input.email);
|
|
695
|
+
await d1Run(
|
|
696
|
+
`UPDATE auth_invites SET status = 'expired', updated_at = ?
|
|
697
|
+
WHERE workspace_id = ?
|
|
698
|
+
AND status = 'pending'
|
|
699
|
+
AND expires_at <= ?`,
|
|
700
|
+
now,
|
|
701
|
+
input.workspaceId,
|
|
702
|
+
now
|
|
703
|
+
);
|
|
704
|
+
const [invite] = await d1All(
|
|
705
|
+
`SELECT * FROM auth_invites
|
|
706
|
+
WHERE workspace_id = ? AND email = ?
|
|
707
|
+
ORDER BY created_at ASC
|
|
708
|
+
LIMIT 1`,
|
|
709
|
+
input.workspaceId,
|
|
710
|
+
normalized
|
|
711
|
+
);
|
|
712
|
+
if (!invite) return { ok: false, reason: "not_found" };
|
|
713
|
+
if (invite.status === "revoked") return { ok: false, reason: "revoked" };
|
|
714
|
+
if (invite.status === "accepted") return { ok: false, reason: "already_used" };
|
|
715
|
+
if (invite.status === "expired" || invite.expires_at <= now) {
|
|
716
|
+
return { ok: false, reason: "expired" };
|
|
717
|
+
}
|
|
718
|
+
if (invite.token_hash !== input.tokenHash) {
|
|
719
|
+
return { ok: false, reason: "token_mismatch" };
|
|
720
|
+
}
|
|
721
|
+
const results = await d1Batch([
|
|
722
|
+
{
|
|
723
|
+
sql: `UPDATE auth_invites
|
|
724
|
+
SET status = 'accepted', accepted_at = ?, accepted_user_id = ?, updated_at = ?
|
|
725
|
+
WHERE id = ? AND status = 'pending' AND token_hash = ?`,
|
|
726
|
+
parameters: [
|
|
727
|
+
now,
|
|
728
|
+
input.acceptedUserId,
|
|
729
|
+
now,
|
|
730
|
+
invite.id,
|
|
731
|
+
input.tokenHash
|
|
732
|
+
]
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
sql: `INSERT INTO workspace_members (
|
|
736
|
+
id, workspace_id, user_id, role, created_at
|
|
737
|
+
)
|
|
738
|
+
SELECT ?, ?, ?, role, ?
|
|
739
|
+
FROM auth_invites
|
|
740
|
+
WHERE id = ?
|
|
741
|
+
AND status = 'accepted'
|
|
742
|
+
AND accepted_user_id = ?
|
|
743
|
+
ON CONFLICT(workspace_id, user_id) DO UPDATE SET role = excluded.role`,
|
|
744
|
+
parameters: [
|
|
745
|
+
uid(),
|
|
746
|
+
input.workspaceId,
|
|
747
|
+
input.acceptedUserId,
|
|
748
|
+
now,
|
|
749
|
+
invite.id,
|
|
750
|
+
input.acceptedUserId
|
|
751
|
+
]
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
sql: `UPDATE users SET active_workspace_id = ?
|
|
755
|
+
WHERE id = ?
|
|
756
|
+
AND EXISTS (
|
|
757
|
+
SELECT 1 FROM workspace_members
|
|
758
|
+
WHERE workspace_id = ? AND user_id = ?
|
|
759
|
+
)`,
|
|
760
|
+
parameters: [
|
|
761
|
+
input.workspaceId,
|
|
762
|
+
input.acceptedUserId,
|
|
763
|
+
input.workspaceId,
|
|
764
|
+
input.acceptedUserId
|
|
765
|
+
]
|
|
766
|
+
}
|
|
767
|
+
]);
|
|
768
|
+
if ((results[0]?.meta?.changes ?? 0) !== 1) {
|
|
769
|
+
const [latest] = await d1All("SELECT status FROM auth_invites WHERE id = ?", invite.id);
|
|
770
|
+
if (!latest) return { ok: false, reason: "not_found" };
|
|
771
|
+
if (latest.status === "revoked") return { ok: false, reason: "revoked" };
|
|
772
|
+
if (latest.status === "accepted") return { ok: false, reason: "already_used" };
|
|
773
|
+
return { ok: false, reason: "expired" };
|
|
774
|
+
}
|
|
775
|
+
return { ok: true, role: invite.role };
|
|
776
|
+
}
|
|
777
|
+
async inviteProvisionFailureInD1(workspaceId, email, tokenHash) {
|
|
778
|
+
const validation = await this.validateInviteInD1({
|
|
779
|
+
workspaceId,
|
|
780
|
+
email,
|
|
781
|
+
tokenHash
|
|
782
|
+
});
|
|
783
|
+
return validation.ok ? { ok: false, reason: "already_used" } : validation;
|
|
784
|
+
}
|
|
457
785
|
}
|
|
458
786
|
export function createSqliteAuthWorkspaceStore() {
|
|
459
787
|
return new SqliteAuthWorkspaceStore();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import {
|
|
3
2
|
ConnectStoreError
|
|
4
3
|
} from "~~/server/connect/store/types";
|
|
5
4
|
import { getRawDb } from "../db/kysely.js";
|
|
6
5
|
const ENVIRONMENT_COLUMNS = `
|
|
7
|
-
id, user_id, workspace_id, name, platform, architecture,
|
|
6
|
+
id, user_id, workspace_id, name, platform, architecture, driver, runtime,
|
|
7
|
+
base_path, host_id,
|
|
8
8
|
signing_public_key, noise_public_key, authorization_id, hostname,
|
|
9
9
|
tunnel_id, dns_record_id, control_token_hash, access_credential_ciphertext,
|
|
10
10
|
tunnel_secret_ciphertext, status, lifecycle_attempts,
|
|
@@ -40,7 +40,7 @@ class SqliteConnectStore {
|
|
|
40
40
|
status, host_json, expires_at, created_at, updated_at
|
|
41
41
|
) VALUES (?, ?, ?, 'pending', ?, ?, ?, ?)`
|
|
42
42
|
).run(
|
|
43
|
-
randomUUID(),
|
|
43
|
+
globalThis.crypto.randomUUID(),
|
|
44
44
|
input.deviceCodeHash,
|
|
45
45
|
input.userCodeHash,
|
|
46
46
|
JSON.stringify(input.host),
|
|
@@ -188,10 +188,14 @@ class SqliteConnectStore {
|
|
|
188
188
|
db.prepare(
|
|
189
189
|
`INSERT INTO connect_environments (
|
|
190
190
|
id, user_id, workspace_id, name, platform, architecture,
|
|
191
|
+
driver, runtime, base_path,
|
|
191
192
|
host_id, signing_public_key, noise_public_key, hostname,
|
|
192
193
|
tunnel_id, dns_record_id, control_token_hash,
|
|
193
194
|
access_credential_ciphertext, status, created_at, updated_at
|
|
194
|
-
) VALUES (
|
|
195
|
+
) VALUES (
|
|
196
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
197
|
+
'active', ?, ?
|
|
198
|
+
)`
|
|
195
199
|
).run(
|
|
196
200
|
environment.id,
|
|
197
201
|
input.userId,
|
|
@@ -199,6 +203,9 @@ class SqliteConnectStore {
|
|
|
199
203
|
environment.name,
|
|
200
204
|
environment.platform,
|
|
201
205
|
environment.architecture,
|
|
206
|
+
environment.driver ?? null,
|
|
207
|
+
environment.runtime ?? null,
|
|
208
|
+
environment.base_path ?? null,
|
|
202
209
|
environment.host_id ?? null,
|
|
203
210
|
environment.signing_public_key ?? null,
|
|
204
211
|
environment.noise_public_key ?? null,
|
|
@@ -277,6 +284,7 @@ class SqliteConnectStore {
|
|
|
277
284
|
db.prepare(
|
|
278
285
|
`INSERT INTO connect_environments (
|
|
279
286
|
id, user_id, workspace_id, name, platform, architecture,
|
|
287
|
+
driver, runtime, base_path,
|
|
280
288
|
host_id, signing_public_key, noise_public_key,
|
|
281
289
|
authorization_id, hostname, tunnel_id, dns_record_id,
|
|
282
290
|
control_token_hash, access_credential_ciphertext,
|
|
@@ -286,7 +294,7 @@ class SqliteConnectStore {
|
|
|
286
294
|
activation_deadline_at, activation_claimed_at,
|
|
287
295
|
created_at, updated_at
|
|
288
296
|
) VALUES (
|
|
289
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '', '', ?, ?, ?,
|
|
297
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '', '', ?, ?, ?,
|
|
290
298
|
'provisioning', 0, ?, ?, ?, ?, ?, NULL, ?, ?
|
|
291
299
|
)`
|
|
292
300
|
).run(
|
|
@@ -296,6 +304,9 @@ class SqliteConnectStore {
|
|
|
296
304
|
environment.name,
|
|
297
305
|
environment.platform,
|
|
298
306
|
environment.architecture,
|
|
307
|
+
environment.driver ?? null,
|
|
308
|
+
environment.runtime ?? null,
|
|
309
|
+
environment.base_path ?? null,
|
|
299
310
|
environment.host_id ?? null,
|
|
300
311
|
environment.signing_public_key ?? null,
|
|
301
312
|
environment.noise_public_key ?? null,
|
|
@@ -835,6 +846,9 @@ function toEnvironmentRecord(row) {
|
|
|
835
846
|
name: row.name,
|
|
836
847
|
platform: row.platform,
|
|
837
848
|
architecture: row.architecture,
|
|
849
|
+
driver: row.driver ?? void 0,
|
|
850
|
+
runtime: row.runtime ?? void 0,
|
|
851
|
+
base_path: row.base_path ?? void 0,
|
|
838
852
|
host_id: row.host_id ?? void 0,
|
|
839
853
|
signing_public_key: row.signing_public_key ?? void 0,
|
|
840
854
|
noise_public_key: row.noise_public_key ?? void 0,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small Kysely dialect for a Cloudflare D1 binding.
|
|
3
|
+
*
|
|
4
|
+
* D1 exposes an asynchronous prepared-statement API. Keeping this adapter
|
|
5
|
+
* local avoids pulling a Node-only database driver into a Workers bundle.
|
|
6
|
+
*/
|
|
7
|
+
import { SqliteAdapter, type DatabaseIntrospector, type Dialect, type Driver, type Kysely, type QueryCompiler } from 'kysely';
|
|
8
|
+
export interface D1Result<T = unknown> {
|
|
9
|
+
success?: boolean;
|
|
10
|
+
results?: T[];
|
|
11
|
+
meta?: {
|
|
12
|
+
changes?: number;
|
|
13
|
+
last_row_id?: number | string | null;
|
|
14
|
+
};
|
|
15
|
+
error?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface D1PreparedStatement {
|
|
18
|
+
bind(...values: unknown[]): D1PreparedStatement;
|
|
19
|
+
all<T = unknown>(): Promise<D1Result<T>>;
|
|
20
|
+
run<T = unknown>(): Promise<D1Result<T>>;
|
|
21
|
+
}
|
|
22
|
+
export interface D1Database {
|
|
23
|
+
prepare(query: string): D1PreparedStatement;
|
|
24
|
+
batch?<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* D1 does not expose SQL BEGIN/COMMIT transactions. Callers that require an
|
|
28
|
+
* atomic write use D1Database.batch(), which is intentionally kept outside
|
|
29
|
+
* the general-purpose Kysely transaction API.
|
|
30
|
+
*/
|
|
31
|
+
export declare const D1_TRANSACTION_ERROR = "Cloudflare D1 does not support Kysely transactions. Use the D1-specific atomic operation instead.";
|
|
32
|
+
export declare class D1Dialect implements Dialect {
|
|
33
|
+
private readonly database;
|
|
34
|
+
constructor(database: D1Database);
|
|
35
|
+
createAdapter(): SqliteAdapter;
|
|
36
|
+
createDriver(): Driver;
|
|
37
|
+
createQueryCompiler(): QueryCompiler;
|
|
38
|
+
createIntrospector(db: Kysely<unknown>): DatabaseIntrospector;
|
|
39
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SqliteAdapter,
|
|
3
|
+
SqliteIntrospector,
|
|
4
|
+
SqliteQueryCompiler
|
|
5
|
+
} from "kysely";
|
|
6
|
+
export const D1_TRANSACTION_ERROR = "Cloudflare D1 does not support Kysely transactions. Use the D1-specific atomic operation instead.";
|
|
7
|
+
export class D1Dialect {
|
|
8
|
+
constructor(database) {
|
|
9
|
+
this.database = database;
|
|
10
|
+
}
|
|
11
|
+
createAdapter() {
|
|
12
|
+
return new SqliteAdapter();
|
|
13
|
+
}
|
|
14
|
+
createDriver() {
|
|
15
|
+
return new D1Driver(this.database);
|
|
16
|
+
}
|
|
17
|
+
createQueryCompiler() {
|
|
18
|
+
return new SqliteQueryCompiler();
|
|
19
|
+
}
|
|
20
|
+
createIntrospector(db) {
|
|
21
|
+
return new SqliteIntrospector(db);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
class D1Driver {
|
|
25
|
+
connection;
|
|
26
|
+
constructor(database) {
|
|
27
|
+
this.connection = new D1Connection(database);
|
|
28
|
+
}
|
|
29
|
+
async init() {
|
|
30
|
+
}
|
|
31
|
+
async acquireConnection() {
|
|
32
|
+
return this.connection;
|
|
33
|
+
}
|
|
34
|
+
async beginTransaction() {
|
|
35
|
+
throw new Error(D1_TRANSACTION_ERROR);
|
|
36
|
+
}
|
|
37
|
+
async commitTransaction() {
|
|
38
|
+
throw new Error(D1_TRANSACTION_ERROR);
|
|
39
|
+
}
|
|
40
|
+
async rollbackTransaction() {
|
|
41
|
+
throw new Error(D1_TRANSACTION_ERROR);
|
|
42
|
+
}
|
|
43
|
+
async releaseConnection() {
|
|
44
|
+
}
|
|
45
|
+
async destroy() {
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
class D1Connection {
|
|
49
|
+
constructor(database) {
|
|
50
|
+
this.database = database;
|
|
51
|
+
}
|
|
52
|
+
async executeQuery(compiledQuery) {
|
|
53
|
+
const statement = this.database.prepare(compiledQuery.sql).bind(...compiledQuery.parameters);
|
|
54
|
+
const result = await statement.all();
|
|
55
|
+
if (result.success === false) {
|
|
56
|
+
throw new Error(result.error ?? "Cloudflare D1 query failed.");
|
|
57
|
+
}
|
|
58
|
+
if (result.error) throw new Error(result.error);
|
|
59
|
+
const changes = result.meta?.changes;
|
|
60
|
+
const lastRowId = result.meta?.last_row_id;
|
|
61
|
+
return {
|
|
62
|
+
rows: result.results ?? [],
|
|
63
|
+
numAffectedRows: changes === void 0 || changes === null ? void 0 : BigInt(changes),
|
|
64
|
+
insertId: lastRowId === void 0 || lastRowId === null ? void 0 : BigInt(lastRowId)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async *streamQuery(compiledQuery) {
|
|
68
|
+
yield await this.executeQuery(compiledQuery);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Native helpers for the D1 operations that need batch-level atomicity. */
|
|
2
|
+
import type { D1Result } from './d1-dialect.js';
|
|
3
|
+
export type D1SqlStatement = {
|
|
4
|
+
sql: string;
|
|
5
|
+
parameters?: readonly unknown[];
|
|
6
|
+
};
|
|
7
|
+
export declare function d1All<T = Record<string, unknown>>(sql: string, ...parameters: unknown[]): Promise<T[]>;
|
|
8
|
+
export declare function d1Run(sql: string, ...parameters: unknown[]): Promise<D1Result>;
|
|
9
|
+
/**
|
|
10
|
+
* D1 runs all statements in a batch sequentially and atomically. It is the
|
|
11
|
+
* native replacement for the local BEGIN IMMEDIATE paths in this provider.
|
|
12
|
+
*/
|
|
13
|
+
export declare function d1Batch(statements: readonly D1SqlStatement[]): Promise<D1Result[]>;
|