@rebasepro/client 0.17.3 → 0.18.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 +4 -0
- package/dist/auth.d.ts +80 -0
- package/dist/functions.d.ts +6 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.es.js +471 -94
- package/dist/index.es.js.map +1 -1
- package/dist/offline-connectivity.d.ts +12 -1
- package/dist/offline.d.ts +23 -1
- package/dist/query-contract.types.d.ts +30 -0
- package/dist/realtime-channel.d.ts +29 -1
- package/dist/sdk_query_builder.d.ts +21 -2
- package/dist/transport.d.ts +24 -0
- package/package.json +28 -15
- package/src/admin.ts +0 -90
- package/src/anonymous-client-guard.test.ts +0 -190
- package/src/api-keys.ts +0 -87
- package/src/auth-listener-errors.test.ts +0 -57
- package/src/auth-refresh-overflow.test.ts +0 -89
- package/src/auth.ts +0 -982
- package/src/backups.ts +0 -40
- package/src/client-close.test.ts +0 -80
- package/src/collection-listen-meta.test.ts +0 -105
- package/src/collection-observe.test.ts +0 -138
- package/src/collection.test.ts +0 -293
- package/src/collection.ts +0 -525
- package/src/cron.test.ts +0 -164
- package/src/cron.ts +0 -62
- package/src/data-proxy.test.ts +0 -183
- package/src/errors.ts +0 -9
- package/src/functions.ts +0 -82
- package/src/index.ts +0 -639
- package/src/like-pattern-redos.test.ts +0 -61
- package/src/offline-codec.ts +0 -79
- package/src/offline-connectivity.test.ts +0 -191
- package/src/offline-connectivity.ts +0 -255
- package/src/offline-idb-store.test.ts +0 -340
- package/src/offline-integration.test.ts +0 -180
- package/src/offline-query.test.ts +0 -431
- package/src/offline-query.ts +0 -529
- package/src/offline-store.ts +0 -357
- package/src/offline-sync-engine.test.ts +0 -857
- package/src/offline.test.ts +0 -897
- package/src/offline.ts +0 -1928
- package/src/query-contract.types.ts +0 -206
- package/src/query_builder.ts +0 -1
- package/src/realtime-channel.test.ts +0 -542
- package/src/realtime-channel.ts +0 -539
- package/src/realtime-concurrent-subscribe.test.ts +0 -102
- package/src/realtime-error-surfacing.test.ts +0 -105
- package/src/realtime-optout.test.ts +0 -279
- package/src/realtime-row-identity.test.ts +0 -254
- package/src/realtime-subscription-key.test.ts +0 -92
- package/src/reviver.ts +0 -39
- package/src/sdk_query_builder.ts +0 -206
- package/src/storage-key-encoding.test.ts +0 -65
- package/src/storage-registry.ts +0 -102
- package/src/storage.ts +0 -253
- package/src/transport-baseurl.test.ts +0 -101
- package/src/transport.ts +0 -505
- package/src/vector-search-listen.test.ts +0 -42
- package/src/vector-search-query.test.ts +0 -55
- package/src/websocket-url.test.ts +0 -97
- package/src/websocket.ts +0 -1837
package/src/api-keys.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type { Transport } from "./transport";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* These were re-declared here, under a comment saying they lived in the server
|
|
5
|
-
* package rather than in `@rebasepro/types`. That stopped being true, and the
|
|
6
|
-
* copy drifted: it never gained `admin`, the flag that grants a key the `admin`
|
|
7
|
-
* role — admin routes plus the RLS `default_admin` policies — so the SDK could
|
|
8
|
-
* describe every kind of key except a privileged one, and
|
|
9
|
-
* `createKey({ …, admin: true })` was an excess-property error.
|
|
10
|
-
*/
|
|
11
|
-
export type {
|
|
12
|
-
ApiKeyPermission,
|
|
13
|
-
ApiKeyMasked,
|
|
14
|
-
ApiKeyWithSecret,
|
|
15
|
-
CreateApiKeyRequest,
|
|
16
|
-
UpdateApiKeyRequest
|
|
17
|
-
} from "@rebasepro/types";
|
|
18
|
-
|
|
19
|
-
import type {
|
|
20
|
-
ApiKeyMasked,
|
|
21
|
-
ApiKeyWithSecret,
|
|
22
|
-
CreateApiKeyRequest,
|
|
23
|
-
UpdateApiKeyRequest
|
|
24
|
-
} from "@rebasepro/types";
|
|
25
|
-
|
|
26
|
-
/** Options for the `createApiKeys` factory. */
|
|
27
|
-
export interface CreateApiKeysOptions {
|
|
28
|
-
apiKeysPath?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Creates a client for managing API keys via the admin routes.
|
|
33
|
-
*
|
|
34
|
-
* @param transport - The shared HTTP transport created by `createTransport`.
|
|
35
|
-
* @param options - Optional overrides (e.g. a custom base path).
|
|
36
|
-
*/
|
|
37
|
-
export function createApiKeys(transport: Transport, options?: CreateApiKeysOptions) {
|
|
38
|
-
const apiKeysPath = options?.apiKeysPath || "/admin/api-keys";
|
|
39
|
-
|
|
40
|
-
/** List all API keys (masked). */
|
|
41
|
-
async function listKeys(): Promise<{ keys: ApiKeyMasked[] }> {
|
|
42
|
-
return transport.request<{ keys: ApiKeyMasked[] }>(apiKeysPath, { method: "GET" });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Get a single API key by ID (masked). */
|
|
46
|
-
async function getKey(id: string): Promise<{ key: ApiKeyMasked }> {
|
|
47
|
-
return transport.request<{ key: ApiKeyMasked }>(
|
|
48
|
-
apiKeysPath + "/" + encodeURIComponent(id),
|
|
49
|
-
{ method: "GET" }
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Create a new API key. The full secret is included in the response. */
|
|
54
|
-
async function createKey(data: CreateApiKeyRequest): Promise<{ key: ApiKeyWithSecret }> {
|
|
55
|
-
return transport.request<{ key: ApiKeyWithSecret }>(apiKeysPath, {
|
|
56
|
-
method: "POST",
|
|
57
|
-
body: JSON.stringify(data)
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Update an existing API key. */
|
|
62
|
-
async function updateKey(id: string, data: UpdateApiKeyRequest): Promise<{ key: ApiKeyMasked }> {
|
|
63
|
-
return transport.request<{ key: ApiKeyMasked }>(
|
|
64
|
-
apiKeysPath + "/" + encodeURIComponent(id),
|
|
65
|
-
{
|
|
66
|
-
method: "PUT",
|
|
67
|
-
body: JSON.stringify(data)
|
|
68
|
-
}
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Revoke (soft-delete) an API key. */
|
|
73
|
-
async function revokeKey(id: string): Promise<{ success: boolean }> {
|
|
74
|
-
return transport.request<{ success: boolean }>(
|
|
75
|
-
apiKeysPath + "/" + encodeURIComponent(id),
|
|
76
|
-
{ method: "DELETE" }
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
listKeys,
|
|
82
|
-
getKey,
|
|
83
|
-
createKey,
|
|
84
|
-
updateKey,
|
|
85
|
-
revokeKey
|
|
86
|
-
};
|
|
87
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { jest } from "@jest/globals";
|
|
2
|
-
import { createAuth } from "./auth";
|
|
3
|
-
import type { Transport } from "./transport";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* What happens to an error thrown by the app's own auth listener.
|
|
7
|
-
*
|
|
8
|
-
* `emit` isolates listeners from each other, which is right — one bad handler
|
|
9
|
-
* must not stop the rest from being told about a sign-in. It also discarded the
|
|
10
|
-
* error entirely, which is not: the throw came from the *caller's* code, and it
|
|
11
|
-
* vanished with nothing in the console, so a broken `onAuthStateChange` handler
|
|
12
|
-
* looked like an event that never fired.
|
|
13
|
-
*
|
|
14
|
-
* The socket in this same package already reports these
|
|
15
|
-
* ("Error in channel handler:"). This was the one listener loop that did not.
|
|
16
|
-
*/
|
|
17
|
-
function transport(): Transport {
|
|
18
|
-
return {
|
|
19
|
-
request: jest.fn<any>().mockResolvedValue({}),
|
|
20
|
-
setToken: jest.fn(),
|
|
21
|
-
setAuthTokenGetter: jest.fn(),
|
|
22
|
-
setOnUnauthorized: jest.fn(),
|
|
23
|
-
baseUrl: "http://localhost:3000",
|
|
24
|
-
apiPath: "/api",
|
|
25
|
-
fetchFn: globalThis.fetch,
|
|
26
|
-
getHeaders: () => ({}),
|
|
27
|
-
resolveToken: jest.fn<any>().mockResolvedValue(null)
|
|
28
|
-
} as unknown as Transport;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe("auth state listeners", () => {
|
|
32
|
-
let error: ReturnType<typeof jest.spyOn>;
|
|
33
|
-
|
|
34
|
-
beforeEach(() => { error = jest.spyOn(console, "error").mockImplementation(() => { /* quiet */ }); });
|
|
35
|
-
afterEach(() => error.mockRestore());
|
|
36
|
-
|
|
37
|
-
it("keeps telling the other listeners when one throws", () => {
|
|
38
|
-
const auth = createAuth(transport());
|
|
39
|
-
const second = jest.fn();
|
|
40
|
-
|
|
41
|
-
auth.onAuthStateChange(() => { throw new Error("handler blew up"); });
|
|
42
|
-
auth.onAuthStateChange(second);
|
|
43
|
-
auth.signOut();
|
|
44
|
-
|
|
45
|
-
expect(second).toHaveBeenCalled();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("reports the throw instead of discarding it", () => {
|
|
49
|
-
const auth = createAuth(transport());
|
|
50
|
-
auth.onAuthStateChange(() => { throw new Error("handler blew up"); });
|
|
51
|
-
|
|
52
|
-
auth.signOut();
|
|
53
|
-
|
|
54
|
-
const said = error.mock.calls.map((c: unknown[]) => c.map(String).join(" ")).join("\n");
|
|
55
|
-
expect(said).toMatch(/handler blew up/);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { jest } from "@jest/globals";
|
|
2
|
-
import { createAuth, createMemoryStorage } from "./auth";
|
|
3
|
-
import type { Transport } from "./transport";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* How far ahead the token refresh is allowed to be scheduled.
|
|
7
|
-
*
|
|
8
|
-
* `scheduleRefresh` computes `(expiresAt - REFRESH_BUFFER_MS) - Date.now()` and
|
|
9
|
-
* hands it to `setTimeout`, with a floor for the past and no ceiling. Node and
|
|
10
|
-
* every browser store that delay in a **32-bit signed integer**: hand them more
|
|
11
|
-
* than 2,147,483,647 ms — about 24.8 days — and the timer does not wait, it
|
|
12
|
-
* clamps to 1 ms and fires immediately.
|
|
13
|
-
*
|
|
14
|
-
* `auth.accessExpiresIn` is configurable and defaults to `"1h"`, so this is
|
|
15
|
-
* dormant on a default deployment. Set it to `"30d"` — an ordinary choice for an
|
|
16
|
-
* internal tool or a kiosk — and every signed-in browser refreshes at once,
|
|
17
|
-
* receives a token expiring another 30 days out, schedules again, overflows
|
|
18
|
-
* again: a hot loop against `/auth/refresh`, one per open tab.
|
|
19
|
-
*
|
|
20
|
-
* A floor without a ceiling is the tell. Someone already knew the input was
|
|
21
|
-
* untrusted and bounded one side of it. (Class 23.)
|
|
22
|
-
*/
|
|
23
|
-
const MAX_DELAY = 2_147_483_647;
|
|
24
|
-
|
|
25
|
-
function transport(): Transport {
|
|
26
|
-
return {
|
|
27
|
-
request: jest.fn<any>().mockResolvedValue({}),
|
|
28
|
-
setToken: jest.fn(),
|
|
29
|
-
setAuthTokenGetter: jest.fn(),
|
|
30
|
-
setOnUnauthorized: jest.fn(),
|
|
31
|
-
baseUrl: "http://localhost:3000",
|
|
32
|
-
apiPath: "/api",
|
|
33
|
-
fetchFn: jest.fn<any>().mockResolvedValue({ ok: true, status: 200, json: async () => ({}) }),
|
|
34
|
-
getHeaders: () => ({}),
|
|
35
|
-
resolveToken: jest.fn<any>().mockResolvedValue(null)
|
|
36
|
-
} as unknown as Transport;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Storage already holding a session expiring `days` from now. */
|
|
40
|
-
function storageWithSession(days: number) {
|
|
41
|
-
const storage = createMemoryStorage();
|
|
42
|
-
storage.setItem("rebase_auth", JSON.stringify({
|
|
43
|
-
accessToken: "at",
|
|
44
|
-
refreshToken: "rt",
|
|
45
|
-
expiresAt: Date.now() + days * 24 * 60 * 60 * 1000,
|
|
46
|
-
user: { uid: "u1" }
|
|
47
|
-
}));
|
|
48
|
-
return storage;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
describe("scheduled token refresh", () => {
|
|
52
|
-
let delays: number[];
|
|
53
|
-
let realSetTimeout: typeof setTimeout;
|
|
54
|
-
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
delays = [];
|
|
57
|
-
realSetTimeout = globalThis.setTimeout;
|
|
58
|
-
globalThis.setTimeout = ((fn: () => void, ms?: number) => {
|
|
59
|
-
if (typeof ms === "number") delays.push(ms);
|
|
60
|
-
return realSetTimeout(() => { /* never run in this test */ }, 0);
|
|
61
|
-
}) as unknown as typeof setTimeout;
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
afterEach(() => { globalThis.setTimeout = realSetTimeout; });
|
|
65
|
-
|
|
66
|
-
it("schedules a normal one-hour token in the ordinary way", () => {
|
|
67
|
-
createAuth(transport(), { storage: storageWithSession(1 / 24) });
|
|
68
|
-
|
|
69
|
-
const scheduled = delays.filter(d => d > 1000);
|
|
70
|
-
expect(scheduled.length).toBeGreaterThan(0);
|
|
71
|
-
expect(Math.max(...scheduled)).toBeLessThanOrEqual(MAX_DELAY);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("never asks setTimeout for a delay it cannot hold", () => {
|
|
75
|
-
// 30 days: 2,592,000,000 ms, past the 32-bit ceiling.
|
|
76
|
-
createAuth(transport(), { storage: storageWithSession(30) });
|
|
77
|
-
|
|
78
|
-
for (const d of delays) expect(d).toBeLessThanOrEqual(MAX_DELAY);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("does not schedule a refresh for one millisecond from now", () => {
|
|
82
|
-
// The symptom of the overflow: the clamp makes it immediate, and the
|
|
83
|
-
// refresh that follows schedules another one just as far out.
|
|
84
|
-
createAuth(transport(), { storage: storageWithSession(90) });
|
|
85
|
-
|
|
86
|
-
const immediate = delays.filter(d => d > 0 && d < 1000);
|
|
87
|
-
expect(immediate).toEqual([]);
|
|
88
|
-
});
|
|
89
|
-
});
|