@spooky-sync/core 0.0.1-canary.317 → 0.0.1-canary.319
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.js
CHANGED
|
@@ -2178,8 +2178,8 @@ function trimHistory(history, cap) {
|
|
|
2178
2178
|
|
|
2179
2179
|
//#endregion
|
|
2180
2180
|
//#region src/modules/devtools/index.ts
|
|
2181
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
2182
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
2181
|
+
const CORE_VERSION = "0.0.1-canary.319";
|
|
2182
|
+
const WASM_VERSION = "0.0.1-canary.319";
|
|
2183
2183
|
const SURREAL_VERSION = "3.0.3";
|
|
2184
2184
|
var DevToolsService = class DevToolsService {
|
|
2185
2185
|
version = CORE_VERSION;
|
|
@@ -11574,7 +11574,7 @@ var AuthService = class {
|
|
|
11574
11574
|
* callback is called immediately with current value and whenever validation status changes.
|
|
11575
11575
|
*/
|
|
11576
11576
|
subscribe(cb) {
|
|
11577
|
-
cb(this.
|
|
11577
|
+
cb(this.announcedUserId());
|
|
11578
11578
|
const id = this.events.subscribe(AuthEventTypes.AuthStateChanged, (event) => {
|
|
11579
11579
|
cb(event.payload);
|
|
11580
11580
|
});
|
|
@@ -11614,8 +11614,24 @@ var AuthService = class {
|
|
|
11614
11614
|
clearTimeout(timer);
|
|
11615
11615
|
}
|
|
11616
11616
|
}
|
|
11617
|
+
/**
|
|
11618
|
+
* The signed-in user as subscribers see it: the raw `table:id` string, the
|
|
11619
|
+
* way the token's `ID` claim spells it. `currentUser.id` is that string while
|
|
11620
|
+
* the session is restored from the token and the server's `RecordId` once
|
|
11621
|
+
* `check()` has answered, and the two must announce as the same user: every
|
|
11622
|
+
* subscriber that compares ids (feature flags, app release) took the switch
|
|
11623
|
+
* for a new principal and reset, so flag-gated UI blinked off and back on a
|
|
11624
|
+
* few seconds into every reload. A `RecordId`'s own `toString()` will not do
|
|
11625
|
+
* either, as it escapes ids like `user:⟨5abc⟩` that the claim spells raw.
|
|
11626
|
+
*/
|
|
11627
|
+
announcedUserId() {
|
|
11628
|
+
const id = this.currentUser?.id;
|
|
11629
|
+
if (!id) return null;
|
|
11630
|
+
if (typeof id === "string") return id;
|
|
11631
|
+
return id instanceof RecordId$1 ? encodeRecordId(id) : String(id);
|
|
11632
|
+
}
|
|
11617
11633
|
notifyListeners() {
|
|
11618
|
-
const userId = this.
|
|
11634
|
+
const userId = this.announcedUserId();
|
|
11619
11635
|
this.events.emit(AuthEventTypes.AuthStateChanged, userId);
|
|
11620
11636
|
const impersonation = this.impersonation;
|
|
11621
11637
|
if (!sameImpersonation(impersonation, this.announcedImpersonation)) {
|
|
@@ -14766,7 +14782,7 @@ function createTabsCoordinator(config, s, host) {
|
|
|
14766
14782
|
return new TabsCoordinator({
|
|
14767
14783
|
tabId: s.tabId,
|
|
14768
14784
|
fingerprint: computeTabsFingerprint({
|
|
14769
|
-
coreVersion: "0.0.1-canary.
|
|
14785
|
+
coreVersion: "0.0.1-canary.319",
|
|
14770
14786
|
schemaHash: hash53(config.schemaSurql),
|
|
14771
14787
|
endpoint: config.database.endpoint ?? "",
|
|
14772
14788
|
namespace: config.database.namespace,
|
package/dist/types.d.ts
CHANGED
|
@@ -842,6 +842,17 @@ declare class AuthService<S extends SchemaStructure> {
|
|
|
842
842
|
*/
|
|
843
843
|
onBeforeSignOut(hook: SignOutHook): () => void;
|
|
844
844
|
private runSignOutHooks;
|
|
845
|
+
/**
|
|
846
|
+
* The signed-in user as subscribers see it: the raw `table:id` string, the
|
|
847
|
+
* way the token's `ID` claim spells it. `currentUser.id` is that string while
|
|
848
|
+
* the session is restored from the token and the server's `RecordId` once
|
|
849
|
+
* `check()` has answered, and the two must announce as the same user: every
|
|
850
|
+
* subscriber that compares ids (feature flags, app release) took the switch
|
|
851
|
+
* for a new principal and reset, so flag-gated UI blinked off and back on a
|
|
852
|
+
* few seconds into every reload. A `RecordId`'s own `toString()` will not do
|
|
853
|
+
* either, as it escapes ids like `user:⟨5abc⟩` that the claim spells raw.
|
|
854
|
+
*/
|
|
855
|
+
private announcedUserId;
|
|
845
856
|
private notifyListeners;
|
|
846
857
|
/**
|
|
847
858
|
* The impersonation the current session is, or null. Derived from the
|
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.319",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -75,8 +75,8 @@
|
|
|
75
75
|
}
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@spooky-sync/query-builder": "0.0.1-canary.
|
|
79
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
78
|
+
"@spooky-sync/query-builder": "0.0.1-canary.319",
|
|
79
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.319",
|
|
80
80
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
81
81
|
"@surrealdb/wasm": "^3.0.3",
|
|
82
82
|
"blurhash": "^2.0.5",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { RecordId } from 'surrealdb';
|
|
2
3
|
import { AuthService } from './index';
|
|
3
4
|
|
|
4
5
|
// A SurrealDB record-access JWT carries the access method as `AC` and the
|
|
@@ -139,3 +140,30 @@ describe('auth token reaches the transport', () => {
|
|
|
139
140
|
expect(remote.setAuthToken).toHaveBeenLastCalledWith(null);
|
|
140
141
|
});
|
|
141
142
|
});
|
|
143
|
+
|
|
144
|
+
describe('the announced user id', () => {
|
|
145
|
+
// The cached token names the user as a raw string; once check() answers,
|
|
146
|
+
// currentUser holds the server's RecordId. Subscribers compare ids, so the
|
|
147
|
+
// two must announce identically or every reload reads as a sign-in switch.
|
|
148
|
+
it.each([['abc'], ['5onl3w8g9jhnymk3d10p']])(
|
|
149
|
+
'is the same raw string before and after the server check (user:%s)',
|
|
150
|
+
async (key) => {
|
|
151
|
+
const claim = `user:${key}`;
|
|
152
|
+
const { auth } = makeAuth({
|
|
153
|
+
token: jwt({ AC: 'account', ID: claim }),
|
|
154
|
+
query: vi.fn(async () => [{ id: new RecordId('user', key), email: 'x@y' }]),
|
|
155
|
+
});
|
|
156
|
+
const seen: (string | null)[] = [];
|
|
157
|
+
auth.subscribe((uid) => seen.push(uid));
|
|
158
|
+
|
|
159
|
+
await auth.restoreSessionFromToken();
|
|
160
|
+
await auth.check();
|
|
161
|
+
|
|
162
|
+
expect(auth.currentUser.id).toBeInstanceOf(RecordId);
|
|
163
|
+
// A RecordId's toString escapes a key that starts with a digit.
|
|
164
|
+
expect(seen.filter((v) => v !== null)).toEqual([claim, claim]);
|
|
165
|
+
expect(seen.every((v) => v === null || typeof v === 'string')).toBe(true);
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
@@ -11,6 +11,7 @@ import { AuthEventTypes, createAuthEventSystem } from './events/index';
|
|
|
11
11
|
import type { PersistenceClient } from '../../types';
|
|
12
12
|
import { classifySyncError } from '../../utils/error-classification';
|
|
13
13
|
import { encodeRecordId } from '../../utils/index';
|
|
14
|
+
import { RecordId } from 'surrealdb';
|
|
14
15
|
import {
|
|
15
16
|
decodeTokenClaims,
|
|
16
17
|
impersonationFromToken,
|
|
@@ -152,7 +153,7 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
152
153
|
*/
|
|
153
154
|
subscribe(cb: (userId: string | null) => void): () => void {
|
|
154
155
|
// Immediate callback
|
|
155
|
-
cb(this.
|
|
156
|
+
cb(this.announcedUserId());
|
|
156
157
|
|
|
157
158
|
const id = this.events.subscribe(AuthEventTypes.AuthStateChanged, (event) => {
|
|
158
159
|
cb(event.payload);
|
|
@@ -197,8 +198,25 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
/**
|
|
202
|
+
* The signed-in user as subscribers see it: the raw `table:id` string, the
|
|
203
|
+
* way the token's `ID` claim spells it. `currentUser.id` is that string while
|
|
204
|
+
* the session is restored from the token and the server's `RecordId` once
|
|
205
|
+
* `check()` has answered, and the two must announce as the same user: every
|
|
206
|
+
* subscriber that compares ids (feature flags, app release) took the switch
|
|
207
|
+
* for a new principal and reset, so flag-gated UI blinked off and back on a
|
|
208
|
+
* few seconds into every reload. A `RecordId`'s own `toString()` will not do
|
|
209
|
+
* either, as it escapes ids like `user:⟨5abc⟩` that the claim spells raw.
|
|
210
|
+
*/
|
|
211
|
+
private announcedUserId(): string | null {
|
|
212
|
+
const id = this.currentUser?.id;
|
|
213
|
+
if (!id) return null;
|
|
214
|
+
if (typeof id === 'string') return id;
|
|
215
|
+
return id instanceof RecordId ? encodeRecordId(id as RecordId<string>) : String(id);
|
|
216
|
+
}
|
|
217
|
+
|
|
200
218
|
private notifyListeners() {
|
|
201
|
-
const userId = this.
|
|
219
|
+
const userId = this.announcedUserId();
|
|
202
220
|
this.events.emit(AuthEventTypes.AuthStateChanged, userId);
|
|
203
221
|
const impersonation = this.impersonation;
|
|
204
222
|
if (!sameImpersonation(impersonation, this.announcedImpersonation)) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { RecordId } from 'surrealdb';
|
|
2
3
|
import { FeatureFlagModule } from './index';
|
|
4
|
+
import { AuthService } from '../auth/index';
|
|
3
5
|
|
|
4
6
|
// vitest runs in the `node` environment here, so there is no localStorage.
|
|
5
7
|
// The module guards every access with `globalThis.localStorage?.`, which keeps
|
|
@@ -249,3 +251,48 @@ describe('FeatureFlagModule local overrides', () => {
|
|
|
249
251
|
expect(() => new FeatureFlagModule(makeDeps().deps)).not.toThrow();
|
|
250
252
|
});
|
|
251
253
|
});
|
|
254
|
+
|
|
255
|
+
describe('FeatureFlagModule across a reload', () => {
|
|
256
|
+
// A reload restores the session from the cached token (the id as a string),
|
|
257
|
+
// then the server check lands the user row (the id as a RecordId) a few
|
|
258
|
+
// seconds later. Same user: an assigned flag must stay on throughout, not
|
|
259
|
+
// blink off while the module re-registers its query.
|
|
260
|
+
it('keeps flags through the server verification of the same user', async () => {
|
|
261
|
+
const b64 = Buffer.from(JSON.stringify({ AC: 'account', ID: 'user:5abc' })).toString('base64url');
|
|
262
|
+
const token = `h.${b64}.s`;
|
|
263
|
+
const store = new Map<string, unknown>([['sp00ky_auth_token', token]]);
|
|
264
|
+
const persistence = {
|
|
265
|
+
get: async (k: string) => store.get(k) ?? null,
|
|
266
|
+
set: async (k: string, v: unknown) => void store.set(k, v),
|
|
267
|
+
remove: async (k: string) => void store.delete(k),
|
|
268
|
+
} as any;
|
|
269
|
+
const remote = {
|
|
270
|
+
query: async () => [{ id: new RecordId('user', '5abc') }],
|
|
271
|
+
setAuthToken: () => undefined,
|
|
272
|
+
getClient: () => ({ authenticate: async () => undefined, invalidate: async () => undefined }),
|
|
273
|
+
} as any;
|
|
274
|
+
const silent: any = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
|
|
275
|
+
silent.child = () => silent;
|
|
276
|
+
const auth = new AuthService({} as any, remote, persistence, silent);
|
|
277
|
+
|
|
278
|
+
const env = makeDeps();
|
|
279
|
+
const flags = new FeatureFlagModule({ ...env.deps, auth } as any);
|
|
280
|
+
flags.init();
|
|
281
|
+
await auth.restoreSessionFromToken();
|
|
282
|
+
const clubs = flags.feature('clubs');
|
|
283
|
+
const seen: (string | undefined)[] = [];
|
|
284
|
+
clubs.subscribe((s) => seen.push(s.variant));
|
|
285
|
+
await tick();
|
|
286
|
+
env.push([{ key: 'clubs', variant: 'on' }]);
|
|
287
|
+
expect(clubs.variant()).toBe('on');
|
|
288
|
+
const registrations = env.calls.length;
|
|
289
|
+
|
|
290
|
+
await auth.check();
|
|
291
|
+
await tick();
|
|
292
|
+
|
|
293
|
+
expect(clubs.variant()).toBe('on');
|
|
294
|
+
expect(seen.slice(seen.indexOf('on'))).not.toContain(undefined);
|
|
295
|
+
expect(env.calls.length).toBe(registrations);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|