@spooky-sync/core 0.0.1-canary.217 → 0.0.1-canary.218
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/dist/index.d.ts +29 -0
- package/dist/index.js +41 -4
- package/package.json +3 -3
- package/src/modules/auth/auth.local-first.test.ts +41 -1
- package/src/modules/auth/index.ts +11 -1
- package/src/services/database/remote.ts +36 -1
- package/src/sp00ky.ts +4 -0
package/dist/index.d.ts
CHANGED
|
@@ -57,8 +57,37 @@ declare class RemoteDatabaseService extends AbstractDatabaseService {
|
|
|
57
57
|
* racing two sockets. Cleared on settle, so a later call always reconnects.
|
|
58
58
|
*/
|
|
59
59
|
private connecting;
|
|
60
|
+
/**
|
|
61
|
+
* The token to re-authenticate a freshly opened socket with, kept current by
|
|
62
|
+
* {@link setAuthToken}.
|
|
63
|
+
*
|
|
64
|
+
* `config.token` is fixed at construction and most apps never set it: they
|
|
65
|
+
* sign in later, which authenticates the socket that happens to be open at
|
|
66
|
+
* the time. That is enough for the SDK's OWN reconnects (it replays
|
|
67
|
+
* `version`/`use`/`authenticate` itself), but not for the supervisor's revive
|
|
68
|
+
* loop, which builds a socket from scratch. Without this the revived socket
|
|
69
|
+
* came back UNAUTHENTICATED and stayed that way for the life of the page,
|
|
70
|
+
* while the client's `currentUserId` — restored from local storage — kept
|
|
71
|
+
* reporting the user as signed in.
|
|
72
|
+
*
|
|
73
|
+
* The visible damage is silent and total: `fn::query::register` sends
|
|
74
|
+
* `<string>($auth.id OR '')`, so every view registered afterwards is stamped
|
|
75
|
+
* with an empty identity, and every `$auth.id` predicate in it resolves
|
|
76
|
+
* false. Public tables keep returning rows while everything owned by the user
|
|
77
|
+
* returns nothing, on a page that still looks signed in.
|
|
78
|
+
*/
|
|
79
|
+
private authToken;
|
|
60
80
|
constructor(config: Sp00kyConfig<any>['database'], logger: Logger$1);
|
|
61
81
|
getConfig(): Sp00kyConfig<any>['database'];
|
|
82
|
+
/**
|
|
83
|
+
* Record the token every future connect should authenticate with, or `null`
|
|
84
|
+
* on sign-out. See {@link authToken}.
|
|
85
|
+
*
|
|
86
|
+
* Does not touch the CURRENT socket: callers authenticate that themselves
|
|
87
|
+
* (sign-in and session restore both already do). This only makes the next
|
|
88
|
+
* from-scratch connect reproduce that state.
|
|
89
|
+
*/
|
|
90
|
+
setAuthToken(token: string | null): void;
|
|
62
91
|
/** Resolved reconnect tunables; the supervisor reads its own knobs here. */
|
|
63
92
|
getReconnectConfig(): Required<ReconnectConfig>;
|
|
64
93
|
/** Current transport state as reported by the SDK. */
|
package/dist/index.js
CHANGED
|
@@ -1004,6 +1004,26 @@ var RemoteDatabaseService = class extends AbstractDatabaseService {
|
|
|
1004
1004
|
* racing two sockets. Cleared on settle, so a later call always reconnects.
|
|
1005
1005
|
*/
|
|
1006
1006
|
connecting = null;
|
|
1007
|
+
/**
|
|
1008
|
+
* The token to re-authenticate a freshly opened socket with, kept current by
|
|
1009
|
+
* {@link setAuthToken}.
|
|
1010
|
+
*
|
|
1011
|
+
* `config.token` is fixed at construction and most apps never set it: they
|
|
1012
|
+
* sign in later, which authenticates the socket that happens to be open at
|
|
1013
|
+
* the time. That is enough for the SDK's OWN reconnects (it replays
|
|
1014
|
+
* `version`/`use`/`authenticate` itself), but not for the supervisor's revive
|
|
1015
|
+
* loop, which builds a socket from scratch. Without this the revived socket
|
|
1016
|
+
* came back UNAUTHENTICATED and stayed that way for the life of the page,
|
|
1017
|
+
* while the client's `currentUserId` — restored from local storage — kept
|
|
1018
|
+
* reporting the user as signed in.
|
|
1019
|
+
*
|
|
1020
|
+
* The visible damage is silent and total: `fn::query::register` sends
|
|
1021
|
+
* `<string>($auth.id OR '')`, so every view registered afterwards is stamped
|
|
1022
|
+
* with an empty identity, and every `$auth.id` predicate in it resolves
|
|
1023
|
+
* false. Public tables keep returning rows while everything owned by the user
|
|
1024
|
+
* returns nothing, on a page that still looks signed in.
|
|
1025
|
+
*/
|
|
1026
|
+
authToken = null;
|
|
1007
1027
|
constructor(config, logger) {
|
|
1008
1028
|
const events = createDatabaseEventSystem();
|
|
1009
1029
|
super(new Surreal({ engines: applyDiagnostics(createRemoteEngines(), ({ key, type, phase, ...other }) => {
|
|
@@ -1023,6 +1043,17 @@ var RemoteDatabaseService = class extends AbstractDatabaseService {
|
|
|
1023
1043
|
getConfig() {
|
|
1024
1044
|
return this.config;
|
|
1025
1045
|
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Record the token every future connect should authenticate with, or `null`
|
|
1048
|
+
* on sign-out. See {@link authToken}.
|
|
1049
|
+
*
|
|
1050
|
+
* Does not touch the CURRENT socket: callers authenticate that themselves
|
|
1051
|
+
* (sign-in and session restore both already do). This only makes the next
|
|
1052
|
+
* from-scratch connect reproduce that state.
|
|
1053
|
+
*/
|
|
1054
|
+
setAuthToken(token) {
|
|
1055
|
+
this.authToken = token;
|
|
1056
|
+
}
|
|
1026
1057
|
/** Resolved reconnect tunables; the supervisor reads its own knobs here. */
|
|
1027
1058
|
getReconnectConfig() {
|
|
1028
1059
|
return this.reconnectConfig;
|
|
@@ -1069,7 +1100,8 @@ var RemoteDatabaseService = class extends AbstractDatabaseService {
|
|
|
1069
1100
|
return this.connecting;
|
|
1070
1101
|
}
|
|
1071
1102
|
async doConnect() {
|
|
1072
|
-
const { endpoint,
|
|
1103
|
+
const { endpoint, namespace, database } = this.getConfig();
|
|
1104
|
+
const token = this.authToken ?? this.getConfig().token;
|
|
1073
1105
|
if (endpoint) {
|
|
1074
1106
|
this.logger.info({
|
|
1075
1107
|
endpoint,
|
|
@@ -7632,8 +7664,8 @@ function selfAllowlistedVariant(flag, userId) {
|
|
|
7632
7664
|
|
|
7633
7665
|
//#endregion
|
|
7634
7666
|
//#region src/modules/devtools/index.ts
|
|
7635
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
7636
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
7667
|
+
const CORE_VERSION = "0.0.1-canary.218";
|
|
7668
|
+
const WASM_VERSION = "0.0.1-canary.218";
|
|
7637
7669
|
const SURREAL_VERSION = "3.0.3";
|
|
7638
7670
|
var DevToolsService = class DevToolsService {
|
|
7639
7671
|
eventsHistory = [];
|
|
@@ -8254,6 +8286,7 @@ var AuthService = class {
|
|
|
8254
8286
|
const { access, userId } = decodeTokenClaims(token);
|
|
8255
8287
|
if (!userId) return null;
|
|
8256
8288
|
this.token = token;
|
|
8289
|
+
this.remote.setAuthToken(token);
|
|
8257
8290
|
this.currentUser = { id: userId };
|
|
8258
8291
|
this.isAuthenticated = true;
|
|
8259
8292
|
this.access = access ?? this.defaultAccessName();
|
|
@@ -8278,6 +8311,7 @@ var AuthService = class {
|
|
|
8278
8311
|
this.notifyListeners();
|
|
8279
8312
|
return;
|
|
8280
8313
|
}
|
|
8314
|
+
this.remote.setAuthToken(token);
|
|
8281
8315
|
await this.remote.getClient().authenticate(token);
|
|
8282
8316
|
const result = await this.remote.query("SELECT * FROM ONLY $auth.id");
|
|
8283
8317
|
const items = Array.isArray(result) && Array.isArray(result[0]) ? result[0] : result;
|
|
@@ -8326,6 +8360,7 @@ var AuthService = class {
|
|
|
8326
8360
|
*/
|
|
8327
8361
|
async signOut() {
|
|
8328
8362
|
this.token = null;
|
|
8363
|
+
this.remote.setAuthToken(null);
|
|
8329
8364
|
this.currentUser = null;
|
|
8330
8365
|
this.isAuthenticated = false;
|
|
8331
8366
|
this.access = null;
|
|
@@ -8337,6 +8372,7 @@ var AuthService = class {
|
|
|
8337
8372
|
}
|
|
8338
8373
|
async setSession(token, user) {
|
|
8339
8374
|
this.token = token;
|
|
8375
|
+
this.remote.setAuthToken(token);
|
|
8340
8376
|
this.currentUser = user;
|
|
8341
8377
|
this.isAuthenticated = true;
|
|
8342
8378
|
this.access = decodeAccessFromToken(token) ?? this.defaultAccessName();
|
|
@@ -12644,7 +12680,7 @@ var Sp00kyClient = class {
|
|
|
12644
12680
|
return new TabsCoordinator({
|
|
12645
12681
|
tabId,
|
|
12646
12682
|
fingerprint: computeTabsFingerprint({
|
|
12647
|
-
coreVersion: "0.0.1-canary.
|
|
12683
|
+
coreVersion: "0.0.1-canary.218",
|
|
12648
12684
|
schemaHash: hash53(this.config.schemaSurql),
|
|
12649
12685
|
endpoint: this.config.database.endpoint ?? "",
|
|
12650
12686
|
namespace: this.config.database.namespace,
|
|
@@ -13055,6 +13091,7 @@ var Sp00kyClient = class {
|
|
|
13055
13091
|
return this.appReleases.release(app, options);
|
|
13056
13092
|
}
|
|
13057
13093
|
authenticate(token) {
|
|
13094
|
+
this.remote.setAuthToken(token);
|
|
13058
13095
|
return this.remote.getClient().authenticate(token);
|
|
13059
13096
|
}
|
|
13060
13097
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spooky-sync/core",
|
|
3
|
-
"version": "0.0.1-canary.
|
|
3
|
+
"version": "0.0.1-canary.218",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@spooky-sync/query-builder": "0.0.1-canary.
|
|
64
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
63
|
+
"@spooky-sync/query-builder": "0.0.1-canary.218",
|
|
64
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.218",
|
|
65
65
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
66
66
|
"@surrealdb/wasm": "^3.0.3",
|
|
67
67
|
"blurhash": "^2.0.5",
|
|
@@ -22,7 +22,11 @@ function makeAuth(opts: { token?: string | null; query?: any; authenticate?: any
|
|
|
22
22
|
} as any;
|
|
23
23
|
const remote = {
|
|
24
24
|
query: opts.query ?? vi.fn(async () => [[]]),
|
|
25
|
-
|
|
25
|
+
setAuthToken: vi.fn(),
|
|
26
|
+
getClient: () => ({
|
|
27
|
+
authenticate: opts.authenticate ?? vi.fn(async () => undefined),
|
|
28
|
+
invalidate: vi.fn(async () => undefined),
|
|
29
|
+
}),
|
|
26
30
|
} as any;
|
|
27
31
|
return { auth: new AuthService({} as any, remote, persistence, silentLogger()), remote, persistence, store };
|
|
28
32
|
}
|
|
@@ -99,3 +103,39 @@ describe('check() error handling', () => {
|
|
|
99
103
|
expect(store.get('sp00ky_auth_token')).toBeUndefined();
|
|
100
104
|
});
|
|
101
105
|
});
|
|
106
|
+
|
|
107
|
+
// The transport authenticates a NEW socket from the token it was handed, not
|
|
108
|
+
// from the one the app passed at construction (most apps pass none and sign in
|
|
109
|
+
// later). If sign-in never reaches it, the supervisor's revive loop rebuilds a
|
|
110
|
+
// socket that comes back anonymous and stays that way for the life of the page,
|
|
111
|
+
// while `currentUser` — restored from local storage — keeps the UI signed in.
|
|
112
|
+
// Every view registered after that carries `auth_id = ''`, so `$auth.id`
|
|
113
|
+
// predicates resolve false and the user's own rows silently vanish.
|
|
114
|
+
describe('auth token reaches the transport', () => {
|
|
115
|
+
it('hands the token to the transport when restoring from cache', async () => {
|
|
116
|
+
const token = jwt({ AC: 'account', ID: 'user:abc' });
|
|
117
|
+
const { auth, remote } = makeAuth({ token });
|
|
118
|
+
|
|
119
|
+
await auth.restoreSessionFromToken();
|
|
120
|
+
|
|
121
|
+
expect(remote.setAuthToken).toHaveBeenCalledWith(token);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('hands the token to the transport on check()', async () => {
|
|
125
|
+
const token = jwt({ AC: 'account', ID: 'user:abc' });
|
|
126
|
+
const { auth, remote } = makeAuth({ token });
|
|
127
|
+
|
|
128
|
+
await auth.check();
|
|
129
|
+
|
|
130
|
+
expect(remote.setAuthToken).toHaveBeenCalledWith(token);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('clears it on sign-out so a revived socket does not come back as the old user', async () => {
|
|
134
|
+
const { auth, remote } = makeAuth({ token: jwt({ AC: 'account', ID: 'user:abc' }) });
|
|
135
|
+
await auth.restoreSessionFromToken();
|
|
136
|
+
|
|
137
|
+
await auth.signOut();
|
|
138
|
+
|
|
139
|
+
expect(remote.setAuthToken).toHaveBeenLastCalledWith(null);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -153,6 +153,11 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
153
153
|
if (!userId) return null;
|
|
154
154
|
|
|
155
155
|
this.token = token;
|
|
156
|
+
// Hand it to the transport too, so a socket rebuilt from scratch later
|
|
157
|
+
// (the supervisor's revive loop) comes back authenticated. Without this the
|
|
158
|
+
// page kept reporting this user while its session was anonymous, and every
|
|
159
|
+
// view registered afterwards was stamped with an empty identity.
|
|
160
|
+
this.remote.setAuthToken(token);
|
|
156
161
|
// Only the id: the full row is not in the token. It lands from the local
|
|
157
162
|
// cache when the app's own `user` query paints, and is replaced wholesale
|
|
158
163
|
// by `check()` once the server answers.
|
|
@@ -187,7 +192,10 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
187
192
|
return;
|
|
188
193
|
}
|
|
189
194
|
|
|
190
|
-
// Authenticate with the token
|
|
195
|
+
// Authenticate with the token, and record it for future connects so a
|
|
196
|
+
// socket rebuilt from scratch reproduces this identity (see
|
|
197
|
+
// `RemoteDatabaseService.setAuthToken`).
|
|
198
|
+
this.remote.setAuthToken(token);
|
|
191
199
|
await this.remote.getClient().authenticate(token);
|
|
192
200
|
|
|
193
201
|
// Verify the session by fetching the full user record using $auth.id
|
|
@@ -259,6 +267,7 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
259
267
|
*/
|
|
260
268
|
async signOut() {
|
|
261
269
|
this.token = null;
|
|
270
|
+
this.remote.setAuthToken(null);
|
|
262
271
|
this.currentUser = null;
|
|
263
272
|
this.isAuthenticated = false;
|
|
264
273
|
this.access = null;
|
|
@@ -276,6 +285,7 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
276
285
|
|
|
277
286
|
private async setSession(token: string, user: any) {
|
|
278
287
|
this.token = token;
|
|
288
|
+
this.remote.setAuthToken(token);
|
|
279
289
|
this.currentUser = user;
|
|
280
290
|
this.isAuthenticated = true;
|
|
281
291
|
// Resolve the access-method name (e.g. "account") for in-browser SSP
|
|
@@ -40,6 +40,26 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
|
|
|
40
40
|
* racing two sockets. Cleared on settle, so a later call always reconnects.
|
|
41
41
|
*/
|
|
42
42
|
private connecting: Promise<void> | null = null;
|
|
43
|
+
/**
|
|
44
|
+
* The token to re-authenticate a freshly opened socket with, kept current by
|
|
45
|
+
* {@link setAuthToken}.
|
|
46
|
+
*
|
|
47
|
+
* `config.token` is fixed at construction and most apps never set it: they
|
|
48
|
+
* sign in later, which authenticates the socket that happens to be open at
|
|
49
|
+
* the time. That is enough for the SDK's OWN reconnects (it replays
|
|
50
|
+
* `version`/`use`/`authenticate` itself), but not for the supervisor's revive
|
|
51
|
+
* loop, which builds a socket from scratch. Without this the revived socket
|
|
52
|
+
* came back UNAUTHENTICATED and stayed that way for the life of the page,
|
|
53
|
+
* while the client's `currentUserId` — restored from local storage — kept
|
|
54
|
+
* reporting the user as signed in.
|
|
55
|
+
*
|
|
56
|
+
* The visible damage is silent and total: `fn::query::register` sends
|
|
57
|
+
* `<string>($auth.id OR '')`, so every view registered afterwards is stamped
|
|
58
|
+
* with an empty identity, and every `$auth.id` predicate in it resolves
|
|
59
|
+
* false. Public tables keep returning rows while everything owned by the user
|
|
60
|
+
* returns nothing, on a page that still looks signed in.
|
|
61
|
+
*/
|
|
62
|
+
private authToken: string | null = null;
|
|
43
63
|
|
|
44
64
|
constructor(config: Sp00kyConfig<any>['database'], logger: Logger) {
|
|
45
65
|
const events = createDatabaseEventSystem();
|
|
@@ -76,6 +96,18 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
|
|
|
76
96
|
return this.config;
|
|
77
97
|
}
|
|
78
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Record the token every future connect should authenticate with, or `null`
|
|
101
|
+
* on sign-out. See {@link authToken}.
|
|
102
|
+
*
|
|
103
|
+
* Does not touch the CURRENT socket: callers authenticate that themselves
|
|
104
|
+
* (sign-in and session restore both already do). This only makes the next
|
|
105
|
+
* from-scratch connect reproduce that state.
|
|
106
|
+
*/
|
|
107
|
+
setAuthToken(token: string | null): void {
|
|
108
|
+
this.authToken = token;
|
|
109
|
+
}
|
|
110
|
+
|
|
79
111
|
/** Resolved reconnect tunables; the supervisor reads its own knobs here. */
|
|
80
112
|
getReconnectConfig(): Required<ReconnectConfig> {
|
|
81
113
|
return this.reconnectConfig;
|
|
@@ -130,7 +162,10 @@ export class RemoteDatabaseService extends AbstractDatabaseService {
|
|
|
130
162
|
}
|
|
131
163
|
|
|
132
164
|
private async doConnect(): Promise<void> {
|
|
133
|
-
const { endpoint,
|
|
165
|
+
const { endpoint, namespace, database } = this.getConfig();
|
|
166
|
+
// The live token wins over the constructor-time one: this connect may be
|
|
167
|
+
// the supervisor rebuilding a socket long after sign-in. See `authToken`.
|
|
168
|
+
const token = this.authToken ?? this.getConfig().token;
|
|
134
169
|
if (endpoint) {
|
|
135
170
|
this.logger.info(
|
|
136
171
|
{
|
package/src/sp00ky.ts
CHANGED
|
@@ -1304,6 +1304,10 @@ export class Sp00kyClient<S extends SchemaStructure> {
|
|
|
1304
1304
|
}
|
|
1305
1305
|
|
|
1306
1306
|
authenticate(token: string) {
|
|
1307
|
+
// Record it before authenticating, so a socket the supervisor rebuilds
|
|
1308
|
+
// later comes back as this identity rather than anonymous. See
|
|
1309
|
+
// `RemoteDatabaseService.setAuthToken`.
|
|
1310
|
+
this.remote.setAuthToken(token);
|
|
1307
1311
|
return this.remote.getClient().authenticate(token);
|
|
1308
1312
|
}
|
|
1309
1313
|
|