@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 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, token, namespace, database } = this.getConfig();
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.217";
7636
- const WASM_VERSION = "0.0.1-canary.217";
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.217",
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.217",
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.217",
64
- "@spooky-sync/ssp-wasm": "0.0.1-canary.217",
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
- getClient: () => ({ authenticate: opts.authenticate ?? vi.fn(async () => undefined) }),
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, token, namespace, database } = this.getConfig();
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