@apito-io/js-admin-sdk 2.1.2 → 2.7.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.
@@ -230,10 +230,7 @@ describe('ApitoClient', () => {
230
230
  const client = getTestClient();
231
231
 
232
232
  try {
233
- const token = await client.generateTenantToken(
234
- 'ak_4HESWVQEXE7V4GVGGDRYGXVWXSCAJL44TAUICSLBPQTOB6CJ53KTU3GUOEXJUIXVAKFMM2BDRJRWWPKEN3DRA3HDLZUY4NZMVLFJUIK5H4BWLY26AUKDOHPZE2ENGJNCXPPPEBKCNLTUXXUFUKVDGYJ2H6CZCSMQCY5KSCYNJVYBXVJBYE6O7C73DI3NV7Q',
235
- 'ba0ee756-6aea-43a6-b052-c7baab3da91c'
236
- );
233
+ const token = await client.generateTenantToken('ba0ee756-6aea-43a6-b052-c7baab3da91c');
237
234
 
238
235
  expect(token).toBeDefined();
239
236
  expect(typeof token).toBe('string');
@@ -245,6 +242,67 @@ describe('ApitoClient', () => {
245
242
  }, 30000);
246
243
  });
247
244
 
245
+ describe('Tenant users (Pro)', () => {
246
+ it('should search tenant users when APITO_PROJECT_ID is set', async () => {
247
+ const projectId = process.env.APITO_PROJECT_ID;
248
+ if (!projectId) {
249
+ console.log('Tenant users (Pro): skipped — set APITO_PROJECT_ID');
250
+ return;
251
+ }
252
+ const client = getTestClient();
253
+ try {
254
+ const { count, users } = await client.searchTenantUsers(projectId, 10, 0);
255
+ expect(count).toBeGreaterThanOrEqual(0);
256
+ console.log('✅ searchTenantUsers:', count, users?.length);
257
+ } catch (error) {
258
+ console.log('searchTenantUsers failed (may be expected):', error);
259
+ }
260
+ }, 30000);
261
+
262
+ it('should search tenants by domain when APITO_PROJECT_ID is set', async () => {
263
+ const projectId = process.env.APITO_PROJECT_ID;
264
+ if (!projectId) {
265
+ console.log('searchTenantsByDomain: skipped — set APITO_PROJECT_ID');
266
+ return;
267
+ }
268
+ const client = getTestClient();
269
+ try {
270
+ const { tenant } = await client.searchTenantsByDomain(projectId, 'example.invalid');
271
+ expect(tenant === null || typeof tenant?.id === 'string').toBe(true);
272
+ console.log('✅ searchTenantsByDomain:', tenant ? 'match' : 'no match');
273
+ } catch (error) {
274
+ console.log('searchTenantsByDomain failed (may be expected):', error);
275
+ }
276
+ }, 30000);
277
+
278
+ it('should login when APITO_PROJECT_ID and credentials are set', async () => {
279
+ const projectId = process.env.APITO_PROJECT_ID;
280
+ const email = process.env.APITO_TENANT_EMAIL ?? '';
281
+ const phone = process.env.APITO_TENANT_PHONE ?? '';
282
+ const password = process.env.APITO_TENANT_PASSWORD;
283
+ if (!projectId || !(email.trim() || phone.trim()) || !password) {
284
+ console.log(
285
+ 'loginTenantUser skipped — set APITO_PROJECT_ID, APITO_TENANT_PASSWORD, and APITO_TENANT_EMAIL and/or APITO_TENANT_PHONE'
286
+ );
287
+ return;
288
+ }
289
+ const client = getTestClient();
290
+ try {
291
+ const login = await client.loginTenantUser({
292
+ projectId,
293
+ password,
294
+ ...(email.trim() ? { email: email.trim() } : {}),
295
+ ...(phone.trim() ? { phone: phone.trim() } : {}),
296
+ });
297
+ expect(login.token).toBeDefined();
298
+ expect(login.token.length).toBeGreaterThan(0);
299
+ console.log('✅ loginTenantUser token length:', login.token.length);
300
+ } catch (error) {
301
+ console.log('loginTenantUser failed (may be expected):', error);
302
+ }
303
+ }, 30000);
304
+ });
305
+
248
306
  describe('Debug', () => {
249
307
  it('should send debug data', async () => {
250
308
  const client = getTestClient();
package/src/client.ts CHANGED
@@ -11,6 +11,14 @@ import {
11
11
  ApitoError,
12
12
  ValidationError,
13
13
  InjectedDBOperationInterface,
14
+ TenantLoginResponse,
15
+ TenantUser,
16
+ TenantUsersResponse,
17
+ TenantByDomainResponse,
18
+ TenantCatalogSearchRow,
19
+ TenantLoginParams,
20
+ CreateTenantUserParams,
21
+ UpdateTenantUserParams,
14
22
  } from './types';
15
23
 
16
24
  /**
@@ -32,7 +40,9 @@ export class ApitoClient implements InjectedDBOperationInterface {
32
40
  timeout: config.timeout || 30000,
33
41
  headers: {
34
42
  'Content-Type': 'application/json',
35
- 'X-Apito-Key': this.apiKey,
43
+ ...(this.apiKey.startsWith('cli-') || this.apiKey.startsWith('sdk-')
44
+ ? { 'X-Apito-Sync-Key': this.apiKey }
45
+ : { 'X-Apito-Key': this.apiKey }),
36
46
  },
37
47
  ...config.httpClient,
38
48
  });
@@ -60,7 +70,9 @@ export class ApitoClient implements InjectedDBOperationInterface {
60
70
 
61
71
  const headers: Record<string, string> = {
62
72
  'Content-Type': 'application/json',
63
- 'X-Apito-Key': this.apiKey,
73
+ ...(this.apiKey.startsWith('cli-') || this.apiKey.startsWith('sdk-')
74
+ ? { 'X-Apito-Sync-Key': this.apiKey }
75
+ : { 'X-Apito-Key': this.apiKey }),
64
76
  };
65
77
 
66
78
  if (options?.tenantId || this.tenantId) {
@@ -96,28 +108,35 @@ export class ApitoClient implements InjectedDBOperationInterface {
96
108
  }
97
109
 
98
110
  /**
99
- * Generate a tenant-scoped API key for {@link tenantId}.
111
+ * Generate a tenant-scoped API key. Matches engine `generateTenantToken`: `tenant_id`, `duration`, optional `role`.
112
+ * Auth uses `X-Apito-Key` (client `apiKey`).
100
113
  *
101
- * `token` is legacy and ignored; the engine authenticates via `X-Apito-Key` (client `apiKey`).
102
- * Expiry is sent as a calendar day `YYYY-MM-DD`; defaults to one year ahead in UTC (same default as Go admin/internal SDK).
114
+ * @param tenantId Catalog tenant id (`tenant_id` in the mutation).
115
+ * @param duration Expiry calendar day `YYYY-MM-DD`. If omitted/empty, defaults to one year ahead in UTC.
116
+ * @param role Optional token role; if omitted/empty, the engine defaults to `admin`.
103
117
  */
104
- async generateTenantToken(token: string, tenantId: string): Promise<string> {
105
- void token;
106
-
107
- const d = new Date();
108
- const duration = `${d.getUTCFullYear() + 1}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(
109
- d.getUTCDate()
110
- ).padStart(2, '0')}`;
118
+ async generateTenantToken(tenantId: string, duration?: string, role?: string): Promise<string> {
119
+ let dur = (duration ?? '').trim();
120
+ if (!dur) {
121
+ const d = new Date();
122
+ dur = `${d.getUTCFullYear() + 1}-${String(d.getUTCMonth() + 1).padStart(2, '0')}-${String(
123
+ d.getUTCDate()
124
+ ).padStart(2, '0')}`;
125
+ }
111
126
 
112
127
  const query = `
113
- mutation GenerateTenantToken($tenantId: String!, $duration: String!) {
114
- generateTenantToken(tenant_id: $tenantId, duration: $duration) {
128
+ mutation GenerateTenantToken($tenantId: String!, $duration: String!, $role: String) {
129
+ generateTenantToken(tenant_id: $tenantId, duration: $duration, role: $role) {
115
130
  token
116
131
  }
117
132
  }
118
133
  `;
119
134
 
120
- const variables = { tenantId, duration };
135
+ const variables: Record<string, any> = {
136
+ tenantId,
137
+ duration: dur,
138
+ role: role !== undefined && role.trim() !== '' ? role.trim() : null,
139
+ };
121
140
  const response = await this.executeGraphQL(query, variables, { tenantId });
122
141
 
123
142
  const data = response.data?.generateTenantToken;
@@ -128,6 +147,270 @@ export class ApitoClient implements InjectedDBOperationInterface {
128
147
  return data.token;
129
148
  }
130
149
 
150
+ /**
151
+ * Tenant catalog login (system GraphQL `loginTenantUser`). Password path: pass `password` and `email` or `phone` per project Authentication settings. Google OAuth path: `authMethod: 'google'` with `code` and `state` from the redirect; call `tenantGoogleOAuthState(projectId)` before opening Google to obtain `state`.
152
+ */
153
+ async loginTenantUser(params: TenantLoginParams): Promise<TenantLoginResponse> {
154
+ const authMethod = (params.authMethod ?? 'general').trim().toLowerCase() || 'general';
155
+ const variables: Record<string, any> = {
156
+ project_id: params.projectId,
157
+ };
158
+
159
+ if (authMethod === 'google') {
160
+ variables.auth_method = 'google';
161
+ const code = (params.code ?? '').trim();
162
+ const state = (params.state ?? '').trim();
163
+ if (!code || !state) {
164
+ throw new ValidationError('code and state are required for Google login');
165
+ }
166
+ variables.code = code;
167
+ variables.state = state;
168
+ } else {
169
+ const password = (params.password ?? '').trim();
170
+ if (!password) {
171
+ throw new ValidationError('password is required');
172
+ }
173
+ variables.password = password;
174
+ const email = (params.email ?? '').trim();
175
+ const phone = (params.phone ?? '').trim();
176
+ if (!email && !phone) {
177
+ throw new ValidationError('email or phone is required');
178
+ }
179
+ if (email) variables.email = email;
180
+ if (phone) variables.phone = phone;
181
+ }
182
+
183
+ const query = `
184
+ query LoginTenantUser($project_id: String!, $password: String, $auth_method: String, $email: String, $phone: String, $code: String, $state: String) {
185
+ loginTenantUser(project_id: $project_id, password: $password, auth_method: $auth_method, email: $email, phone: $phone, code: $code, state: $state) {
186
+ token
187
+ user {
188
+ id
189
+ email
190
+ phone
191
+ role
192
+ provider
193
+ tenant_id
194
+ status
195
+ created_at
196
+ updated_at
197
+ }
198
+ }
199
+ }
200
+ `;
201
+ const response = await this.executeGraphQL(query, variables);
202
+ const raw = response.data?.loginTenantUser;
203
+ if (!raw?.token) {
204
+ throw new ValidationError('Invalid response format for loginTenantUser');
205
+ }
206
+ return {
207
+ token: raw.token as string,
208
+ user: raw.user as TenantUser | undefined,
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Signed OAuth state for tenant Google login (system query `tenantGoogleOAuthState`). Use in the authorize URL together with project `google_client_id` and the configured redirect URI.
214
+ */
215
+ async tenantGoogleOAuthState(projectId: string): Promise<{ state: string }> {
216
+ const query = `
217
+ query TenantGoogleOAuthState($project_id: String!) {
218
+ tenantGoogleOAuthState(project_id: $project_id) {
219
+ state
220
+ }
221
+ }
222
+ `;
223
+ const variables = { project_id: projectId };
224
+ const response = await this.executeGraphQL(query, variables);
225
+ const raw = response.data?.tenantGoogleOAuthState;
226
+ const state = typeof raw?.state === 'string' ? raw.state.trim() : '';
227
+ if (!state) {
228
+ throw new ValidationError('Invalid response format for tenantGoogleOAuthState');
229
+ }
230
+ return { state };
231
+ }
232
+
233
+ /**
234
+ * Search tenant users for a project.
235
+ */
236
+ async searchTenantUsers(
237
+ projectId: string,
238
+ limit?: number,
239
+ offset?: number
240
+ ): Promise<TenantUsersResponse> {
241
+ const query = `
242
+ query SearchTenantUsers($project_id: String!, $limit: Int, $offset: Int) {
243
+ searchTenantUsers(project_id: $project_id, limit: $limit, offset: $offset) {
244
+ count
245
+ users {
246
+ id
247
+ email
248
+ phone
249
+ role
250
+ provider
251
+ tenant_id
252
+ status
253
+ created_at
254
+ updated_at
255
+ }
256
+ }
257
+ }
258
+ `;
259
+ const variables: Record<string, any> = { project_id: projectId };
260
+ if (limit !== undefined) variables.limit = limit;
261
+ if (offset !== undefined) variables.offset = offset;
262
+ const response = await this.executeGraphQL(query, variables);
263
+ const raw = response.data?.searchTenantUsers;
264
+ if (!raw) {
265
+ throw new ValidationError('Invalid response format for searchTenantUsers');
266
+ }
267
+ let count = 0;
268
+ if (typeof raw.count === 'number') {
269
+ count = raw.count;
270
+ }
271
+ const users = (raw.users ?? []) as TenantUser[];
272
+ return { users, count };
273
+ }
274
+
275
+ /**
276
+ * Resolve the single SaaS catalog tenant for an exact domain match in the project (`tenant` null if none).
277
+ */
278
+ async searchTenantsByDomain(projectId: string, domain: string): Promise<TenantByDomainResponse> {
279
+ const query = `
280
+ query SearchTenantsByDomain($project_id: String!, $domain: String!) {
281
+ searchTenantsByDomain(project_id: $project_id, domain: $domain) {
282
+ tenant {
283
+ id
284
+ name
285
+ status
286
+ domain
287
+ data
288
+ }
289
+ }
290
+ }
291
+ `;
292
+ const variables: Record<string, any> = {
293
+ project_id: projectId,
294
+ domain,
295
+ };
296
+ const response = await this.executeGraphQL(query, variables);
297
+ const raw = response.data?.searchTenantsByDomain;
298
+ if (!raw) {
299
+ throw new ValidationError('Invalid response format for searchTenantsByDomain');
300
+ }
301
+ const tenant = (raw.tenant ?? null) as TenantCatalogSearchRow | null;
302
+ return { tenant };
303
+ }
304
+
305
+ /**
306
+ * Create a tenant catalog user (local password). Use `email` and/or `phone` per engine validation for the project identifier mode.
307
+ */
308
+ async createTenantUser(
309
+ projectId: string,
310
+ params: CreateTenantUserParams
311
+ ): Promise<TenantUser> {
312
+ const password = (params.password ?? '').trim();
313
+ if (!password) {
314
+ throw new ValidationError('password is required');
315
+ }
316
+ const query = `
317
+ mutation CreateTenantUser($project_id: String!, $password: String!, $role: String, $email: String, $phone: String) {
318
+ createTenantUser(project_id: $project_id, password: $password, role: $role, email: $email, phone: $phone) {
319
+ id
320
+ email
321
+ phone
322
+ role
323
+ provider
324
+ tenant_id
325
+ status
326
+ created_at
327
+ updated_at
328
+ }
329
+ }
330
+ `;
331
+ const variables: Record<string, any> = {
332
+ project_id: projectId,
333
+ password,
334
+ };
335
+ const role = (params.role ?? '').trim();
336
+ if (role) variables.role = role;
337
+ const email = (params.email ?? '').trim();
338
+ if (email) variables.email = email;
339
+ const phone = (params.phone ?? '').trim();
340
+ if (phone) variables.phone = phone;
341
+ const response = await this.executeGraphQL(query, variables);
342
+ const u = response.data?.createTenantUser;
343
+ if (!u?.id) {
344
+ throw new ValidationError('Invalid response format for createTenantUser');
345
+ }
346
+ return u as TenantUser;
347
+ }
348
+
349
+ /**
350
+ * Update a tenant catalog user. Project scope is implied by the API key. Only include fields to change.
351
+ */
352
+ async updateTenantUser(userId: string, params: UpdateTenantUserParams): Promise<TenantUser> {
353
+ const uid = (userId ?? '').trim();
354
+ if (!uid) {
355
+ throw new ValidationError('userId is required');
356
+ }
357
+ if (
358
+ params.email === undefined &&
359
+ params.phone === undefined &&
360
+ params.password === undefined &&
361
+ params.role === undefined
362
+ ) {
363
+ throw new ValidationError('at least one field must be provided');
364
+ }
365
+ const query = `
366
+ mutation UpdateTenantUser($user_id: String!, $email: String, $phone: String, $password: String, $role: String) {
367
+ updateTenantUser(user_id: $user_id, email: $email, phone: $phone, password: $password, role: $role) {
368
+ id
369
+ email
370
+ phone
371
+ role
372
+ provider
373
+ tenant_id
374
+ status
375
+ created_at
376
+ updated_at
377
+ }
378
+ }
379
+ `;
380
+ const variables: Record<string, any> = { user_id: uid };
381
+ if (params.email !== undefined) variables.email = params.email;
382
+ if (params.phone !== undefined) variables.phone = params.phone;
383
+ if (params.password !== undefined) variables.password = params.password;
384
+ if (params.role !== undefined) variables.role = params.role;
385
+ const response = await this.executeGraphQL(query, variables);
386
+ const u = response.data?.updateTenantUser;
387
+ if (!u?.id) {
388
+ throw new ValidationError('Invalid response format for updateTenantUser');
389
+ }
390
+ return u as TenantUser;
391
+ }
392
+
393
+ /**
394
+ * Delete a tenant catalog user by id. Project scope is implied by the API key.
395
+ */
396
+ async deleteTenantUser(userId: string): Promise<boolean> {
397
+ const uid = (userId ?? '').trim();
398
+ if (!uid) {
399
+ throw new ValidationError('userId is required');
400
+ }
401
+ const query = `
402
+ mutation DeleteTenantUser($user_id: String!) {
403
+ deleteTenantUser(user_id: $user_id)
404
+ }
405
+ `;
406
+ const response = await this.executeGraphQL(query, { user_id: uid });
407
+ const ok = response.data?.deleteTenantUser;
408
+ if (typeof ok !== 'boolean') {
409
+ throw new ValidationError('Invalid response format for deleteTenantUser');
410
+ }
411
+ return ok;
412
+ }
413
+
131
414
  /**
132
415
  * Get a single resource by model and ID
133
416
  */
package/src/index.ts CHANGED
@@ -24,6 +24,9 @@ export type {
24
24
  ApitoError,
25
25
  ValidationError,
26
26
  InjectedDBOperationInterface,
27
+ TenantLoginParams,
28
+ CreateTenantUserParams,
29
+ UpdateTenantUserParams,
27
30
  } from './types';
28
31
 
29
32
  // Default export for convenience
package/src/types.ts CHANGED
@@ -86,6 +86,76 @@ export interface CreateAndUpdateRequest {
86
86
  forceUpdate?: boolean;
87
87
  }
88
88
 
89
+ /** Tenant catalog user from engine system DB (pro_tenant_users). */
90
+ export interface TenantUser {
91
+ id: string;
92
+ email?: string;
93
+ phone?: string;
94
+ role: string;
95
+ tenant_id: string;
96
+ provider?: string;
97
+ status?: string;
98
+ created_at?: string;
99
+ updated_at?: string;
100
+ }
101
+
102
+ /** Login via system GraphQL `loginTenantUser`. Password path: use `email` or `phone` per project settings. Google OAuth code path: `authMethod: 'google'`, `code`, `state` from redirect (get `state` first via `tenantGoogleOAuthState`). */
103
+ export interface TenantLoginParams {
104
+ projectId: string;
105
+ /** Required for general (password) login. */
106
+ password?: string;
107
+ email?: string;
108
+ phone?: string;
109
+ /** `general` (default) or `google`. */
110
+ authMethod?: string;
111
+ /** Google authorization code (with `authMethod: 'google'`). */
112
+ code?: string;
113
+ /** OAuth state from `tenantGoogleOAuthState` or callback (with `authMethod: 'google'`). */
114
+ state?: string;
115
+ }
116
+
117
+ export interface TenantGoogleOAuthStateResponse {
118
+ state: string;
119
+ }
120
+
121
+ export interface CreateTenantUserParams {
122
+ password: string;
123
+ role?: string;
124
+ email?: string;
125
+ phone?: string;
126
+ }
127
+
128
+ /** Optional fields for `updateTenantUser`; omitted keys are not sent. */
129
+ export interface UpdateTenantUserParams {
130
+ email?: string;
131
+ phone?: string;
132
+ password?: string;
133
+ role?: string;
134
+ }
135
+
136
+ export interface TenantLoginResponse {
137
+ token: string;
138
+ user?: TenantUser;
139
+ }
140
+
141
+ export interface TenantUsersResponse {
142
+ users: TenantUser[];
143
+ count: number;
144
+ }
145
+
146
+ /** One SaaS catalog tenant row from searchTenantsByDomain. */
147
+ export interface TenantCatalogSearchRow {
148
+ id: string;
149
+ name: string;
150
+ status?: string;
151
+ domain?: string;
152
+ data?: string;
153
+ }
154
+
155
+ export interface TenantByDomainResponse {
156
+ tenant: TenantCatalogSearchRow | null;
157
+ }
158
+
89
159
  export interface ClientConfig {
90
160
  baseURL: string;
91
161
  apiKey: string;
@@ -121,7 +191,14 @@ export interface InjectedDBOperationInterface {
121
191
  options?: { tenantId?: string }
122
192
  ): Promise<GraphQLResponse>;
123
193
  /** @param token Legacy; ignored. Auth uses client API key. */
124
- generateTenantToken(token: string, tenantId: string): Promise<string>;
194
+ generateTenantToken(tenantId: string, duration?: string, role?: string): Promise<string>;
195
+ loginTenantUser(params: TenantLoginParams): Promise<TenantLoginResponse>;
196
+ tenantGoogleOAuthState(projectId: string): Promise<TenantGoogleOAuthStateResponse>;
197
+ searchTenantUsers(projectId: string, limit?: number, offset?: number): Promise<TenantUsersResponse>;
198
+ searchTenantsByDomain(projectId: string, domain: string): Promise<TenantByDomainResponse>;
199
+ createTenantUser(projectId: string, params: CreateTenantUserParams): Promise<TenantUser>;
200
+ updateTenantUser(userId: string, params: UpdateTenantUserParams): Promise<TenantUser>;
201
+ deleteTenantUser(userId: string): Promise<boolean>;
125
202
  getSingleResource(model: string, id: string, singlePageData?: boolean): Promise<DefaultDocumentStructure>;
126
203
  searchResources(model: string, filter?: Record<string, any>, aggregate?: boolean): Promise<SearchResult>;
127
204
  getRelationDocuments(id: string, connection: Record<string, any>): Promise<SearchResult>;
package/src/version.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Apito JavaScript internal SDK version (kept in sync with package.json for releases)
3
3
  */
4
- export const Version = '2.1.1';
4
+ export const Version = '2.7.0';
5
5
 
6
6
  /**
7
7
  * GetVersion returns the current version of the SDK