@qmuse/cloud-client 0.0.1-dev.0 → 0.0.1-dev.2

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 CHANGED
@@ -6,54 +6,60 @@ its control plane while large file bytes use a platform-specific Storage data
6
6
  plane inside this package.
7
7
 
8
8
  The package intentionally does not expose AppBase clients, Functions, Storage,
9
- TablesDB, endpoint, project ID, database ID, bucket ID, or permissions to
10
- business modules. A QMuse host adapter supplies narrow execution, Session and
11
- file transport capabilities.
9
+ TablesDB, endpoint, project ID, database ID, bucket ID, permissions, or a
10
+ platform-injection API to business modules. Platform implementations are fixed
11
+ by the `/web` and `/miniapp` package entry points.
12
+
13
+ The host initializes one client for the current JavaScript runtime before
14
+ mounting the application. Web and mini-program hosts may load the public
15
+ connection config differently while business modules always read the same
16
+ singleton:
12
17
 
13
18
  ```ts
14
- import { createQmuseCloudClient } from '@qmuse/cloud-client';
19
+ import { getQMuseClient } from '@qmuse/cloud-client';
20
+ import { initQMuseCloudClient } from '@qmuse/cloud-client/web';
15
21
 
16
- export const cloudClient = createQmuseCloudClient({
17
- execute: input => runtime.executeCloudFunction(input),
22
+ await initQMuseCloudClient({
23
+ config: () => hostRuntime.appwrite,
18
24
  });
19
25
 
20
- const result = await cloudClient.request<{ data: Order }>({
26
+ const result = await getQMuseClient().request<{ data: Order }>({
21
27
  method: 'GET',
22
28
  path: '/api/orders/order_1',
23
29
  });
24
30
  ```
25
31
 
26
- Each generated application owns its own users, credentials, sessions, roles,
27
- and policies. It does not reuse the QMuse platform login. AppBase Auth is only
28
- an internal shadow transport identity used for Function runtime headers and
29
- Storage permissions.
32
+ `initQMuseCloudClient` accepts a config object or a sync/async config loader. Calls
33
+ with the same normalized `endpoint`, `projectId`, and `functionId` are
34
+ idempotent and concurrent calls share the same initialization. Reinitializing
35
+ with different connection config is rejected.
36
+
37
+ Each generated application owns its own AppBase users, Sessions, roles and
38
+ policies. The AppBase Session is the only application login state. It supplies
39
+ Function runtime identity headers and Storage permissions; there is no second
40
+ application access/refresh-token layer.
30
41
 
31
- The host supplies an application credential store and the two narrow AppBase
32
- Account operations needed to establish/remove that shadow Session:
42
+ The selected package entry point supplies the AppBase Account capability. Login receives a
43
+ short-lived custom-token secret from the Function and exchanges it immediately;
44
+ the secret is never returned from `login`, persisted, logged or put in business
45
+ state.
33
46
 
34
47
  ```ts
35
- export const cloudClient = createQmuseCloudClient({
36
- execute: input => runtime.executeCloudFunction(input),
37
- auth: {
38
- credentialStore: secureCredentialStore,
39
- shadowSession: {
40
- createSession: input => account.createSession(input),
41
- deleteCurrentSession: () => account.deleteSession({ sessionId: 'current' }),
42
- },
43
- },
44
- });
48
+ import { getQMuseClient } from '@qmuse/cloud-client';
45
49
 
46
- await cloudClient.auth.login({ email, password });
50
+ const cloudClient = getQMuseClient();
51
+ await cloudClient.auth.login({ username, password });
47
52
  const currentUser = await cloudClient.auth.getCurrentUser();
48
53
  await cloudClient.auth.logout();
54
+ // Or revoke every AppBase Session for this user:
55
+ await cloudClient.auth.logoutAll();
49
56
  ```
50
57
 
51
- `login/register/refresh` receive a one-time shadow Custom Token from Hono and
52
- exchange it internally. The token secret is never persisted in the application
53
- credential store. Refresh is single-flight, rotates application credentials,
54
- and replaces the current shadow Session. Hosts must back `credentialStore` with
55
- their platform's secure storage policy; this package does not default refresh
56
- tokens to `localStorage`.
58
+ `register` creates the user without changing the current Session. `login`
59
+ exchanges the custom token internally. Session refresh is single-flight and is
60
+ performed before business requests when expiry is within five minutes. Web
61
+ hosts use AppBase's secure cookie; mini-program hosts persist AppBase fallback
62
+ Session headers in storage scoped by endpoint and project ID.
57
63
 
58
64
  Business file operations use a logical resource, never a bucket ID:
59
65
 
@@ -80,42 +86,47 @@ the backend must also expire abandoned intents. Preview/download access is
80
86
  authorized by the Function and may return a short-lived File Token URL or an
81
87
  authenticated URL that the Web transport converts to a disposable Blob URL.
82
88
 
83
- Platform transports are separate package entry points:
89
+ Initialization is available only from platform-specific entry points:
84
90
 
85
91
  ```ts
86
- import { createWebFileTransport } from '@qmuse/cloud-client/web';
87
- import { createMiniappFileTransport } from '@qmuse/cloud-client/miniapp';
92
+ // Web build
93
+ import { initQMuseCloudClient } from '@qmuse/cloud-client/web';
94
+
95
+ await initQMuseCloudClient({
96
+ config: () => window.__MUSE__.appwrite,
97
+ });
98
+
99
+ // Taro mini-program build
100
+ import { initQMuseCloudClient } from '@qmuse/cloud-client/miniapp';
101
+
102
+ await initQMuseCloudClient({
103
+ config: () => getMuseRuntime().appwrite,
104
+ });
88
105
  ```
89
106
 
90
- They migrate the proven file mechanics from the legacy `@qmuse/appwrite`
91
- implementation: Web `File`/`Blob`, Taro-style local paths, 5 MiB chunks,
107
+ The Web entry owns the Appwrite Web SDK and secure-cookie Session. The miniapp
108
+ entry owns Taro request, upload, storage, file-system and project-isolated
109
+ `X-Fallback-Cookies` Session handling. Both entries include the proven file
110
+ mechanics from the legacy `@qmuse/appwrite` implementation: Web `File`/`Blob`, Taro-style local paths, 5 MiB chunks,
92
111
  `Content-Range`, `X-Appwrite-ID` continuation, progress, fallback Session
93
112
  headers and mini-program temporary-file cleanup. They do not migrate TablesDB
94
113
  CRUD or the legacy anonymous `Role.any()` update/delete permissions.
95
114
 
96
- For Web hosts, bind an initialized `Functions` instance without exposing it to
97
- business modules:
115
+ For Web hosts, initialization therefore stays small:
98
116
 
99
117
  ```ts
100
- import { createAppBaseWebCloudClient } from '@qmuse/cloud-client';
118
+ import { initQMuseCloudClient } from '@qmuse/cloud-client/web';
101
119
 
102
- export const cloudClient = createAppBaseWebCloudClient(functions, {
103
- refreshSession,
120
+ export const cloudClient = await initQMuseCloudClient({
121
+ config: () => window.__MUSE__.appwrite,
104
122
  });
105
123
  ```
106
124
 
107
- The legacy portable adapter remains available during migration:
108
-
109
- ```ts
110
- import { createPortableCloudClient } from '@qmuse/cloud-client';
111
-
112
- export const cloudClient = createPortableCloudClient(appwrite);
113
- ```
125
+ `functionId` is the public physical Function ID assigned by the AppBase project.
126
+ It is fixed during initialization and is not supplied by business requests.
114
127
 
115
- New applications should bind Function, Session and file capabilities directly
116
- at their host bootstrap and should not depend on `@qmuse/appwrite` or
117
- `@qmuse/appwrite-runtime-sdk`. The portable adapter only translates this
118
- package's `path` field to the legacy `xpath` field for staged migration.
128
+ New applications should select one platform entry at build time and should not
129
+ depend on `@qmuse/appwrite` or `@qmuse/appwrite-runtime-sdk`.
119
130
 
120
131
  Only `/api` paths and an allowlist of business headers are accepted. The client
121
132
  uses synchronous Function execution, reads `responseStatusCode`, `responseBody`,
@@ -1,15 +1,50 @@
1
- import type { CloudAuthOptions, CloudExecution, CloudExecutionInput, CloudFileTransport, QmuseCloudClient } from './types.js';
1
+ import type { CloudAccountCapability, CloudAuthOptions, CloudExecution, CloudExecutionInput, CloudFileTransport, QmuseCloudClient } from './types.js';
2
+ export type AppBaseExecutionInput = Omit<CloudExecutionInput, 'functionId'> & {
3
+ functionId: string;
4
+ };
2
5
  export interface AppBaseFunctionsCapability {
3
- createExecution(input: CloudExecutionInput): Promise<CloudExecution>;
6
+ createExecution(input: AppBaseExecutionInput): Promise<CloudExecution>;
4
7
  }
5
- export interface PortableExecutionInput extends Omit<CloudExecutionInput, 'path'> {
8
+ export interface AppBaseFunctionsSdkCapability {
9
+ createExecution(...args: never[]): Promise<CloudExecution>;
10
+ }
11
+ export declare function createAppBaseFunctionsCapability(functions: AppBaseFunctionsSdkCapability): AppBaseFunctionsCapability;
12
+ export interface AppBaseAccountSdkCapability {
13
+ createSession(input: {
14
+ userId: string;
15
+ secret: string;
16
+ }): Promise<{
17
+ expire: string;
18
+ }>;
19
+ get(): Promise<{
20
+ $id: string;
21
+ email?: string;
22
+ labels?: string[];
23
+ }>;
24
+ getSession(input: {
25
+ sessionId: 'current';
26
+ }): Promise<{
27
+ expire: string;
28
+ }>;
29
+ updateSession(input: {
30
+ sessionId: 'current';
31
+ }): Promise<{
32
+ expire: string;
33
+ }>;
34
+ deleteSession(input: {
35
+ sessionId: 'current';
36
+ }): Promise<unknown>;
37
+ deleteSessions(): Promise<unknown>;
38
+ }
39
+ export declare function createAppBaseAccountCapability(account: AppBaseAccountSdkCapability): CloudAccountCapability;
40
+ export interface PortableExecutionInput extends Omit<AppBaseExecutionInput, 'path'> {
6
41
  xpath: string;
7
42
  }
8
43
  export interface PortableAppwriteCapability {
9
44
  executeFunction(input: PortableExecutionInput): Promise<CloudExecution>;
10
45
  }
11
46
  export interface HostAdapterOptions<FileSource = never> {
12
- refreshSession?(): Promise<void>;
47
+ functionId?: string;
13
48
  auth?: CloudAuthOptions;
14
49
  fileTransport?: CloudFileTransport<FileSource>;
15
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAYpB,MAAM,WAAW,0BAA0B;IACzC,eAAe,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACtE;AAED,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,mBAAmB,EAAE,MAAM,CAAC;IAC/E,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,eAAe,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACzE;AAED,MAAM,WAAW,kBAAkB,CAAC,UAAU,GAAG,KAAK;IACpD,cAAc,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,aAAa,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;CAChD;AAsBD,wBAAgB,2BAA2B,CAAC,UAAU,GAAG,KAAK,EAC5D,SAAS,EAAE,0BAA0B,EACrC,OAAO,GAAE,kBAAkB,CAAC,UAAU,CAAM,GAC3C,gBAAgB,CAAC,UAAU,CAAC,CAY9B;AAED,wBAAgB,yBAAyB,CAAC,UAAU,GAAG,KAAK,EAC1D,QAAQ,EAAE,0BAA0B,EACpC,OAAO,GAAE,kBAAkB,CAAC,UAAU,CAAM,GAC3C,gBAAgB,CAAC,UAAU,CAAC,CAe9B"}
1
+ {"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAGpB,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,mBAAmB,EAAE,YAAY,CAAC,GAAG;IAC5E,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,WAAW,0BAA0B;IACzC,eAAe,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACxE;AAED,MAAM,WAAW,6BAA6B;IAC5C,eAAe,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC5D;AAED,wBAAgB,gCAAgC,CAC9C,SAAS,EAAE,6BAA6B,GACvC,0BAA0B,CAU5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,aAAa,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtF,GAAG,IAAI,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACnE,UAAU,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,SAAS,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzE,aAAa,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,SAAS,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E,aAAa,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,SAAS,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;CACpC;AAED,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,2BAA2B,GAAG,sBAAsB,CAY3G;AAED,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACjF,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,eAAe,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACzE;AAED,MAAM,WAAW,kBAAkB,CAAC,UAAU,GAAG,KAAK;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,aAAa,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;CAChD;AAYD,wBAAgB,2BAA2B,CAAC,UAAU,GAAG,KAAK,EAC5D,SAAS,EAAE,0BAA0B,EACrC,OAAO,GAAE,kBAAkB,CAAC,UAAU,CAAM,GAC3C,gBAAgB,CAAC,UAAU,CAAC,CAS9B;AAED,wBAAgB,yBAAyB,CAAC,UAAU,GAAG,KAAK,EAC1D,QAAQ,EAAE,0BAA0B,EACpC,OAAO,GAAE,kBAAkB,CAAC,UAAU,CAAM,GAC3C,gBAAgB,CAAC,UAAU,CAAC,CAY9B"}
package/dist/adapters.js CHANGED
@@ -1,28 +1,41 @@
1
1
  import { createQmuseCloudClient } from './client.js';
2
- function isTransportUnauthorized(error) {
3
- if (!error || typeof error !== 'object')
4
- return false;
5
- const value = error;
6
- return [value.code, value.status, value.statusCode, value.response?.status, value.response?.statusCode]
7
- .some(code => Number(code) === 401);
8
- }
9
- async function withTransportRetry(operation, refreshSession) {
10
- try {
11
- return await operation();
2
+ import { toCloudAuthUser } from './auth.js';
3
+ export function createAppBaseFunctionsCapability(functions) {
4
+ if (!functions || typeof functions.createExecution !== 'function') {
5
+ throw new TypeError('An AppBase Functions SDK capability is required');
12
6
  }
13
- catch (error) {
14
- if (!refreshSession || !isTransportUnauthorized(error))
15
- throw error;
16
- await refreshSession();
17
- return operation();
7
+ return {
8
+ createExecution(input) {
9
+ const { path, ...sdkInput } = input;
10
+ return functions.createExecution({ ...sdkInput, xpath: path });
11
+ },
12
+ };
13
+ }
14
+ export function createAppBaseAccountCapability(account) {
15
+ if (!account || typeof account.createSession !== 'function' || typeof account.get !== 'function') {
16
+ throw new TypeError('An AppBase Account SDK capability is required');
18
17
  }
18
+ return {
19
+ createSession: input => account.createSession(input),
20
+ getCurrentUser: async () => toCloudAuthUser(await account.get()),
21
+ getCurrentSession: () => account.getSession({ sessionId: 'current' }),
22
+ refreshCurrentSession: () => account.updateSession({ sessionId: 'current' }),
23
+ deleteCurrentSession: async () => { await account.deleteSession({ sessionId: 'current' }); },
24
+ deleteAllSessions: async () => { await account.deleteSessions(); },
25
+ };
26
+ }
27
+ function executionInput(input, functionId) {
28
+ return {
29
+ ...input,
30
+ functionId: functionId?.trim() || input.functionId,
31
+ };
19
32
  }
20
33
  export function createAppBaseWebCloudClient(functions, options = {}) {
21
34
  if (!functions || typeof functions.createExecution !== 'function') {
22
35
  throw new TypeError('An AppBase Functions capability is required');
23
36
  }
24
37
  return createQmuseCloudClient({
25
- execute: input => withTransportRetry(() => functions.createExecution(input), options.refreshSession),
38
+ execute: input => functions.createExecution(executionInput(input, options.functionId)),
26
39
  ...(options.auth ? { auth: options.auth } : {}),
27
40
  ...(options.fileTransport ? { fileTransport: options.fileTransport } : {}),
28
41
  });
@@ -33,8 +46,8 @@ export function createPortableCloudClient(appwrite, options = {}) {
33
46
  }
34
47
  return createQmuseCloudClient({
35
48
  execute: input => {
36
- const { path, ...portableInput } = input;
37
- return withTransportRetry(() => appwrite.executeFunction({ ...portableInput, xpath: path }), options.refreshSession);
49
+ const { path, ...portableInput } = executionInput(input, options.functionId);
50
+ return appwrite.executeFunction({ ...portableInput, xpath: path });
38
51
  },
39
52
  ...(options.auth ? { auth: options.auth } : {}),
40
53
  ...(options.fileTransport ? { fileTransport: options.fileTransport } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"adapters.js","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAqCrD,SAAS,uBAAuB,CAAC,KAAc;IAC7C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACtD,MAAM,KAAK,GAAG,KAA2B,CAAC;IAC1C,OAAO,CAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,QAAQ,EAAE,UAAU,CAAE;SACtG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;AACxC,CAAC;AAED,KAAK,UAAU,kBAAkB,CAC/B,SAA2B,EAC3B,cAAoC;IAEpC,IAAI,CAAC;QACH,OAAO,MAAM,SAAS,EAAE,CAAC;IAC3B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,cAAc,IAAI,CAAC,uBAAuB,CAAC,KAAK,CAAC;YAAE,MAAM,KAAK,CAAC;QACpE,MAAM,cAAc,EAAE,CAAC;QACvB,OAAO,SAAS,EAAE,CAAC;IACrB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,2BAA2B,CACzC,SAAqC,EACrC,UAA0C,EAAE;IAE5C,IAAI,CAAC,SAAS,IAAI,OAAO,SAAS,CAAC,eAAe,KAAK,UAAU,EAAE,CAAC;QAClE,MAAM,IAAI,SAAS,CAAC,6CAA6C,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,sBAAsB,CAAC;QAC5B,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAClC,GAAG,EAAE,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,EACtC,OAAO,CAAC,cAAc,CACvB;QACD,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3E,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,QAAoC,EACpC,UAA0C,EAAE;IAE5C,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,CAAC,eAAe,KAAK,UAAU,EAAE,CAAC;QAChE,MAAM,IAAI,SAAS,CAAC,sDAAsD,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,sBAAsB,CAAC;QAC5B,OAAO,EAAE,KAAK,CAAC,EAAE;YACf,MAAM,EAAE,IAAI,EAAE,GAAG,aAAa,EAAE,GAAG,KAAK,CAAC;YACzC,OAAO,kBAAkB,CACvB,GAAG,EAAE,CAAC,QAAQ,CAAC,eAAe,CAAC,EAAE,GAAG,aAAa,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EACjE,OAAO,CAAC,cAAc,CACvB,CAAC;QACJ,CAAC;QACD,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/C,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC3E,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"adapters.js","sourceRoot":"","sources":["../src/adapters.ts"],"names":[],"mappings":"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"}
package/dist/auth.d.ts CHANGED
@@ -1,8 +1,13 @@
1
- import type { CloudAuthOptions, CloudRequest, CloudResponse, QmuseCloudAuth } from './types.js';
2
- type AuthRequest = <T>(request: CloudRequest, accessToken?: string) => Promise<CloudResponse<T>>;
1
+ import type { CloudAuthOptions, CloudAuthUser, CloudRequest, CloudResponse, QmuseCloudAuth } from './types.js';
2
+ type AuthRequest = <T>(request: CloudRequest) => Promise<CloudResponse<T>>;
3
3
  export type CloudAuthRuntime = QmuseCloudAuth & {
4
- accessToken(): Promise<string | undefined>;
4
+ prepareRequest(): Promise<void>;
5
5
  };
6
+ export declare function toCloudAuthUser(value: {
7
+ $id: string;
8
+ email?: string;
9
+ labels?: string[];
10
+ }): CloudAuthUser;
6
11
  export declare function createUnavailableCloudAuth(): CloudAuthRuntime;
7
12
  export declare function createCloudAuth(options: CloudAuthOptions, request: AuthRequest): CloudAuthRuntime;
8
13
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,gBAAgB,EAKhB,YAAY,EACZ,aAAa,EACb,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB,KAAK,WAAW,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,WAAW,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;AAEjG,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG;IAAE,WAAW,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;CAAE,CAAC;AAqC/F,wBAAgB,0BAA0B,IAAI,gBAAgB,CAa7D;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,WAAW,GAAG,gBAAgB,CA4GjG"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EAIb,YAAY,EACZ,aAAa,EACb,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB,KAAK,WAAW,GAAG,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;AAE3E,MAAM,MAAM,gBAAgB,GAAG,cAAc,GAAG;IAC9C,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC,CAAC;AAoBF,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,GAAG,aAAa,CAOhB;AAuCD,wBAAgB,0BAA0B,IAAI,gBAAgB,CAc7D;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,WAAW,GAAG,gBAAgB,CAuHjG"}
package/dist/auth.js CHANGED
@@ -1,30 +1,55 @@
1
1
  import { QmuseCloudError } from './errors.js';
2
- function validCredentials(value) {
2
+ const DEFAULT_REFRESH_BEFORE_MS = 5 * 60 * 1000;
3
+ function validUser(value) {
3
4
  if (!value || typeof value !== 'object')
4
5
  return false;
5
6
  const item = value;
6
- return typeof item.accessToken === 'string' && item.accessToken.length > 0 &&
7
- typeof item.refreshToken === 'string' && item.refreshToken.length > 0 &&
8
- typeof item.accessTokenExpiresAt === 'string' && item.accessTokenExpiresAt.length > 0 &&
9
- !!item.user && typeof item.user === 'object' && typeof item.user.id === 'string' && item.user.id.length > 0;
7
+ return typeof item.id === 'string' && item.id.length > 0 &&
8
+ typeof item.username === 'string' && item.username.length > 0 &&
9
+ (item.role === 'user' || item.role === 'admin');
10
10
  }
11
- function validSessionResult(value) {
12
- if (!value || typeof value !== 'object')
13
- return false;
14
- const item = value;
15
- const shadow = item.shadowSession;
16
- return validCredentials(item.credentials) && !!shadow && typeof shadow === 'object' &&
17
- typeof shadow.userId === 'string' && shadow.userId === item.credentials.user.id &&
18
- typeof shadow.secret === 'string' && shadow.secret.length > 0 &&
19
- typeof shadow.expire === 'string' && shadow.expire.length > 0;
11
+ export function toCloudAuthUser(value) {
12
+ const id = value?.$id?.trim();
13
+ const username = /^([a-z0-9_]{3,32})@qmuse[.]cn$/.exec(value?.email?.trim().toLowerCase() ?? '')?.[1];
14
+ if (!id || !username) {
15
+ throw new QmuseCloudError('AppBase returned an invalid current user', 'AUTH_USER_INVALID');
16
+ }
17
+ return { id, username, role: value.labels?.includes('admin') ? 'admin' : 'user' };
20
18
  }
21
- function sessionResult(response) {
19
+ function userResult(response) {
20
+ const user = response.data?.data;
21
+ if (!validUser(user)) {
22
+ throw new QmuseCloudError('Cloud API returned an invalid user', 'AUTH_USER_INVALID');
23
+ }
24
+ return user;
25
+ }
26
+ function loginResult(response) {
22
27
  const value = response.data?.data;
23
- if (!validSessionResult(value)) {
28
+ if (!value || typeof value !== 'object' || !validUser(value.user)) {
29
+ throw new QmuseCloudError('Cloud API returned an invalid authentication response', 'AUTH_RESPONSE_INVALID');
30
+ }
31
+ const token = value.sessionToken;
32
+ if (!token || typeof token !== 'object' || token.userId !== value.user.id ||
33
+ typeof token.secret !== 'string' || token.secret.length === 0 ||
34
+ typeof token.expire !== 'string' || token.expire.length === 0) {
24
35
  throw new QmuseCloudError('Cloud API returned an invalid authentication response', 'AUTH_RESPONSE_INVALID');
25
36
  }
26
37
  return value;
27
38
  }
39
+ function expiration(value) {
40
+ const parsed = Date.parse(value);
41
+ if (!Number.isFinite(parsed)) {
42
+ throw new QmuseCloudError('AppBase returned an invalid Session expiry', 'AUTH_SESSION_INVALID');
43
+ }
44
+ return parsed;
45
+ }
46
+ function isMissingSession(error) {
47
+ if (!error || typeof error !== 'object')
48
+ return false;
49
+ const value = error;
50
+ return [value.code, value.status, value.statusCode]
51
+ .some(code => Number(code) === 401 || Number(code) === 404);
52
+ }
28
53
  export function createUnavailableCloudAuth() {
29
54
  const unavailable = async () => {
30
55
  throw new QmuseCloudError('Cloud Auth is not configured', 'AUTH_UNAVAILABLE');
@@ -32,74 +57,85 @@ export function createUnavailableCloudAuth() {
32
57
  return {
33
58
  register: unavailable,
34
59
  login: unavailable,
35
- refresh: unavailable,
36
- restore: async () => null,
60
+ refreshSession: unavailable,
61
+ restoreSession: async () => null,
37
62
  logout: async () => undefined,
63
+ logoutAll: async () => undefined,
38
64
  getCurrentUser: async () => null,
39
- accessToken: async () => undefined,
65
+ prepareRequest: async () => undefined,
40
66
  };
41
67
  }
42
68
  export function createCloudAuth(options, request) {
43
- if (!options || typeof options.credentialStore?.load !== 'function' ||
44
- typeof options.credentialStore.save !== 'function' || typeof options.credentialStore.clear !== 'function') {
45
- throw new TypeError('Cloud Auth requires a credential store');
69
+ const account = options?.account;
70
+ if (!account || typeof account.createSession !== 'function' ||
71
+ typeof account.getCurrentUser !== 'function' || typeof account.getCurrentSession !== 'function' ||
72
+ typeof account.refreshCurrentSession !== 'function' || typeof account.deleteCurrentSession !== 'function' ||
73
+ typeof account.deleteAllSessions !== 'function') {
74
+ throw new TypeError('Cloud Auth requires an AppBase Account capability');
46
75
  }
47
- if (typeof options.shadowSession?.createSession !== 'function' ||
48
- typeof options.shadowSession.deleteCurrentSession !== 'function') {
49
- throw new TypeError('Cloud Auth requires a shadow Session capability');
76
+ const refreshBeforeMs = options.refreshBeforeMs ?? DEFAULT_REFRESH_BEFORE_MS;
77
+ if (!Number.isFinite(refreshBeforeMs) || refreshBeforeMs < 0) {
78
+ throw new TypeError('refreshBeforeMs must be a non-negative finite number');
50
79
  }
51
- let loaded = false;
52
- let credentials = null;
80
+ const now = options.now ?? Date.now;
81
+ let initialized = false;
82
+ let user = null;
83
+ let expiresAt = 0;
53
84
  let refreshFlight = null;
54
- const load = async () => {
55
- if (loaded)
56
- return credentials;
57
- const stored = await options.credentialStore.load();
58
- credentials = validCredentials(stored) ? stored : null;
59
- loaded = true;
60
- if (stored && !credentials)
61
- await options.credentialStore.clear();
62
- return credentials;
85
+ let restoreFlight = null;
86
+ const clear = () => {
87
+ initialized = true;
88
+ user = null;
89
+ expiresAt = 0;
63
90
  };
64
- const clear = async () => {
65
- credentials = null;
66
- loaded = true;
67
- await options.credentialStore.clear();
91
+ const readCurrent = async () => {
92
+ const [currentUser, currentSession] = await Promise.all([
93
+ account.getCurrentUser(),
94
+ account.getCurrentSession(),
95
+ ]);
96
+ if (!validUser(currentUser)) {
97
+ throw new QmuseCloudError('AppBase returned an invalid current user', 'AUTH_USER_INVALID');
98
+ }
99
+ expiresAt = expiration(currentSession.expire);
100
+ user = currentUser;
101
+ initialized = true;
102
+ return currentUser;
68
103
  };
69
- const establish = async (result) => {
70
- await options.shadowSession.createSession({
71
- userId: result.shadowSession.userId,
72
- secret: result.shadowSession.secret,
73
- });
104
+ const restoreSession = async () => {
105
+ if (restoreFlight)
106
+ return restoreFlight;
107
+ restoreFlight = (async () => {
108
+ try {
109
+ return await readCurrent();
110
+ }
111
+ catch (error) {
112
+ if (!isMissingSession(error))
113
+ throw error;
114
+ clear();
115
+ return null;
116
+ }
117
+ })();
74
118
  try {
75
- await options.credentialStore.save(result.credentials);
76
- credentials = result.credentials;
77
- loaded = true;
78
- return result.credentials.user;
119
+ return await restoreFlight;
79
120
  }
80
- catch (error) {
81
- await options.shadowSession.deleteCurrentSession().catch(() => undefined);
82
- throw error;
121
+ finally {
122
+ restoreFlight = null;
83
123
  }
84
124
  };
85
- const authenticate = async (path, body) => {
86
- await options.shadowSession.deleteCurrentSession().catch(() => undefined);
87
- return establish(sessionResult(await request({ method: 'POST', path, body })));
88
- };
89
- const refresh = async () => {
125
+ const refreshSession = async () => {
90
126
  if (refreshFlight)
91
127
  return refreshFlight;
92
128
  refreshFlight = (async () => {
93
- const current = await load();
94
- if (!current)
95
- throw new QmuseCloudError('No application session is available', 'AUTH_SESSION_MISSING');
96
- await options.shadowSession.deleteCurrentSession().catch(() => undefined);
97
- const response = await request({
98
- method: 'POST',
99
- path: '/api/auth/refresh',
100
- body: { refreshToken: current.refreshToken },
101
- });
102
- return establish(sessionResult(response));
129
+ const session = await account.refreshCurrentSession();
130
+ expiresAt = expiration(session.expire);
131
+ const currentUser = await account.getCurrentUser();
132
+ if (!validUser(currentUser)) {
133
+ clear();
134
+ throw new QmuseCloudError('AppBase returned an invalid current user', 'AUTH_USER_INVALID');
135
+ }
136
+ user = currentUser;
137
+ initialized = true;
138
+ return currentUser;
103
139
  })();
104
140
  try {
105
141
  return await refreshFlight;
@@ -109,48 +145,65 @@ export function createCloudAuth(options, request) {
109
145
  }
110
146
  };
111
147
  return {
112
- register: input => authenticate('/api/auth/register', input),
113
- login: input => authenticate('/api/auth/login', input),
114
- refresh,
115
- async restore() {
116
- if (!await load())
117
- return null;
148
+ async register(input) {
149
+ return userResult(await request({ method: 'POST', path: '/api/auth/register', body: input }));
150
+ },
151
+ async login(input) {
152
+ const result = loginResult(await request({ method: 'POST', path: '/api/auth/login', body: input }));
118
153
  try {
119
- return await refresh();
154
+ await account.deleteCurrentSession().catch(error => {
155
+ if (!isMissingSession(error))
156
+ throw error;
157
+ });
158
+ const session = await account.createSession({
159
+ userId: result.sessionToken.userId,
160
+ secret: result.sessionToken.secret,
161
+ });
162
+ expiresAt = expiration(session.expire);
163
+ const currentUser = await account.getCurrentUser();
164
+ if (!validUser(currentUser)) {
165
+ throw new QmuseCloudError('AppBase returned an invalid current user', 'AUTH_USER_INVALID');
166
+ }
167
+ user = currentUser;
168
+ initialized = true;
169
+ return currentUser;
120
170
  }
121
- catch (error) {
122
- await clear();
123
- await options.shadowSession.deleteCurrentSession().catch(() => undefined);
124
- throw error;
171
+ catch {
172
+ clear();
173
+ throw new QmuseCloudError('Could not establish the AppBase Session', 'AUTH_SESSION_EXCHANGE_FAILED');
174
+ }
175
+ finally {
176
+ result.sessionToken.secret = '';
125
177
  }
126
178
  },
179
+ refreshSession,
180
+ restoreSession,
127
181
  async logout() {
128
- const current = await load();
129
182
  try {
130
- if (current) {
131
- await request({ method: 'POST', path: '/api/auth/logout' }, current.accessToken);
132
- }
183
+ await account.deleteCurrentSession();
133
184
  }
134
185
  finally {
135
- await options.shadowSession.deleteCurrentSession().catch(() => undefined);
136
- await clear();
186
+ clear();
137
187
  }
138
188
  },
139
- async getCurrentUser() {
140
- const current = await load();
141
- if (!current)
142
- return null;
143
- const response = await request({ method: 'GET', path: '/api/auth/me' }, current.accessToken);
144
- const user = response.data?.data;
145
- if (!user || typeof user !== 'object' || typeof user.id !== 'string') {
146
- throw new QmuseCloudError('Cloud API returned an invalid current user', 'AUTH_USER_INVALID');
189
+ async logoutAll() {
190
+ try {
191
+ await account.deleteAllSessions();
147
192
  }
148
- credentials = { ...current, user };
149
- await options.credentialStore.save(credentials);
193
+ finally {
194
+ clear();
195
+ }
196
+ },
197
+ async getCurrentUser() {
198
+ if (!initialized)
199
+ return restoreSession();
150
200
  return user;
151
201
  },
152
- async accessToken() {
153
- return (await load())?.accessToken;
202
+ async prepareRequest() {
203
+ if (!initialized)
204
+ await restoreSession();
205
+ if (user && expiresAt - now() <= refreshBeforeMs)
206
+ await refreshSession();
154
207
  },
155
208
  };
156
209
  }