@letpeoplework/lighthouse-client 0.1.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,578 @@
1
+ // src/index.ts
2
+ var getClientPackageContract = () => ({
3
+ name: "@letpeoplework/lighthouse-client",
4
+ capabilities: [
5
+ "versioned-api-contracts",
6
+ "shared-domain-operations",
7
+ "connectivity-and-discovery-contracts",
8
+ "automation-auth-contracts"
9
+ ]
10
+ });
11
+ var SUPPORTED_DISCOVERY_CONTRACT_VERSION = 1;
12
+ var isObjectRecord = (value) => {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ };
15
+ var getNormalizedLighthouseUrl = (value) => {
16
+ const trimmed = value.trim();
17
+ if (trimmed.length === 0) {
18
+ return null;
19
+ }
20
+ try {
21
+ const parsed = new URL(trimmed);
22
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
23
+ return null;
24
+ }
25
+ const pathname = parsed.pathname.replace(/\/+$/u, "");
26
+ if (pathname.length === 0 || pathname === "/") {
27
+ return parsed.origin;
28
+ }
29
+ return `${parsed.origin}${pathname}`;
30
+ } catch {
31
+ return null;
32
+ }
33
+ };
34
+ var getApiBaseUrl = (lighthouseUrl) => {
35
+ if (lighthouseUrl.endsWith("/api")) {
36
+ return lighthouseUrl;
37
+ }
38
+ return `${lighthouseUrl}/api`;
39
+ };
40
+ var validateDiscoveryTimestamp = (detectedAtUtc) => {
41
+ return !Number.isNaN(Date.parse(detectedAtUtc));
42
+ };
43
+ var getInvalidContractResult = (reason) => ({
44
+ isValid: false,
45
+ reason
46
+ });
47
+ var validateStandaloneDiscoveryContract = (contract) => {
48
+ if (contract.contractVersion !== SUPPORTED_DISCOVERY_CONTRACT_VERSION) {
49
+ return getInvalidContractResult(
50
+ "Discovery contractVersion is not supported."
51
+ );
52
+ }
53
+ const normalizedUrl = getNormalizedLighthouseUrl(contract.lighthouseUrl);
54
+ if (normalizedUrl === null) {
55
+ return getInvalidContractResult("Discovery lighthouseUrl is invalid.");
56
+ }
57
+ if (!validateDiscoveryTimestamp(contract.detectedAtUtc)) {
58
+ return getInvalidContractResult("Discovery detectedAtUtc is invalid.");
59
+ }
60
+ return {
61
+ isValid: true,
62
+ contract: {
63
+ contractVersion: SUPPORTED_DISCOVERY_CONTRACT_VERSION,
64
+ lighthouseUrl: normalizedUrl,
65
+ detectedAtUtc: contract.detectedAtUtc
66
+ }
67
+ };
68
+ };
69
+ var parseStandaloneDiscoveryContract = (serializedContract) => {
70
+ let parsed;
71
+ try {
72
+ parsed = JSON.parse(serializedContract);
73
+ } catch {
74
+ return getInvalidContractResult(
75
+ "Discovery contract payload is not valid JSON."
76
+ );
77
+ }
78
+ if (!isObjectRecord(parsed)) {
79
+ return getInvalidContractResult(
80
+ "Discovery contract payload must be an object."
81
+ );
82
+ }
83
+ const contractVersion = parsed.contractVersion;
84
+ if (typeof contractVersion !== "number") {
85
+ return getInvalidContractResult(
86
+ "Discovery contractVersion must be a number."
87
+ );
88
+ }
89
+ const lighthouseUrl = parsed.lighthouseUrl;
90
+ if (typeof lighthouseUrl !== "string") {
91
+ return getInvalidContractResult(
92
+ "Discovery lighthouseUrl must be a string."
93
+ );
94
+ }
95
+ const detectedAtUtc = parsed.detectedAtUtc;
96
+ if (typeof detectedAtUtc !== "string") {
97
+ return getInvalidContractResult(
98
+ "Discovery detectedAtUtc must be a string."
99
+ );
100
+ }
101
+ return validateStandaloneDiscoveryContract({
102
+ contractVersion,
103
+ lighthouseUrl,
104
+ detectedAtUtc
105
+ });
106
+ };
107
+ var getResolvedEndpoint = (mode, lighthouseUrl) => {
108
+ const apiBaseUrl = getApiBaseUrl(lighthouseUrl);
109
+ return {
110
+ mode,
111
+ lighthouseUrl,
112
+ apiBaseUrl,
113
+ healthCheckUrl: `${apiBaseUrl}/v1/version`
114
+ };
115
+ };
116
+ var getMisconfiguredResult = (reason) => ({
117
+ category: "misconfigured",
118
+ reason
119
+ });
120
+ var resolveConnectionEndpoint = async (configuration) => {
121
+ if (configuration.kind === "explicit") {
122
+ const normalizedUrl = getNormalizedLighthouseUrl(
123
+ configuration.lighthouseUrl
124
+ );
125
+ if (normalizedUrl === null) {
126
+ return {
127
+ isValid: false,
128
+ result: getMisconfiguredResult("Explicit Lighthouse URL is invalid.")
129
+ };
130
+ }
131
+ return {
132
+ isValid: true,
133
+ endpoint: getResolvedEndpoint("explicit", normalizedUrl)
134
+ };
135
+ }
136
+ const discoveryContract = await configuration.getDiscoveryContract();
137
+ if (discoveryContract === null) {
138
+ return {
139
+ isValid: false,
140
+ result: getMisconfiguredResult(
141
+ "Standalone discovery contract is not available."
142
+ )
143
+ };
144
+ }
145
+ const parsedContract = validateStandaloneDiscoveryContract(discoveryContract);
146
+ if (!parsedContract.isValid) {
147
+ return {
148
+ isValid: false,
149
+ result: getMisconfiguredResult(parsedContract.reason)
150
+ };
151
+ }
152
+ return {
153
+ isValid: true,
154
+ endpoint: getResolvedEndpoint(
155
+ "standalone",
156
+ parsedContract.contract.lighthouseUrl
157
+ )
158
+ };
159
+ };
160
+ var getFailureCategoryForStatus = (statusCode) => {
161
+ if (statusCode === 401 || statusCode === 403) {
162
+ return "unauthorized";
163
+ }
164
+ if (statusCode === 404) {
165
+ return "misconfigured";
166
+ }
167
+ if (statusCode >= 500 && statusCode <= 599) {
168
+ return "dependency-failure";
169
+ }
170
+ return "unexpected";
171
+ };
172
+ var validateLighthouseConnectivity = async (configuration, dependencies, requestInit) => {
173
+ const resolvedEndpoint = await resolveConnectionEndpoint(configuration);
174
+ if (!resolvedEndpoint.isValid) {
175
+ return resolvedEndpoint.result;
176
+ }
177
+ const endpoint = resolvedEndpoint.endpoint;
178
+ try {
179
+ const response = await dependencies.fetch(
180
+ endpoint.healthCheckUrl,
181
+ requestInit
182
+ );
183
+ if (response.ok) {
184
+ const serverVersion = (await response.text()).trim();
185
+ return {
186
+ category: "success",
187
+ endpoint,
188
+ serverVersion
189
+ };
190
+ }
191
+ return {
192
+ category: getFailureCategoryForStatus(response.status),
193
+ reason: `Connectivity check failed with status ${response.status}.`,
194
+ statusCode: response.status,
195
+ endpoint
196
+ };
197
+ } catch (error) {
198
+ const message = error instanceof Error ? error.message : "Connectivity request failed.";
199
+ return {
200
+ category: "unreachable",
201
+ reason: message,
202
+ endpoint
203
+ };
204
+ }
205
+ };
206
+ var getTrimmedValue = (value) => {
207
+ if (value === void 0) {
208
+ return void 0;
209
+ }
210
+ const trimmed = value.trim();
211
+ if (trimmed.length === 0) {
212
+ return void 0;
213
+ }
214
+ return trimmed;
215
+ };
216
+ var getAuthFromOverrides = (overrides) => {
217
+ const apiKey = getTrimmedValue(overrides?.apiKey);
218
+ const bearerToken = getTrimmedValue(overrides?.bearerToken);
219
+ if (apiKey !== void 0 && bearerToken !== void 0) {
220
+ throw new Error("Specify either --api-key or --bearer-token, not both.");
221
+ }
222
+ if (apiKey !== void 0) {
223
+ return {
224
+ kind: "api-key",
225
+ value: apiKey,
226
+ headerName: getTrimmedValue(overrides?.apiKeyHeaderName)
227
+ };
228
+ }
229
+ if (bearerToken !== void 0) {
230
+ return {
231
+ kind: "bearer-token",
232
+ token: bearerToken
233
+ };
234
+ }
235
+ return null;
236
+ };
237
+ var getStatusFromAuth = (auth, source) => {
238
+ if (auth.kind === "none") {
239
+ return {
240
+ isAuthenticated: false,
241
+ kind: "none",
242
+ source: "none"
243
+ };
244
+ }
245
+ return {
246
+ isAuthenticated: true,
247
+ kind: auth.kind,
248
+ source
249
+ };
250
+ };
251
+ var createLighthouseAuthContext = (store) => ({
252
+ resolveAuth: async (overrides) => {
253
+ const overrideAuth = getAuthFromOverrides(overrides);
254
+ if (overrideAuth !== null) {
255
+ return overrideAuth;
256
+ }
257
+ const storedAuth = await store.load();
258
+ if (storedAuth !== null) {
259
+ return storedAuth;
260
+ }
261
+ return {
262
+ kind: "none"
263
+ };
264
+ },
265
+ login: async (overrides) => {
266
+ const auth = getAuthFromOverrides(overrides);
267
+ if (auth === null) {
268
+ throw new Error("Provide --api-key or --bearer-token for auth login.");
269
+ }
270
+ await store.save(auth);
271
+ },
272
+ logout: async () => {
273
+ await store.clear();
274
+ },
275
+ getStatus: async (overrides) => {
276
+ const overrideAuth = getAuthFromOverrides(overrides);
277
+ if (overrideAuth !== null) {
278
+ return getStatusFromAuth(overrideAuth, "override");
279
+ }
280
+ const storedAuth = await store.load();
281
+ if (storedAuth !== null) {
282
+ return getStatusFromAuth(storedAuth, "stored");
283
+ }
284
+ return getStatusFromAuth({ kind: "none" }, "none");
285
+ }
286
+ });
287
+ var getAuthHeaders = (auth) => {
288
+ if (auth === void 0 || auth.kind === "none") {
289
+ return {};
290
+ }
291
+ if (auth.kind === "api-key") {
292
+ return {
293
+ [auth.headerName ?? "X-Api-Key"]: auth.value
294
+ };
295
+ }
296
+ return {
297
+ Authorization: `Bearer ${auth.token}`
298
+ };
299
+ };
300
+ var getRequestInit = (auth, requestOptions) => {
301
+ const authHeaders = getAuthHeaders(auth);
302
+ const hasBody = requestOptions?.body !== void 0;
303
+ const headers = hasBody ? {
304
+ ...authHeaders,
305
+ "content-type": "application/json"
306
+ } : authHeaders;
307
+ return {
308
+ method: requestOptions?.method,
309
+ headers,
310
+ body: hasBody ? JSON.stringify(requestOptions.body) : void 0
311
+ };
312
+ };
313
+ var getFetchDependency = (dependencies) => {
314
+ if (dependencies.fetch !== void 0) {
315
+ return dependencies.fetch;
316
+ }
317
+ return async (url, init) => {
318
+ const response = await fetch(url, init);
319
+ return response;
320
+ };
321
+ };
322
+ var getErrorResult = (error) => ({
323
+ ok: false,
324
+ error
325
+ });
326
+ var getErrorFromConnectivityResult = (result) => ({
327
+ category: result.category,
328
+ reason: result.reason,
329
+ statusCode: result.statusCode
330
+ });
331
+ var toApiError = (statusCode, reason) => ({
332
+ category: getFailureCategoryForStatus(statusCode),
333
+ reason,
334
+ statusCode
335
+ });
336
+ var requestText = async (configuration, dependencies, route) => {
337
+ const fetchDependency = getFetchDependency(dependencies);
338
+ const connectivityResult = await validateLighthouseConnectivity(
339
+ configuration.connection,
340
+ {
341
+ fetch: fetchDependency
342
+ },
343
+ getRequestInit(configuration.auth)
344
+ );
345
+ if (connectivityResult.category !== "success") {
346
+ return getErrorResult(getErrorFromConnectivityResult(connectivityResult));
347
+ }
348
+ try {
349
+ const response = await fetchDependency(
350
+ `${connectivityResult.endpoint.apiBaseUrl}${route}`,
351
+ getRequestInit(configuration.auth)
352
+ );
353
+ if (!response.ok) {
354
+ return getErrorResult(
355
+ toApiError(
356
+ response.status,
357
+ `Request failed with status ${response.status}.`
358
+ )
359
+ );
360
+ }
361
+ return {
362
+ ok: true,
363
+ value: (await response.text()).trim()
364
+ };
365
+ } catch (error) {
366
+ const reason = error instanceof Error ? error.message : "Request execution failed.";
367
+ return getErrorResult({
368
+ category: "unreachable",
369
+ reason
370
+ });
371
+ }
372
+ };
373
+ var requestJson = async (configuration, dependencies, route, requestOptions) => {
374
+ const fetchDependency = getFetchDependency(dependencies);
375
+ const connectivityResult = await validateLighthouseConnectivity(
376
+ configuration.connection,
377
+ {
378
+ fetch: fetchDependency
379
+ },
380
+ getRequestInit(configuration.auth, {
381
+ method: "GET"
382
+ })
383
+ );
384
+ if (connectivityResult.category !== "success") {
385
+ return getErrorResult(getErrorFromConnectivityResult(connectivityResult));
386
+ }
387
+ try {
388
+ const response = await fetchDependency(
389
+ `${connectivityResult.endpoint.apiBaseUrl}${route}`,
390
+ getRequestInit(configuration.auth, requestOptions)
391
+ );
392
+ if (!response.ok) {
393
+ return getErrorResult(
394
+ toApiError(
395
+ response.status,
396
+ `Request failed with status ${response.status}.`
397
+ )
398
+ );
399
+ }
400
+ const value = response.json !== void 0 ? await response.json() : JSON.parse(await response.text());
401
+ return {
402
+ ok: true,
403
+ value
404
+ };
405
+ } catch (error) {
406
+ const reason = error instanceof Error ? error.message : "Request execution failed.";
407
+ return getErrorResult({
408
+ category: "unreachable",
409
+ reason
410
+ });
411
+ }
412
+ };
413
+ var requestNoContent = async (configuration, dependencies, route, requestOptions) => {
414
+ const fetchDependency = getFetchDependency(dependencies);
415
+ const connectivityResult = await validateLighthouseConnectivity(
416
+ configuration.connection,
417
+ {
418
+ fetch: fetchDependency
419
+ },
420
+ getRequestInit(configuration.auth, {
421
+ method: "GET"
422
+ })
423
+ );
424
+ if (connectivityResult.category !== "success") {
425
+ return getErrorResult(getErrorFromConnectivityResult(connectivityResult));
426
+ }
427
+ try {
428
+ const response = await fetchDependency(
429
+ `${connectivityResult.endpoint.apiBaseUrl}${route}`,
430
+ getRequestInit(configuration.auth, requestOptions)
431
+ );
432
+ if (!response.ok) {
433
+ return getErrorResult(
434
+ toApiError(
435
+ response.status,
436
+ `Request failed with status ${response.status}.`
437
+ )
438
+ );
439
+ }
440
+ return {
441
+ ok: true,
442
+ value: void 0
443
+ };
444
+ } catch (error) {
445
+ const reason = error instanceof Error ? error.message : "Request execution failed.";
446
+ return getErrorResult({
447
+ category: "unreachable",
448
+ reason
449
+ });
450
+ }
451
+ };
452
+ var createLighthouseClient = (configuration, dependencies = {}) => ({
453
+ checkConnectivity: async () => {
454
+ const fetchDependency = getFetchDependency(dependencies);
455
+ return validateLighthouseConnectivity(
456
+ configuration.connection,
457
+ {
458
+ fetch: fetchDependency
459
+ },
460
+ getRequestInit(configuration.auth)
461
+ );
462
+ },
463
+ getVersion: async () => requestText(configuration, dependencies, "/v1/version"),
464
+ listWorkTrackingConnections: async () => requestJson(
465
+ configuration,
466
+ dependencies,
467
+ "/v1/worktrackingsystemconnections",
468
+ {
469
+ method: "GET"
470
+ }
471
+ ),
472
+ getWorkTrackingConnection: async (connectionId) => requestJson(
473
+ configuration,
474
+ dependencies,
475
+ `/v1/worktrackingsystemconnections/${connectionId}`,
476
+ {
477
+ method: "GET"
478
+ }
479
+ ),
480
+ createWorkTrackingConnection: async (payload) => requestJson(
481
+ configuration,
482
+ dependencies,
483
+ "/v1/worktrackingsystemconnections",
484
+ {
485
+ method: "POST",
486
+ body: payload
487
+ }
488
+ ),
489
+ updateWorkTrackingConnection: async (connectionId, payload) => requestJson(
490
+ configuration,
491
+ dependencies,
492
+ `/v1/worktrackingsystemconnections/${connectionId}`,
493
+ {
494
+ method: "PUT",
495
+ body: payload
496
+ }
497
+ ),
498
+ deleteWorkTrackingConnection: async (connectionId) => requestNoContent(
499
+ configuration,
500
+ dependencies,
501
+ `/v1/worktrackingsystemconnections/${connectionId}`,
502
+ {
503
+ method: "DELETE"
504
+ }
505
+ ),
506
+ listTeams: async () => requestJson(configuration, dependencies, "/v1/teams", {
507
+ method: "GET"
508
+ }),
509
+ getTeam: async (teamId) => requestJson(configuration, dependencies, `/v1/teams/${teamId}`, {
510
+ method: "GET"
511
+ }),
512
+ createTeam: async (payload) => requestJson(configuration, dependencies, "/v1/teams", {
513
+ method: "POST",
514
+ body: payload
515
+ }),
516
+ updateTeam: async (teamId, payload) => requestJson(configuration, dependencies, `/v1/teams/${teamId}`, {
517
+ method: "PUT",
518
+ body: payload
519
+ }),
520
+ deleteTeam: async (teamId) => requestNoContent(configuration, dependencies, `/v1/teams/${teamId}`, {
521
+ method: "DELETE"
522
+ }),
523
+ refreshTeam: async (teamId) => requestNoContent(configuration, dependencies, `/v1/teams/${teamId}`, {
524
+ method: "POST"
525
+ }),
526
+ listPortfolios: async () => requestJson(
527
+ configuration,
528
+ dependencies,
529
+ "/v1/portfolios",
530
+ {
531
+ method: "GET"
532
+ }
533
+ ),
534
+ getPortfolio: async (portfolioId) => requestJson(
535
+ configuration,
536
+ dependencies,
537
+ `/v1/portfolios/${portfolioId}`,
538
+ {
539
+ method: "GET"
540
+ }
541
+ ),
542
+ createPortfolio: async (payload) => requestJson(configuration, dependencies, "/v1/portfolios", {
543
+ method: "POST",
544
+ body: payload
545
+ }),
546
+ updatePortfolio: async (portfolioId, payload) => requestJson(
547
+ configuration,
548
+ dependencies,
549
+ `/v1/portfolios/${portfolioId}`,
550
+ {
551
+ method: "PUT",
552
+ body: payload
553
+ }
554
+ ),
555
+ deletePortfolio: async (portfolioId) => requestNoContent(
556
+ configuration,
557
+ dependencies,
558
+ `/v1/portfolios/${portfolioId}`,
559
+ {
560
+ method: "DELETE"
561
+ }
562
+ ),
563
+ refreshPortfolio: async (portfolioId) => requestNoContent(
564
+ configuration,
565
+ dependencies,
566
+ `/v1/portfolios/${portfolioId}/refresh`,
567
+ {
568
+ method: "POST"
569
+ }
570
+ )
571
+ });
572
+ export {
573
+ createLighthouseAuthContext,
574
+ createLighthouseClient,
575
+ getClientPackageContract,
576
+ parseStandaloneDiscoveryContract,
577
+ validateLighthouseConnectivity
578
+ };
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@letpeoplework/lighthouse-client",
3
+ "version": "0.1.0",
4
+ "description": "Shared Lighthouse API client contracts",
5
+ "repository": "https://github.com/LetPeopleWork/lighthouse-clients",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
24
+ "clean": "rimraf dist"
25
+ }
26
+ }