@typeb-digital/nucleus-sdk 0.0.1 → 0.0.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.
@@ -0,0 +1,683 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var ofetch = require('ofetch');
4
+
5
+ const DEFAULT_BASE_URL = 'https://nucleus.typeb.digital';
6
+ function statusToCode(status, apiCode) {
7
+ if (apiCode === 'INVALID_SCOPE') return 'INVALID_SCOPE';
8
+ if (status === 404) return 'NOT_FOUND';
9
+ if (status === 429) return 'RATE_LIMITED';
10
+ return 'FORBIDDEN';
11
+ }
12
+ function makeErrorResult(code, message) {
13
+ return {
14
+ error: {
15
+ code,
16
+ message
17
+ }
18
+ };
19
+ }
20
+ class NucleusTransport {
21
+ constructor(token, baseUrl){
22
+ this.headers = {
23
+ Authorization: `Bearer ${token}`,
24
+ 'Content-Type': 'application/json'
25
+ };
26
+ this.baseUrl = (baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, '');
27
+ }
28
+ async get(path, query) {
29
+ try {
30
+ const params = query ? Object.fromEntries(Object.entries(query).filter(([, v])=>v !== undefined)) : undefined;
31
+ const envelope = await ofetch.$fetch(`${this.baseUrl}${path}`, {
32
+ method: 'GET',
33
+ headers: this.headers,
34
+ params
35
+ });
36
+ if (!envelope.success) {
37
+ return makeErrorResult('FORBIDDEN', envelope.error);
38
+ }
39
+ return {
40
+ data: envelope.data
41
+ };
42
+ } catch (err) {
43
+ if (err instanceof ofetch.FetchError) {
44
+ const body = err.data;
45
+ const code = statusToCode(err.status ?? 0, body?.error);
46
+ return makeErrorResult(code, err.message);
47
+ }
48
+ return makeErrorResult('NETWORK_ERROR', String(err));
49
+ }
50
+ }
51
+ async post(path, body) {
52
+ try {
53
+ const envelope = await ofetch.$fetch(`${this.baseUrl}${path}`, {
54
+ method: 'POST',
55
+ headers: this.headers,
56
+ body: JSON.stringify(body)
57
+ });
58
+ if (!envelope.success) return makeErrorResult('FORBIDDEN', envelope.error);
59
+ return {
60
+ data: envelope.data
61
+ };
62
+ } catch (err) {
63
+ if (err instanceof ofetch.FetchError) {
64
+ const errBody = err.data;
65
+ return makeErrorResult(statusToCode(err.status ?? 0, errBody?.error), err.message);
66
+ }
67
+ return makeErrorResult('NETWORK_ERROR', String(err));
68
+ }
69
+ }
70
+ async del(path, query) {
71
+ try {
72
+ const params = query ? Object.fromEntries(Object.entries(query).filter(([, v])=>v !== undefined)) : undefined;
73
+ const envelope = await ofetch.$fetch(`${this.baseUrl}${path}`, {
74
+ method: 'DELETE',
75
+ headers: this.headers,
76
+ params
77
+ });
78
+ if (!envelope.success) return makeErrorResult('FORBIDDEN', envelope.error);
79
+ return {
80
+ data: envelope.data
81
+ };
82
+ } catch (err) {
83
+ if (err instanceof ofetch.FetchError) {
84
+ const errBody = err.data;
85
+ return makeErrorResult(statusToCode(err.status ?? 0, errBody?.error), err.message);
86
+ }
87
+ return makeErrorResult('NETWORK_ERROR', String(err));
88
+ }
89
+ }
90
+ async getList(path, query) {
91
+ try {
92
+ const params = query ? Object.fromEntries(Object.entries(query).filter(([, v])=>v !== undefined)) : undefined;
93
+ const envelope = await ofetch.$fetch(`${this.baseUrl}${path}`, {
94
+ method: 'GET',
95
+ headers: this.headers,
96
+ params
97
+ });
98
+ if (!envelope.success) {
99
+ return makeErrorResult('FORBIDDEN', envelope.error);
100
+ }
101
+ const { data, meta } = envelope;
102
+ const listMeta = {
103
+ total: meta.total,
104
+ page: meta.page,
105
+ pageSize: meta.pageSize,
106
+ hasMore: meta.page * meta.pageSize < meta.total
107
+ };
108
+ return {
109
+ data,
110
+ meta: listMeta
111
+ };
112
+ } catch (err) {
113
+ if (err instanceof ofetch.FetchError) {
114
+ const body = err.data;
115
+ const code = statusToCode(err.status ?? 0, body?.error);
116
+ return makeErrorResult(code, err.message);
117
+ }
118
+ return makeErrorResult('NETWORK_ERROR', String(err));
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Transform utilities — map snake_case Prisma API responses to camelCase SDK types.
125
+ * Each function accepts `unknown` so it works regardless of which buckets were active.
126
+ */ function str(v) {
127
+ return typeof v === 'string' ? v : null;
128
+ }
129
+ function num(v) {
130
+ return typeof v === 'number' ? v : null;
131
+ }
132
+ function bool(v) {
133
+ return v === true;
134
+ }
135
+ function transformProfile(profile) {
136
+ if (!profile || typeof profile !== 'object') return {};
137
+ const p = profile;
138
+ const out = {};
139
+ if ('phone' in p) out['phone'] = p['phone'];
140
+ if ('city' in p) out['city'] = p['city'];
141
+ if ('country' in p) out['country'] = p['country'];
142
+ if ('biography' in p) out['biography'] = p['biography'];
143
+ if ('emergency_contact_name' in p || 'emergency_contact_phone' in p) {
144
+ out['emergencyContact'] = {
145
+ name: str(p['emergency_contact_name']),
146
+ phone: str(p['emergency_contact_phone'])
147
+ };
148
+ }
149
+ return out;
150
+ }
151
+ function transformCompensation(raw) {
152
+ if (!raw || typeof raw !== 'object') return null;
153
+ const r = raw;
154
+ return {
155
+ hourlyCostRate: num(r['hourly_cost_rate']),
156
+ hourlyBillableRate: num(r['hourly_billable_rate']),
157
+ monthlyCostRate: num(r['monthly_cost_rate']),
158
+ monthlyBillableRate: num(r['monthly_billable_rate']),
159
+ currencyCode: str(r['currency_code']) ?? 'USD',
160
+ effectiveFrom: str(r['effective_from']) ?? ''
161
+ };
162
+ }
163
+ function transformCompensationHistoryEntry(raw) {
164
+ const r = raw ?? {};
165
+ return {
166
+ hourlyCostRate: num(r['hourly_cost_rate']),
167
+ hourlyBillableRate: num(r['hourly_billable_rate']),
168
+ monthlyCostRate: num(r['monthly_cost_rate']),
169
+ monthlyBillableRate: num(r['monthly_billable_rate']),
170
+ currencyCode: str(r['currency_code']) ?? 'USD',
171
+ effectiveFrom: str(r['effective_from']) ?? '',
172
+ effectiveTo: str(r['effective_to'])
173
+ };
174
+ }
175
+ function transformEmployee(raw) {
176
+ if (!raw || typeof raw !== 'object') return {};
177
+ const r = raw;
178
+ const out = {};
179
+ // identity
180
+ if ('id' in r) out['id'] = r['id'];
181
+ if ('display_name' in r) out['displayName'] = str(r['display_name']);
182
+ if ('first_name' in r) out['firstName'] = str(r['first_name']);
183
+ if ('last_name' in r) out['lastName'] = str(r['last_name']);
184
+ if ('email' in r) out['email'] = str(r['email']);
185
+ if ('picture_url' in r) out['pictureUrl'] = str(r['picture_url']);
186
+ if ('job_title' in r) out['jobTitle'] = str(r['job_title']);
187
+ if ('department' in r) out['department'] = str(r['department']);
188
+ if ('employment_status' in r) out['employmentStatus'] = str(r['employment_status']);
189
+ if ('is_external' in r) out['isExternal'] = bool(r['is_external']);
190
+ // employment
191
+ if ('start_date' in r) out['startDate'] = str(r['start_date']);
192
+ if ('weekly_capacity' in r) out['weeklyCapacity'] = num(r['weekly_capacity']);
193
+ if ('manager_id' in r) out['managerId'] = str(r['manager_id']);
194
+ if ('partner_id' in r) out['partnerId'] = str(r['partner_id']);
195
+ if ('timezone' in r) out['timezone'] = str(r['timezone']);
196
+ // sensitive
197
+ if ('birthday' in r) out['birthday'] = str(r['birthday']);
198
+ if ('custom_fields' in r) out['customFields'] = r['custom_fields'];
199
+ // profile JSON (contact + employment.biography + sensitive.emergencyContact)
200
+ if ('profile' in r) {
201
+ out['profile'] = transformProfile(r['profile']);
202
+ }
203
+ // synthesised compensation (injected by Layer 3 interceptor)
204
+ if ('current_compensation' in r) {
205
+ out['currentCompensation'] = transformCompensation(r['current_compensation']);
206
+ }
207
+ if ('compensation_history' in r && Array.isArray(r['compensation_history'])) {
208
+ out['compensationHistory'] = r['compensation_history'].map(transformCompensationHistoryEntry);
209
+ }
210
+ // Expand: manager / partner (recursively transformed)
211
+ if ('manager' in r) {
212
+ out['manager'] = r['manager'] ? transformEmployee(r['manager']) : null;
213
+ }
214
+ if ('partner' in r) {
215
+ out['partner'] = r['partner'] ? transformPartner(r['partner']) : null;
216
+ }
217
+ return out;
218
+ }
219
+ function transformProject(raw) {
220
+ if (!raw || typeof raw !== 'object') return {};
221
+ const r = raw;
222
+ const out = {};
223
+ // core
224
+ if ('id' in r) out['id'] = r['id'];
225
+ if ('name' in r) out['name'] = str(r['name']);
226
+ if ('status' in r) out['status'] = str(r['status']);
227
+ if ('color_tag' in r) out['colorTag'] = str(r['color_tag']);
228
+ if ('icon_name' in r) out['iconName'] = str(r['icon_name']);
229
+ if ('start_date' in r) out['startDate'] = str(r['start_date']);
230
+ if ('end_date' in r) out['endDate'] = str(r['end_date']);
231
+ if ('project_type_id' in r) out['projectTypeId'] = str(r['project_type_id']);
232
+ if ('client_id' in r) out['clientId'] = str(r['client_id']);
233
+ // team
234
+ if ('project_manager_id' in r) out['projectManagerId'] = str(r['project_manager_id']);
235
+ if ('engagement_lead_id' in r) out['engagementLeadId'] = str(r['engagement_lead_id']);
236
+ if ('memberships' in r && Array.isArray(r['memberships'])) {
237
+ out['memberships'] = r['memberships'].map((m)=>{
238
+ const mem = m ?? {};
239
+ const entry = {
240
+ employeeId: str(mem['employee_id']),
241
+ role: str(mem['role'])
242
+ };
243
+ if ('employee' in mem) {
244
+ entry['employee'] = mem['employee'] ? transformEmployee(mem['employee']) : null;
245
+ }
246
+ return entry;
247
+ });
248
+ }
249
+ // integrations
250
+ if ('source' in r) out['source'] = str(r['source']);
251
+ if ('clockify_project_id' in r) out['clockifyProjectId'] = str(r['clockify_project_id']);
252
+ if ('clockify_workspace_id' in r) out['clockifyWorkspaceId'] = str(r['clockify_workspace_id']);
253
+ if ('apollo_deal_id' in r) out['apolloDealId'] = str(r['apollo_deal_id']);
254
+ // expand: client / projectType / projectManager / engagementLead
255
+ if ('client' in r) out['client'] = r['client'] ? transformClient(r['client']) : null;
256
+ if ('project_type' in r || 'projectType' in r) {
257
+ const pt = r['project_type'] ?? r['projectType'];
258
+ out['projectType'] = pt ? transformProjectType(pt) : null;
259
+ }
260
+ if ('project_manager' in r || 'projectManager' in r) {
261
+ const pm = r['project_manager'] ?? r['projectManager'];
262
+ out['projectManager'] = pm ? transformEmployee(pm) : null;
263
+ }
264
+ if ('engagement_lead' in r || 'engagementLead' in r) {
265
+ const el = r['engagement_lead'] ?? r['engagementLead'];
266
+ out['engagementLead'] = el ? transformEmployee(el) : null;
267
+ }
268
+ return out;
269
+ }
270
+ function transformClient(raw) {
271
+ if (!raw || typeof raw !== 'object') return {};
272
+ const r = raw;
273
+ const out = {};
274
+ if ('id' in r) out['id'] = r['id'];
275
+ if ('name' in r) out['name'] = str(r['name']);
276
+ if ('code' in r) out['code'] = str(r['code']);
277
+ if ('industry' in r) out['industry'] = str(r['industry']);
278
+ if ('website' in r) out['website'] = str(r['website']);
279
+ if ('country' in r) out['country'] = str(r['country']);
280
+ if ('primary_contact_name' in r) out['primaryContactName'] = str(r['primary_contact_name']);
281
+ if ('primary_contact_email' in r) out['primaryContactEmail'] = str(r['primary_contact_email']);
282
+ if ('contact_phone' in r) out['contactPhone'] = str(r['contact_phone']);
283
+ if ('default_currency' in r) out['defaultCurrency'] = str(r['default_currency']);
284
+ if ('notes' in r) out['notes'] = str(r['notes']);
285
+ if ('projects' in r && Array.isArray(r['projects'])) {
286
+ out['projects'] = r['projects'].map(transformProject);
287
+ }
288
+ return out;
289
+ }
290
+ function transformPartner(raw) {
291
+ if (!raw || typeof raw !== 'object') return {};
292
+ const r = raw;
293
+ const out = {};
294
+ if ('id' in r) out['id'] = r['id'];
295
+ if ('name' in r) out['name'] = str(r['name']);
296
+ if ('contact_name' in r) out['contactName'] = str(r['contact_name']);
297
+ if ('contact_email' in r) out['contactEmail'] = str(r['contact_email']);
298
+ if ('contact_phone' in r) out['contactPhone'] = str(r['contact_phone']);
299
+ if ('notes' in r) out['notes'] = str(r['notes']);
300
+ if ('employees' in r && Array.isArray(r['employees'])) {
301
+ out['employees'] = r['employees'].map(transformEmployee);
302
+ }
303
+ return out;
304
+ }
305
+ function transformDepartment(raw) {
306
+ if (!raw || typeof raw !== 'object') return {};
307
+ const r = raw;
308
+ const out = {};
309
+ if ('id' in r) out['id'] = r['id'];
310
+ if ('name' in r) out['name'] = str(r['name']);
311
+ if ('titles' in r) out['titles'] = Array.isArray(r['titles']) ? r['titles'] : [];
312
+ if ('sort_order' in r) out['sortOrder'] = num(r['sort_order']) ?? 0;
313
+ return out;
314
+ }
315
+ function transformProjectType(raw) {
316
+ if (!raw || typeof raw !== 'object') return {};
317
+ const r = raw;
318
+ const out = {};
319
+ if ('id' in r) out['id'] = r['id'];
320
+ if ('name' in r) out['name'] = str(r['name']);
321
+ if ('description' in r) out['description'] = str(r['description']);
322
+ if ('sort_order' in r) out['sortOrder'] = num(r['sort_order']) ?? 0;
323
+ return out;
324
+ }
325
+ function transformCurrency(raw) {
326
+ if (!raw || typeof raw !== 'object') return {};
327
+ const r = raw;
328
+ const out = {};
329
+ if ('code' in r) out['code'] = r['code'];
330
+ if ('name' in r) out['name'] = str(r['name']);
331
+ if ('symbol' in r) out['symbol'] = str(r['symbol']);
332
+ if ('is_default' in r) out['isDefault'] = bool(r['is_default']);
333
+ if ('sort_order' in r) out['sortOrder'] = num(r['sort_order']) ?? 0;
334
+ return out;
335
+ }
336
+ function transformGenericRate(raw) {
337
+ if (!raw || typeof raw !== 'object') return {};
338
+ const r = raw;
339
+ const out = {};
340
+ if ('id' in r) out['id'] = r['id'];
341
+ if ('title' in r) out['title'] = str(r['title']);
342
+ if ('department' in r) out['department'] = str(r['department']);
343
+ if ('currency_code' in r) out['currencyCode'] = str(r['currency_code']);
344
+ if ('cost_rate' in r) out['costRate'] = num(r['cost_rate']);
345
+ if ('billable_rate' in r) out['billableRate'] = num(r['billable_rate']);
346
+ return out;
347
+ }
348
+
349
+ class EmployeesAccessor {
350
+ constructor(transport){
351
+ this.transport = transport;
352
+ }
353
+ async list(params) {
354
+ const query = {};
355
+ if (params?.search) query['search'] = params.search;
356
+ if (params?.department) query['department'] = params.department;
357
+ if (params?.employmentStatus) query['employmentStatus'] = params.employmentStatus;
358
+ if (params?.page) query['page'] = params.page;
359
+ if (params?.pageSize) query['pageSize'] = params.pageSize;
360
+ if (params?.expand?.length) query['expand'] = params.expand.join(',');
361
+ const result = await this.transport.getList('/api/v1/data/employees', query);
362
+ if ('error' in result) return result;
363
+ return {
364
+ data: result.data.map((r)=>transformEmployee(r)),
365
+ meta: result.meta
366
+ };
367
+ }
368
+ async getById(id, options) {
369
+ const query = {};
370
+ if (options?.expand?.length) query['expand'] = options.expand.join(',');
371
+ const result = await this.transport.get(`/api/v1/data/employees/${id}`, query);
372
+ if ('error' in result) return result;
373
+ return {
374
+ data: transformEmployee(result.data)
375
+ };
376
+ }
377
+ }
378
+
379
+ class ProjectsAccessor {
380
+ constructor(transport){
381
+ this.transport = transport;
382
+ }
383
+ async list(params) {
384
+ const query = {};
385
+ if (params?.search) query['search'] = params.search;
386
+ if (params?.status) query['status'] = params.status;
387
+ if (params?.clientId) query['clientId'] = params.clientId;
388
+ if (params?.page) query['page'] = params.page;
389
+ if (params?.pageSize) query['pageSize'] = params.pageSize;
390
+ if (params?.expand?.length) query['expand'] = params.expand.join(',');
391
+ const result = await this.transport.getList('/api/v1/data/projects', query);
392
+ if ('error' in result) return result;
393
+ return {
394
+ data: result.data.map((r)=>transformProject(r)),
395
+ meta: result.meta
396
+ };
397
+ }
398
+ async getById(id, options) {
399
+ const query = {};
400
+ if (options?.expand?.length) query['expand'] = options.expand.join(',');
401
+ const result = await this.transport.get(`/api/v1/data/projects/${id}`, query);
402
+ if ('error' in result) return result;
403
+ return {
404
+ data: transformProject(result.data)
405
+ };
406
+ }
407
+ }
408
+
409
+ class ClientsAccessor {
410
+ constructor(transport){
411
+ this.transport = transport;
412
+ }
413
+ async list(params) {
414
+ const query = {};
415
+ if (params?.search) query['search'] = params.search;
416
+ if (params?.industry) query['industry'] = params.industry;
417
+ if (params?.country) query['country'] = params.country;
418
+ if (params?.page) query['page'] = params.page;
419
+ if (params?.pageSize) query['pageSize'] = params.pageSize;
420
+ if (params?.expand?.length) query['expand'] = params.expand.join(',');
421
+ const result = await this.transport.getList('/api/v1/data/clients', query);
422
+ if ('error' in result) return result;
423
+ return {
424
+ data: result.data.map((r)=>transformClient(r)),
425
+ meta: result.meta
426
+ };
427
+ }
428
+ async getById(id, options) {
429
+ const query = {};
430
+ if (options?.expand?.length) query['expand'] = options.expand.join(',');
431
+ const result = await this.transport.get(`/api/v1/data/clients/${id}`, query);
432
+ if ('error' in result) return result;
433
+ return {
434
+ data: transformClient(result.data)
435
+ };
436
+ }
437
+ }
438
+
439
+ class PartnersAccessor {
440
+ constructor(transport){
441
+ this.transport = transport;
442
+ }
443
+ async list(params) {
444
+ const query = {};
445
+ if (params?.search) query['search'] = params.search;
446
+ if (params?.page) query['page'] = params.page;
447
+ if (params?.pageSize) query['pageSize'] = params.pageSize;
448
+ if (params?.expand?.length) query['expand'] = params.expand.join(',');
449
+ const result = await this.transport.getList('/api/v1/data/partners', query);
450
+ if ('error' in result) return result;
451
+ return {
452
+ data: result.data.map((r)=>transformPartner(r)),
453
+ meta: result.meta
454
+ };
455
+ }
456
+ async getById(id, options) {
457
+ const query = {};
458
+ if (options?.expand?.length) query['expand'] = options.expand.join(',');
459
+ const result = await this.transport.get(`/api/v1/data/partners/${id}`, query);
460
+ if ('error' in result) return result;
461
+ return {
462
+ data: transformPartner(result.data)
463
+ };
464
+ }
465
+ }
466
+
467
+ class DepartmentsAccessor {
468
+ constructor(transport){
469
+ this.transport = transport;
470
+ }
471
+ async list(params) {
472
+ const query = {};
473
+ if (params?.search) query['search'] = params.search;
474
+ if (params?.page) query['page'] = params.page;
475
+ if (params?.pageSize) query['pageSize'] = params.pageSize;
476
+ const result = await this.transport.getList('/api/v1/data/departments', query);
477
+ if ('error' in result) return result;
478
+ return {
479
+ data: result.data.map((r)=>transformDepartment(r)),
480
+ meta: result.meta
481
+ };
482
+ }
483
+ async getById(id) {
484
+ const result = await this.transport.get(`/api/v1/data/departments/${id}`);
485
+ if ('error' in result) return result;
486
+ return {
487
+ data: transformDepartment(result.data)
488
+ };
489
+ }
490
+ }
491
+
492
+ class ProjectTypesAccessor {
493
+ constructor(transport){
494
+ this.transport = transport;
495
+ }
496
+ async list(params) {
497
+ const query = {};
498
+ if (params?.page) query['page'] = params.page;
499
+ if (params?.pageSize) query['pageSize'] = params.pageSize;
500
+ const result = await this.transport.getList('/api/v1/data/project-types', query);
501
+ if ('error' in result) return result;
502
+ return {
503
+ data: result.data.map((r)=>transformProjectType(r)),
504
+ meta: result.meta
505
+ };
506
+ }
507
+ async getById(id) {
508
+ const result = await this.transport.get(`/api/v1/data/project-types/${id}`);
509
+ if ('error' in result) return result;
510
+ return {
511
+ data: transformProjectType(result.data)
512
+ };
513
+ }
514
+ }
515
+
516
+ class CurrenciesAccessor {
517
+ constructor(transport){
518
+ this.transport = transport;
519
+ }
520
+ async list(params) {
521
+ const query = {};
522
+ if (params?.page) query['page'] = params.page;
523
+ if (params?.pageSize) query['pageSize'] = params.pageSize;
524
+ const result = await this.transport.getList('/api/v1/data/currencies', query);
525
+ if ('error' in result) return result;
526
+ return {
527
+ data: result.data.map((r)=>transformCurrency(r)),
528
+ meta: result.meta
529
+ };
530
+ }
531
+ async getByCode(code) {
532
+ const result = await this.transport.get(`/api/v1/data/currencies/${code}`);
533
+ if ('error' in result) return result;
534
+ return {
535
+ data: transformCurrency(result.data)
536
+ };
537
+ }
538
+ }
539
+
540
+ class GenericRatesAccessor {
541
+ constructor(transport){
542
+ this.transport = transport;
543
+ }
544
+ async list(params) {
545
+ const query = {};
546
+ if (params?.department) query['department'] = params.department;
547
+ if (params?.page) query['page'] = params.page;
548
+ if (params?.pageSize) query['pageSize'] = params.pageSize;
549
+ const result = await this.transport.getList('/api/v1/data/generic-rates', query);
550
+ if ('error' in result) return result;
551
+ return {
552
+ data: result.data.map((r)=>transformGenericRate(r)),
553
+ meta: result.meta
554
+ };
555
+ }
556
+ async getById(id) {
557
+ const result = await this.transport.get(`/api/v1/data/generic-rates/${id}`);
558
+ if ('error' in result) return result;
559
+ return {
560
+ data: transformGenericRate(result.data)
561
+ };
562
+ }
563
+ }
564
+
565
+ /**
566
+ * AppsAccessor — wraps the self-service app info endpoint (§3.8, §6).
567
+ * Calls GET /api/v1/platform/apps/me with the app token.
568
+ * This is the only platform endpoint accessible via app token.
569
+ */ class AppsAccessor {
570
+ constructor(transport){
571
+ this.transport = transport;
572
+ }
573
+ async me() {
574
+ const result = await this.transport.get('/api/v1/platform/apps/me');
575
+ if ('error' in result) return result;
576
+ return {
577
+ data: result.data
578
+ };
579
+ }
580
+ }
581
+
582
+ class FilesAccessor {
583
+ constructor(transport){
584
+ this.transport = transport;
585
+ }
586
+ /**
587
+ * Upload a file. Upsert semantics: re-uploading the same key overwrites the
588
+ * existing R2 object and updates the File record — no duplicate, no error.
589
+ */ async upload(params) {
590
+ return this.transport.post('/api/v1/platform/files', {
591
+ key: params.key,
592
+ file: params.file.toString('base64'),
593
+ mimeType: params.mimeType,
594
+ visibility: params.visibility,
595
+ ...params.allowedUsers !== undefined ? {
596
+ allowedUsers: params.allowedUsers
597
+ } : {},
598
+ ...params.metadata !== undefined ? {
599
+ metadata: params.metadata
600
+ } : {},
601
+ ...params.filename !== undefined ? {
602
+ filename: params.filename
603
+ } : {}
604
+ });
605
+ }
606
+ /**
607
+ * Resolve a usable URL for a file.
608
+ * - Public files → deterministic CDN URL
609
+ * - Private app-wide files → fresh pre-signed URL (expires in R2_SIGNED_URL_TTL seconds)
610
+ * - Private user-scoped files → supply asUser; 403 if user is not in allowedUsers
611
+ */ async getUrl(key, options) {
612
+ return this.transport.post('/api/v1/platform/files/url', {
613
+ key,
614
+ ...options?.asUser ? {
615
+ asUser: options.asUser
616
+ } : {}
617
+ });
618
+ }
619
+ /** Soft-delete. Resolution of the key returns 404 thereafter. */ async delete(key) {
620
+ return this.transport.del('/api/v1/platform/files', {
621
+ key
622
+ });
623
+ }
624
+ /** List this app's files. Optionally filter by path prefix. */ async list(params) {
625
+ const query = {};
626
+ if (params?.prefix) query['prefix'] = params.prefix;
627
+ if (params?.page) query['page'] = params.page;
628
+ if (params?.pageSize) query['pageSize'] = params.pageSize;
629
+ return this.transport.getList('/api/v1/platform/files', query);
630
+ }
631
+ }
632
+
633
+ /**
634
+ * NucleusClient — entry point for the Nucleus server SDK.
635
+ *
636
+ * @example
637
+ * ```typescript
638
+ * import { NucleusClient } from '@typeb-digital/nucleus-sdk';
639
+ *
640
+ * export const nucleus = new NucleusClient({
641
+ * token: process.env.NUCLEUS_TOKEN!,
642
+ * scopes: {
643
+ * employees: ['identity', 'employment'],
644
+ * projects: ['core'],
645
+ * clients: ['identity'],
646
+ * },
647
+ * });
648
+ * ```
649
+ *
650
+ * The `scopes` object is the heart of the type system — it is declared once at
651
+ * instantiation and drives the return type of every method on the client.
652
+ * TypeScript infers it automatically; no explicit type parameters needed.
653
+ */ class NucleusClient {
654
+ constructor(config){
655
+ const transport = new NucleusTransport(config.token, config.baseUrl);
656
+ this.employees = new EmployeesAccessor(transport);
657
+ this.projects = new ProjectsAccessor(transport);
658
+ this.clients = new ClientsAccessor(transport);
659
+ this.partners = new PartnersAccessor(transport);
660
+ this.departments = new DepartmentsAccessor(transport);
661
+ this.projectTypes = new ProjectTypesAccessor(transport);
662
+ this.currencies = new CurrenciesAccessor(transport);
663
+ this.genericRates = new GenericRatesAccessor(transport);
664
+ this.apps = new AppsAccessor(transport);
665
+ this.files = new FilesAccessor(transport);
666
+ }
667
+ }
668
+
669
+ /**
670
+ * Nucleus SDK — Type System
671
+ *
672
+ * Bucket types are transcribed from docs/NUCLEUS_PIVOT.md §7 (Bucket Definitions).
673
+ * Each bucket type contains only that bucket's fields.
674
+ * The scope-to-type inference intersects selected bucket types at compile time.
675
+ */ // ─── Utility types ────────────────────────────────────────────────────────────
676
+ // Converts a union to an intersection: A | B → A & B
677
+ function isError(result) {
678
+ return 'error' in result;
679
+ }
680
+
681
+ exports.FilesAccessor = FilesAccessor;
682
+ exports.NucleusClient = NucleusClient;
683
+ exports.isError = isError;