@flowerforce/flowerbase-client 0.1.1-beta.0

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/LICENSE +3 -0
  3. package/README.md +209 -0
  4. package/dist/app.d.ts +85 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +461 -0
  7. package/dist/bson.d.ts +8 -0
  8. package/dist/bson.d.ts.map +1 -0
  9. package/dist/bson.js +10 -0
  10. package/dist/credentials.d.ts +8 -0
  11. package/dist/credentials.d.ts.map +1 -0
  12. package/dist/credentials.js +30 -0
  13. package/dist/functions.d.ts +6 -0
  14. package/dist/functions.d.ts.map +1 -0
  15. package/dist/functions.js +47 -0
  16. package/dist/http.d.ts +35 -0
  17. package/dist/http.d.ts.map +1 -0
  18. package/dist/http.js +170 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +16 -0
  22. package/dist/mongo.d.ts +4 -0
  23. package/dist/mongo.d.ts.map +1 -0
  24. package/dist/mongo.js +106 -0
  25. package/dist/session.d.ts +18 -0
  26. package/dist/session.d.ts.map +1 -0
  27. package/dist/session.js +105 -0
  28. package/dist/session.native.d.ts +14 -0
  29. package/dist/session.native.d.ts.map +1 -0
  30. package/dist/session.native.js +76 -0
  31. package/dist/types.d.ts +97 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +2 -0
  34. package/dist/user.d.ts +37 -0
  35. package/dist/user.d.ts.map +1 -0
  36. package/dist/user.js +125 -0
  37. package/dist/watch.d.ts +3 -0
  38. package/dist/watch.d.ts.map +1 -0
  39. package/dist/watch.js +139 -0
  40. package/jest.config.ts +13 -0
  41. package/package.json +41 -0
  42. package/project.json +11 -0
  43. package/rollup.config.js +17 -0
  44. package/src/__tests__/auth.test.ts +213 -0
  45. package/src/__tests__/compat.test.ts +22 -0
  46. package/src/__tests__/functions.test.ts +312 -0
  47. package/src/__tests__/mongo.test.ts +83 -0
  48. package/src/__tests__/session.test.ts +597 -0
  49. package/src/__tests__/watch.test.ts +336 -0
  50. package/src/app.ts +562 -0
  51. package/src/bson.ts +6 -0
  52. package/src/credentials.ts +31 -0
  53. package/src/functions.ts +56 -0
  54. package/src/http.ts +221 -0
  55. package/src/index.ts +15 -0
  56. package/src/mongo.ts +112 -0
  57. package/src/session.native.ts +89 -0
  58. package/src/session.ts +114 -0
  59. package/src/types.ts +114 -0
  60. package/src/user.ts +150 -0
  61. package/src/watch.ts +156 -0
  62. package/tsconfig.json +34 -0
  63. package/tsconfig.spec.json +13 -0
package/dist/user.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ import type { App } from './app';
2
+ import { MongoClientLike, UserLike } from './types';
3
+ export declare class User implements UserLike {
4
+ readonly id: string;
5
+ customData: Record<string, unknown>;
6
+ profile?: {
7
+ email?: string;
8
+ [key: string]: unknown;
9
+ };
10
+ private readonly app;
11
+ private _providerType;
12
+ private readonly listeners;
13
+ functions: Record<string, (...args: unknown[]) => Promise<unknown>> & {
14
+ callFunction: (name: string, ...args: unknown[]) => Promise<unknown>;
15
+ callFunctionStreaming: (name: string, ...args: unknown[]) => Promise<AsyncIterable<Uint8Array>>;
16
+ };
17
+ constructor(app: App, id: string);
18
+ get state(): "active" | "logged-out" | "removed";
19
+ get isLoggedIn(): boolean;
20
+ get providerType(): string | null;
21
+ get identities(): unknown[];
22
+ private resolveCustomDataFromToken;
23
+ get accessToken(): string | null;
24
+ get refreshToken(): string | null;
25
+ setProviderType(providerType: string): void;
26
+ private decodeAccessTokenPayload;
27
+ logOut(): Promise<void>;
28
+ callFunction(name: string, ...args: unknown[]): Promise<any>;
29
+ refreshAccessToken(): Promise<string>;
30
+ refreshCustomData(): Promise<Record<string, unknown>>;
31
+ mongoClient(serviceName: string): MongoClientLike;
32
+ addListener(callback: () => void): void;
33
+ removeListener(callback: () => void): void;
34
+ removeAllListeners(): void;
35
+ notifyListeners(): void;
36
+ }
37
+ //# sourceMappingURL=user.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../src/user.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAGhC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAEnD,qBAAa,IAAK,YAAW,QAAQ;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAK;IACxC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAA,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;IACnD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAK;IACzB,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAElD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG;QACpE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;QACpE,qBAAqB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAA;KAChG,CAAA;gBAEW,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM;IAUhC,IAAI,KAAK,wCAKR;IAED,IAAI,UAAU,YAEb;IAED,IAAI,YAAY,kBAEf;IAED,IAAI,UAAU,cAEb;IAED,OAAO,CAAC,0BAA0B;IAclC,IAAI,WAAW,kBAId;IAED,IAAI,YAAY,kBAIf;IAED,eAAe,CAAC,YAAY,EAAE,MAAM;IAIpC,OAAO,CAAC,wBAAwB;IAuB1B,MAAM;IAIN,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE;IAI7C,kBAAkB;IAMlB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAU3D,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe;IAIjD,WAAW,CAAC,QAAQ,EAAE,MAAM,IAAI;IAIhC,cAAc,CAAC,QAAQ,EAAE,MAAM,IAAI;IAInC,kBAAkB;IAIlB,eAAe;CAShB"}
package/dist/user.js ADDED
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.User = void 0;
4
+ const functions_1 = require("./functions");
5
+ const mongo_1 = require("./mongo");
6
+ class User {
7
+ constructor(app, id) {
8
+ this.customData = {};
9
+ this._providerType = null;
10
+ this.listeners = new Set();
11
+ this.app = app;
12
+ this.id = id;
13
+ this.functions = (0, functions_1.createFunctionsProxy)((name, args) => this.app.callFunction(name, args, this.id), (name, args) => this.app.callFunctionStreaming(name, args, this.id));
14
+ this.customData = this.resolveCustomDataFromToken();
15
+ }
16
+ get state() {
17
+ if (!this.app.hasUser(this.id)) {
18
+ return 'removed';
19
+ }
20
+ return this.isLoggedIn ? 'active' : 'logged-out';
21
+ }
22
+ get isLoggedIn() {
23
+ return this.accessToken !== null && this.refreshToken !== null;
24
+ }
25
+ get providerType() {
26
+ return this._providerType;
27
+ }
28
+ get identities() {
29
+ return this.app.getProfileSnapshot(this.id)?.identities ?? [];
30
+ }
31
+ resolveCustomDataFromToken() {
32
+ const payload = this.decodeAccessTokenPayload();
33
+ if (!payload)
34
+ return {};
35
+ return ('user_data' in payload && payload.user_data && typeof payload.user_data === 'object'
36
+ ? payload.user_data
37
+ : 'userData' in payload && payload.userData && typeof payload.userData === 'object'
38
+ ? payload.userData
39
+ : 'custom_data' in payload && payload.custom_data && typeof payload.custom_data === 'object'
40
+ ? payload.custom_data
41
+ : {});
42
+ }
43
+ get accessToken() {
44
+ const session = this.app.getSession(this.id);
45
+ if (!session)
46
+ return null;
47
+ return session.accessToken;
48
+ }
49
+ get refreshToken() {
50
+ const session = this.app.getSession(this.id);
51
+ if (!session)
52
+ return null;
53
+ return session.refreshToken;
54
+ }
55
+ setProviderType(providerType) {
56
+ this._providerType = providerType;
57
+ }
58
+ decodeAccessTokenPayload() {
59
+ if (!this.accessToken)
60
+ return null;
61
+ const parts = this.accessToken.split('.');
62
+ if (parts.length < 2)
63
+ return null;
64
+ const base64Url = parts[1];
65
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(base64Url.length / 4) * 4, '=');
66
+ const decodeBase64 = (input) => {
67
+ if (typeof atob === 'function')
68
+ return atob(input);
69
+ const runtimeBuffer = globalThis.Buffer;
70
+ if (runtimeBuffer)
71
+ return runtimeBuffer.from(input, 'base64').toString('utf8');
72
+ return '';
73
+ };
74
+ try {
75
+ const decoded = decodeBase64(base64);
76
+ return JSON.parse(decoded);
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ async logOut() {
83
+ await this.app.logoutUser(this.id);
84
+ }
85
+ async callFunction(name, ...args) {
86
+ return this.app.callFunction(name, args, this.id);
87
+ }
88
+ async refreshAccessToken() {
89
+ const accessToken = await this.app.refreshAccessToken(this.id);
90
+ this.customData = this.resolveCustomDataFromToken();
91
+ return accessToken;
92
+ }
93
+ async refreshCustomData() {
94
+ const profile = await this.app.getProfile(this.id);
95
+ this.profile = profile.data;
96
+ this.customData = (profile.custom_data && typeof profile.custom_data === 'object'
97
+ ? profile.custom_data
98
+ : this.resolveCustomDataFromToken());
99
+ this.notifyListeners();
100
+ return this.customData;
101
+ }
102
+ mongoClient(serviceName) {
103
+ return (0, mongo_1.createMongoClient)(this.app, serviceName, this.id);
104
+ }
105
+ addListener(callback) {
106
+ this.listeners.add(callback);
107
+ }
108
+ removeListener(callback) {
109
+ this.listeners.delete(callback);
110
+ }
111
+ removeAllListeners() {
112
+ this.listeners.clear();
113
+ }
114
+ notifyListeners() {
115
+ for (const callback of Array.from(this.listeners)) {
116
+ try {
117
+ callback();
118
+ }
119
+ catch {
120
+ // Listener failures should not break user lifecycle operations.
121
+ }
122
+ }
123
+ }
124
+ }
125
+ exports.User = User;
@@ -0,0 +1,3 @@
1
+ import { WatchAsyncIterator, WatchConfig } from './types';
2
+ export declare const createWatchIterator: (config: WatchConfig) => WatchAsyncIterator<unknown>;
3
+ //# sourceMappingURL=watch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAyCzD,eAAO,MAAM,mBAAmB,WAAY,WAAW,KAAG,mBAAmB,OAAO,CAiHnF,CAAA"}
package/dist/watch.js ADDED
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createWatchIterator = void 0;
4
+ const bson_1 = require("./bson");
5
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6
+ const createWatchRequest = ({ database, collection, filter, ids }) => ({
7
+ name: 'watch',
8
+ service: 'mongodb-atlas',
9
+ arguments: [
10
+ {
11
+ database,
12
+ collection,
13
+ ...(filter ? { filter } : {}),
14
+ ...(ids ? { ids } : {})
15
+ }
16
+ ]
17
+ });
18
+ const toBase64 = (input) => {
19
+ if (typeof btoa === 'function') {
20
+ return btoa(input);
21
+ }
22
+ throw new Error('Base64 encoder not available in current runtime');
23
+ };
24
+ const parseSsePayload = (line) => {
25
+ if (!line.startsWith('data:'))
26
+ return null;
27
+ const raw = line.slice(5).trim();
28
+ if (!raw)
29
+ return null;
30
+ try {
31
+ return bson_1.EJSON.deserialize(JSON.parse(raw));
32
+ }
33
+ catch {
34
+ return raw;
35
+ }
36
+ };
37
+ const createWatchIterator = (config) => {
38
+ let closed = false;
39
+ let activeController = null;
40
+ const queue = [];
41
+ const waiters = [];
42
+ const enqueue = (value) => {
43
+ const waiter = waiters.shift();
44
+ if (waiter) {
45
+ waiter({ done: false, value });
46
+ return;
47
+ }
48
+ queue.push(value);
49
+ };
50
+ const close = () => {
51
+ if (closed)
52
+ return;
53
+ closed = true;
54
+ activeController?.abort();
55
+ while (waiters.length > 0) {
56
+ const resolve = waiters.shift();
57
+ resolve?.({ done: true, value: undefined });
58
+ }
59
+ };
60
+ const run = async () => {
61
+ let attempts = 0;
62
+ while (!closed) {
63
+ const controller = new AbortController();
64
+ activeController = controller;
65
+ const request = createWatchRequest(config);
66
+ const encoded = toBase64(JSON.stringify(bson_1.EJSON.serialize(request, { relaxed: false })));
67
+ const url = `${config.baseUrl}/api/client/v2.0/app/${config.appId}/functions/call?baas_request=${encodeURIComponent(encoded)}`;
68
+ try {
69
+ const response = await fetch(url, {
70
+ method: 'GET',
71
+ headers: {
72
+ Authorization: `Bearer ${config.accessToken}`,
73
+ Accept: 'text/event-stream'
74
+ },
75
+ signal: controller.signal
76
+ });
77
+ if (!response.ok || !response.body) {
78
+ throw new Error(`Watch request failed (${response.status})`);
79
+ }
80
+ attempts = 0;
81
+ const reader = response.body.getReader();
82
+ const decoder = new TextDecoder();
83
+ let buffer = '';
84
+ while (!closed) {
85
+ const { done, value } = await reader.read();
86
+ if (done)
87
+ break;
88
+ buffer += decoder.decode(value, { stream: true });
89
+ const lines = buffer.split('\n');
90
+ buffer = lines.pop() ?? '';
91
+ for (const line of lines) {
92
+ const parsed = parseSsePayload(line);
93
+ if (parsed !== null) {
94
+ enqueue(parsed);
95
+ }
96
+ }
97
+ }
98
+ }
99
+ catch {
100
+ if (closed) {
101
+ break;
102
+ }
103
+ }
104
+ if (closed) {
105
+ break;
106
+ }
107
+ attempts += 1;
108
+ const backoff = Math.min(5000, 250 * 2 ** (attempts - 1));
109
+ await sleep(backoff);
110
+ }
111
+ };
112
+ void run();
113
+ return {
114
+ [Symbol.asyncIterator]() {
115
+ return this;
116
+ },
117
+ next() {
118
+ if (queue.length > 0) {
119
+ return Promise.resolve({ done: false, value: queue.shift() });
120
+ }
121
+ if (closed) {
122
+ return Promise.resolve({ done: true, value: undefined });
123
+ }
124
+ return new Promise((resolve) => {
125
+ waiters.push(resolve);
126
+ });
127
+ },
128
+ return() {
129
+ close();
130
+ return Promise.resolve({ done: true, value: undefined });
131
+ },
132
+ throw(error) {
133
+ close();
134
+ return Promise.reject(error);
135
+ },
136
+ close
137
+ };
138
+ };
139
+ exports.createWatchIterator = createWatchIterator;
package/jest.config.ts ADDED
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ preset: '../../jest.preset.js',
3
+ transform: {
4
+ '^.+\\.[tj]s$': [
5
+ 'ts-jest',
6
+ {
7
+ tsconfig: '<rootDir>/tsconfig.spec.json'
8
+ }
9
+ ]
10
+ },
11
+ testEnvironment: 'node',
12
+ testMatch: ['<rootDir>/src/**/*.test.ts']
13
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@flowerforce/flowerbase-client",
3
+ "version": "0.1.1-beta.0",
4
+ "description": "Client for Flowerbase",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "keywords": [],
8
+ "author": "",
9
+ "license": "MIT",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/flowerforce/flowerbase.git"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ }
22
+ },
23
+ "scripts": {
24
+ "test": "npx jest",
25
+ "build": "rm -rf dist/ && tsc"
26
+ },
27
+ "dependencies": {
28
+ "bson": "^6.10.4"
29
+ },
30
+ "peerDependencies": {
31
+ "@react-native-async-storage/async-storage": "^2.2.0"
32
+ },
33
+ "peerDependenciesMeta": {
34
+ "@react-native-async-storage/async-storage": {
35
+ "optional": true
36
+ }
37
+ },
38
+ "devDependencies": {
39
+ "@react-native-async-storage/async-storage": "^2.2.0"
40
+ }
41
+ }
package/project.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "flowerbase-client",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/flowerbase-client/src",
5
+ "projectType": "library",
6
+ "tags": [
7
+ "flowerbase",
8
+ "client"
9
+ ],
10
+ "targets": {}
11
+ }
@@ -0,0 +1,17 @@
1
+ const { withNx } = require('@nx/rollup/with-nx')
2
+
3
+ module.exports = withNx(
4
+ {
5
+ main: './src/index.ts',
6
+ outputPath: './dist',
7
+ tsConfig: './tsconfig.lib.json',
8
+ compiler: 'tsc',
9
+ format: ['cjs', 'esm'],
10
+ generateExportsField: true
11
+ },
12
+ {
13
+ // Provide additional rollup configuration here. See: https://rollupjs.org/configuration-options
14
+ // e.g.
15
+ // output: { sourcemap: true },
16
+ }
17
+ )
@@ -0,0 +1,213 @@
1
+ import { App } from '../app'
2
+ import { Credentials } from '../credentials'
3
+
4
+ describe('flowerbase-client auth', () => {
5
+ const originalFetch = global.fetch
6
+ const originalLocalStorage = (globalThis as typeof globalThis & { localStorage?: unknown }).localStorage
7
+
8
+ afterEach(() => {
9
+ global.fetch = originalFetch
10
+ if (typeof originalLocalStorage === 'undefined') {
11
+ Reflect.deleteProperty(globalThis, 'localStorage')
12
+ } else {
13
+ Object.defineProperty(globalThis, 'localStorage', {
14
+ configurable: true,
15
+ value: originalLocalStorage
16
+ })
17
+ }
18
+ })
19
+
20
+ it('logs in with email/password', async () => {
21
+ global.fetch = jest
22
+ .fn()
23
+ .mockResolvedValueOnce({
24
+ ok: true,
25
+ text: async () => JSON.stringify({
26
+ access_token: 'access',
27
+ refresh_token: 'refresh',
28
+ user_id: 'user-1'
29
+ })
30
+ })
31
+ .mockResolvedValueOnce({
32
+ ok: true,
33
+ text: async () => JSON.stringify({ access_token: 'access-from-session' })
34
+ }) as unknown as typeof fetch
35
+
36
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
37
+ const user = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
38
+
39
+ expect(user.id).toBe('user-1')
40
+ expect(app.currentUser?.id).toBe('user-1')
41
+ expect(global.fetch).toHaveBeenCalledWith(
42
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/login',
43
+ expect.objectContaining({ method: 'POST' })
44
+ )
45
+ expect(global.fetch).toHaveBeenCalledWith(
46
+ 'http://localhost:3000/api/client/v2.0/auth/session',
47
+ expect.objectContaining({
48
+ method: 'POST',
49
+ headers: expect.objectContaining({ Authorization: 'Bearer refresh' })
50
+ })
51
+ )
52
+ })
53
+
54
+ it('logs in with anonymous provider', async () => {
55
+ global.fetch = jest.fn().mockResolvedValue({
56
+ ok: true,
57
+ text: async () => JSON.stringify({
58
+ access_token: 'access',
59
+ refresh_token: 'refresh',
60
+ user_id: 'anon-1'
61
+ })
62
+ }) as unknown as typeof fetch
63
+
64
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
65
+ const user = await app.logIn(Credentials.anonymous())
66
+
67
+ expect(user.id).toBe('anon-1')
68
+ expect(global.fetch).toHaveBeenCalledWith(
69
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/anon-user/login',
70
+ expect.objectContaining({ method: 'POST' })
71
+ )
72
+ })
73
+
74
+ it('logs in with custom function provider', async () => {
75
+ global.fetch = jest.fn().mockResolvedValue({
76
+ ok: true,
77
+ text: async () => JSON.stringify({
78
+ access_token: 'access',
79
+ refresh_token: 'refresh',
80
+ user_id: 'custom-1'
81
+ })
82
+ }) as unknown as typeof fetch
83
+
84
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
85
+ const user = await app.logIn(Credentials.function({ token: 'abc' }))
86
+
87
+ expect(user.id).toBe('custom-1')
88
+ expect(global.fetch).toHaveBeenCalledWith(
89
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/custom-function/login',
90
+ expect.objectContaining({ method: 'POST' })
91
+ )
92
+ })
93
+
94
+ it('logs in with custom jwt provider', async () => {
95
+ global.fetch = jest.fn().mockResolvedValue({
96
+ ok: true,
97
+ text: async () => JSON.stringify({
98
+ access_token: 'access',
99
+ refresh_token: 'refresh',
100
+ user_id: 'jwt-1'
101
+ })
102
+ }) as unknown as typeof fetch
103
+
104
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
105
+ const user = await app.logIn(Credentials.jwt('jwt-token'))
106
+
107
+ expect(user.id).toBe('jwt-1')
108
+ expect(global.fetch).toHaveBeenCalledWith(
109
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/custom-token/login',
110
+ expect.objectContaining({ method: 'POST' })
111
+ )
112
+ })
113
+
114
+ it('supports register and reset endpoints', async () => {
115
+ global.fetch = jest.fn().mockResolvedValue({
116
+ ok: true,
117
+ text: async () => JSON.stringify({ status: 'ok' })
118
+ }) as unknown as typeof fetch
119
+
120
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
121
+
122
+ await app.emailPasswordAuth.registerUser({ email: 'john@doe.com', password: 'secret123' })
123
+ await app.emailPasswordAuth.sendResetPasswordEmail('john@doe.com')
124
+ await app.emailPasswordAuth.callResetPasswordFunction('john@doe.com', 'new-secret', 'extra')
125
+ await app.emailPasswordAuth.resetPassword({ token: 't1', tokenId: 't2', password: 'new-secret' })
126
+
127
+ expect(global.fetch).toHaveBeenNthCalledWith(
128
+ 1,
129
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/register',
130
+ expect.objectContaining({ method: 'POST' })
131
+ )
132
+ expect(global.fetch).toHaveBeenNthCalledWith(
133
+ 2,
134
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset/send',
135
+ expect.objectContaining({ method: 'POST' })
136
+ )
137
+ expect(global.fetch).toHaveBeenNthCalledWith(
138
+ 3,
139
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset/call',
140
+ expect.objectContaining({ method: 'POST' })
141
+ )
142
+ expect(global.fetch).toHaveBeenNthCalledWith(
143
+ 4,
144
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset',
145
+ expect.objectContaining({ method: 'POST' })
146
+ )
147
+ })
148
+
149
+ it('supports email/password confirmation endpoints', async () => {
150
+ global.fetch = jest.fn().mockResolvedValue({
151
+ ok: true,
152
+ text: async () => JSON.stringify({ status: 'ok' })
153
+ }) as unknown as typeof fetch
154
+
155
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
156
+
157
+ await app.emailPasswordAuth.confirmUser({ token: 't1', tokenId: 'tid1' })
158
+ await app.emailPasswordAuth.resendConfirmationEmail({ email: 'john@doe.com' })
159
+ await app.emailPasswordAuth.retryCustomConfirmation({ email: 'john@doe.com' })
160
+
161
+ expect(global.fetch).toHaveBeenNthCalledWith(
162
+ 1,
163
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/confirm',
164
+ expect.objectContaining({ method: 'POST' })
165
+ )
166
+ expect(global.fetch).toHaveBeenNthCalledWith(
167
+ 2,
168
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/confirm/send',
169
+ expect.objectContaining({ method: 'POST' })
170
+ )
171
+ expect(global.fetch).toHaveBeenNthCalledWith(
172
+ 3,
173
+ 'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/confirm/call',
174
+ expect.objectContaining({ method: 'POST' })
175
+ )
176
+ })
177
+
178
+ it('calls session endpoint on app load when browser session exists', async () => {
179
+ const storage = new Map<string, string>()
180
+ storage.set(
181
+ 'flowerbase:my-app:session',
182
+ JSON.stringify({ accessToken: 'old-access', refreshToken: 'refresh', userId: 'user-1' })
183
+ )
184
+ Object.defineProperty(globalThis, 'localStorage', {
185
+ configurable: true,
186
+ value: {
187
+ getItem: (key: string) => storage.get(key) ?? null,
188
+ setItem: (key: string, value: string) => {
189
+ storage.set(key, value)
190
+ },
191
+ removeItem: (key: string) => {
192
+ storage.delete(key)
193
+ }
194
+ }
195
+ })
196
+
197
+ global.fetch = jest.fn().mockResolvedValue({
198
+ ok: true,
199
+ text: async () => JSON.stringify({ access_token: 'fresh-access' })
200
+ }) as unknown as typeof fetch
201
+
202
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
203
+ await app.getProfile().catch(() => undefined)
204
+
205
+ expect(global.fetch).toHaveBeenCalledWith(
206
+ 'http://localhost:3000/api/client/v2.0/auth/session',
207
+ expect.objectContaining({
208
+ method: 'POST',
209
+ headers: expect.objectContaining({ Authorization: 'Bearer refresh' })
210
+ })
211
+ )
212
+ })
213
+ })
@@ -0,0 +1,22 @@
1
+ import * as Flowerbase from '../index'
2
+
3
+ describe('flowerbase-client compatibility surface', () => {
4
+ it('exposes Realm-like symbols', () => {
5
+ expect(typeof Flowerbase.App).toBe('function')
6
+ expect(typeof Flowerbase.App.getApp).toBe('function')
7
+ expect(Flowerbase.App.Credentials).toBe(Flowerbase.Credentials)
8
+ expect(typeof Flowerbase.Credentials.emailPassword).toBe('function')
9
+ expect(typeof Flowerbase.Credentials.anonymous).toBe('function')
10
+ expect(typeof Flowerbase.Credentials.function).toBe('function')
11
+ expect(typeof Flowerbase.Credentials.jwt).toBe('function')
12
+ expect(typeof Flowerbase.MongoDBRealmError).toBe('function')
13
+ expect(typeof Flowerbase.BSON.ObjectId).toBe('function')
14
+ expect(Flowerbase.ObjectID).toBe(Flowerbase.ObjectId)
15
+ })
16
+
17
+ it('returns singleton app for same id', () => {
18
+ const a1 = Flowerbase.App.getApp('singleton-app')
19
+ const a2 = Flowerbase.App.getApp('singleton-app')
20
+ expect(a1).toBe(a2)
21
+ })
22
+ })