simcapture-sdk 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/LICENSE +21 -0
- package/README.md +126 -0
- package/dist/index.cjs +549 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +694 -0
- package/dist/index.d.ts +694 -0
- package/dist/index.js +527 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 georgegiosue
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# simcapture-sdk
|
|
2
|
+
|
|
3
|
+
SimCapture API client shared across the UPCH Simulation Center microservices
|
|
4
|
+
and frontends. Replaces the per-service hand-rolled `axios` + login code that was
|
|
5
|
+
duplicated across SIMCAPTURE-MS, REPORT-MS, ENGINEERING-MS and the frontend.
|
|
6
|
+
|
|
7
|
+
Published to GitHub Packages under the `@upch` scope. Add an `.npmrc`:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
@upch:registry=https://npm.pkg.github.com
|
|
11
|
+
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install simcapture-sdk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { SimCaptureClient, SimCaptureError } from "simcapture-sdk";
|
|
22
|
+
|
|
23
|
+
const sc = new SimCaptureClient({
|
|
24
|
+
apiUrl: process.env.SIMCAPTURE_API!, // https://api.simcapture.com
|
|
25
|
+
inventoryUrl: process.env.SIMCAPTURE_INVENTORY_API!,
|
|
26
|
+
credentials: {
|
|
27
|
+
username: process.env.SIMCAPTURE_USER!,
|
|
28
|
+
password: process.env.SIMCAPTURE_PASSWORD!,
|
|
29
|
+
// clientSubdomain defaults to "simulacion-upch"
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const orgs = await sc.organizations.findAll();
|
|
34
|
+
const reservations = await sc.reservations.findAll({
|
|
35
|
+
endAfterTs: "2025-07-01T05:00:00.000Z",
|
|
36
|
+
startBeforeTs: "2026-12-31T05:00:00.000Z",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await sc.reservations.findOne("bad-id");
|
|
41
|
+
} catch (e) {
|
|
42
|
+
if (e instanceof SimCaptureError) {
|
|
43
|
+
console.error(e.status, e.code, e.body); // real upstream status, not a blanket 400
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Resources
|
|
49
|
+
|
|
50
|
+
| Resource | Methods |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `organizations` | `findAll` |
|
|
53
|
+
| `locations` | `findAll` |
|
|
54
|
+
| `reservations` | `findAll`, `findOne`, `update`, `updateDetails`, `delete` |
|
|
55
|
+
| `courses` | `find`, `getItems` |
|
|
56
|
+
| `scenarios` | `findOne`, `updateSetup`, `updateDetails`, `getAttachment` |
|
|
57
|
+
| `simulators` | `findAll`, `findOne`, `update`, `getConfig` |
|
|
58
|
+
| `notifications` | `send` |
|
|
59
|
+
| `inventory` | `findAll`, `findOne`, `edit` *(inventory server)* |
|
|
60
|
+
|
|
61
|
+
## Auth behaviour
|
|
62
|
+
|
|
63
|
+
- Tokens are cached and reused until a soft TTL (`tokenTtlMs`, default 30 min) elapses.
|
|
64
|
+
- A `401` clears the cache, re-logins **once**, and retries the failed request once.
|
|
65
|
+
- Concurrent callers share a single in-flight `/auth` request.
|
|
66
|
+
|
|
67
|
+
## Architecture (DDD)
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
domain/ entities, value-objects, ports (HttpClient, TokenStore), models, errors
|
|
71
|
+
application/ use-cases: Authenticator + one resource class per domain
|
|
72
|
+
infrastructure/ axios transport adapter + in-memory token store
|
|
73
|
+
config/ SimCaptureConfig + resolution/validation
|
|
74
|
+
client.ts composition root wiring infra → application
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```mermaid
|
|
78
|
+
flowchart TB
|
|
79
|
+
Consumer["Consumer (MS / frontend)"] --> Client["SimCaptureClient<br/>(composition root)"]
|
|
80
|
+
|
|
81
|
+
subgraph application["application"]
|
|
82
|
+
Resources["Resources<br/>reservations · scenarios · …"]
|
|
83
|
+
Auth["Authenticator<br/>token cache · 401 retry"]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
subgraph domain["domain (no deps)"]
|
|
87
|
+
Ports["Ports<br/>HttpClient · TokenStore"]
|
|
88
|
+
Models["Models · VOs · SimCaptureError"]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
subgraph infrastructure["infrastructure"]
|
|
92
|
+
Axios["AxiosHttpClient"]
|
|
93
|
+
Store["InMemoryTokenStore"]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Client --> Resources
|
|
97
|
+
Client --> Auth
|
|
98
|
+
Resources --> Auth
|
|
99
|
+
Resources -.depends on.-> Ports
|
|
100
|
+
Auth -.depends on.-> Ports
|
|
101
|
+
Auth --> Models
|
|
102
|
+
|
|
103
|
+
Axios -.implements.-> Ports
|
|
104
|
+
Store -.implements.-> Ports
|
|
105
|
+
Axios --> SimCapture["SimCapture API<br/>api + inventory servers"]
|
|
106
|
+
|
|
107
|
+
Client -. injects .-> Axios
|
|
108
|
+
Client -. injects .-> Store
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Dependencies point inward — the domain/application layers depend only on the
|
|
112
|
+
`HttpClient`/`TokenStore` ports, so they are unit-tested with fakes (no network).
|
|
113
|
+
Advanced consumers can inject their own transport or a shared token store via the
|
|
114
|
+
second `SimCaptureClient` constructor argument.
|
|
115
|
+
|
|
116
|
+
## Development (Bun)
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
bun install
|
|
120
|
+
bun run typecheck # tsc --noEmit (strict)
|
|
121
|
+
bun test # unit tests against the ports (no real network)
|
|
122
|
+
bun run build # tsup → ESM + CJS + .d.ts in dist/ (Node-compatible)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The published artifact is built with `tsup` for Node ≥18 (ESM + CJS + types). Bun is
|
|
126
|
+
the dev/build/test toolchain only — there are no `bun:*` imports in shipped code.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var axios = require('axios');
|
|
4
|
+
|
|
5
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
6
|
+
|
|
7
|
+
var axios__default = /*#__PURE__*/_interopDefault(axios);
|
|
8
|
+
|
|
9
|
+
// src/domain/errors/simcapture-error.ts
|
|
10
|
+
var SimCaptureError = class _SimCaptureError extends Error {
|
|
11
|
+
/** HTTP status returned by SimCapture, or 0 when the request never completed. */
|
|
12
|
+
status;
|
|
13
|
+
/** Stable machine-readable code for branching (e.g. `AUTH_FAILED`). */
|
|
14
|
+
code;
|
|
15
|
+
/** Raw upstream response body, when available. */
|
|
16
|
+
body;
|
|
17
|
+
constructor(params) {
|
|
18
|
+
super(params.message, params.cause !== void 0 ? { cause: params.cause } : void 0);
|
|
19
|
+
this.name = "SimCaptureError";
|
|
20
|
+
this.status = params.status;
|
|
21
|
+
this.code = params.code;
|
|
22
|
+
this.body = params.body ?? null;
|
|
23
|
+
Object.setPrototypeOf(this, _SimCaptureError.prototype);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/domain/auth/auth-credentials.vo.ts
|
|
28
|
+
var AuthCredentials = class _AuthCredentials {
|
|
29
|
+
static DEFAULT_SUBDOMAIN = "simulacion-upch";
|
|
30
|
+
username;
|
|
31
|
+
password;
|
|
32
|
+
clientSubdomain;
|
|
33
|
+
constructor(params) {
|
|
34
|
+
if (!params.username) {
|
|
35
|
+
throw new SimCaptureError({
|
|
36
|
+
message: "SimCapture credentials: `username` is required.",
|
|
37
|
+
status: 0,
|
|
38
|
+
code: "CONFIG_INVALID"
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (!params.password) {
|
|
42
|
+
throw new SimCaptureError({
|
|
43
|
+
message: "SimCapture credentials: `password` is required.",
|
|
44
|
+
status: 0,
|
|
45
|
+
code: "CONFIG_INVALID"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
this.username = params.username;
|
|
49
|
+
this.password = params.password;
|
|
50
|
+
this.clientSubdomain = params.clientSubdomain ?? _AuthCredentials.DEFAULT_SUBDOMAIN;
|
|
51
|
+
}
|
|
52
|
+
toAuthRequest() {
|
|
53
|
+
return {
|
|
54
|
+
username: this.username,
|
|
55
|
+
password: this.password,
|
|
56
|
+
clientSubdomain: this.clientSubdomain
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/domain/auth/token.vo.ts
|
|
62
|
+
var Token = class _Token {
|
|
63
|
+
value;
|
|
64
|
+
/** Epoch ms after which the token is considered stale, or null when no TTL. */
|
|
65
|
+
expiresAt;
|
|
66
|
+
constructor(value, expiresAt = null) {
|
|
67
|
+
this.value = value;
|
|
68
|
+
this.expiresAt = expiresAt;
|
|
69
|
+
}
|
|
70
|
+
static issue(value, ttlMs, now = Date.now()) {
|
|
71
|
+
const expiresAt = ttlMs > 0 ? now + ttlMs : null;
|
|
72
|
+
return new _Token(value, expiresAt);
|
|
73
|
+
}
|
|
74
|
+
isExpired(now = Date.now()) {
|
|
75
|
+
return this.expiresAt !== null && now >= this.expiresAt;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/application/auth/authenticator.ts
|
|
80
|
+
var Authenticator = class {
|
|
81
|
+
http;
|
|
82
|
+
store;
|
|
83
|
+
credentials;
|
|
84
|
+
tokenTtlMs;
|
|
85
|
+
now;
|
|
86
|
+
inFlightLogin = null;
|
|
87
|
+
constructor(options) {
|
|
88
|
+
this.http = options.http;
|
|
89
|
+
this.store = options.store;
|
|
90
|
+
this.credentials = options.credentials;
|
|
91
|
+
this.tokenTtlMs = options.tokenTtlMs;
|
|
92
|
+
this.now = options.now ?? Date.now;
|
|
93
|
+
}
|
|
94
|
+
/** Returns a valid token, logging in only when none is cached or it is stale. */
|
|
95
|
+
async getToken() {
|
|
96
|
+
const cached = this.store.get();
|
|
97
|
+
if (cached && !cached.isExpired(this.now())) return cached;
|
|
98
|
+
return this.login();
|
|
99
|
+
}
|
|
100
|
+
/** Forces a fresh login, deduping concurrent calls into one `/auth` request. */
|
|
101
|
+
async login() {
|
|
102
|
+
if (this.inFlightLogin) return this.inFlightLogin;
|
|
103
|
+
this.inFlightLogin = this.requestToken().then((token) => {
|
|
104
|
+
this.store.set(token);
|
|
105
|
+
return token;
|
|
106
|
+
}).finally(() => {
|
|
107
|
+
this.inFlightLogin = null;
|
|
108
|
+
});
|
|
109
|
+
return this.inFlightLogin;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Sends `req` with the cached token attached. On a 401 it refreshes the token
|
|
113
|
+
* once and retries the request a single time; a second 401 propagates.
|
|
114
|
+
*/
|
|
115
|
+
async authorizedRequest(req) {
|
|
116
|
+
const token = await this.getToken();
|
|
117
|
+
try {
|
|
118
|
+
return await this.http.request(withToken(req, token.value));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (!isUnauthorized(error)) throw error;
|
|
121
|
+
this.store.clear();
|
|
122
|
+
const fresh = await this.login();
|
|
123
|
+
return this.http.request(withToken(req, fresh.value));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async requestToken() {
|
|
127
|
+
const response = await this.http.request({
|
|
128
|
+
method: "POST",
|
|
129
|
+
path: "/auth",
|
|
130
|
+
body: this.credentials.toAuthRequest(),
|
|
131
|
+
skipAuth: true
|
|
132
|
+
});
|
|
133
|
+
const value = response.data?.token;
|
|
134
|
+
if (!value) {
|
|
135
|
+
throw new SimCaptureError({
|
|
136
|
+
message: "SimCapture /auth did not return a token.",
|
|
137
|
+
status: response.status,
|
|
138
|
+
code: "AUTH_FAILED",
|
|
139
|
+
body: response.data
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return Token.issue(value, this.tokenTtlMs, this.now());
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
function withToken(req, token) {
|
|
146
|
+
return { ...req, headers: { ...req.headers, token } };
|
|
147
|
+
}
|
|
148
|
+
function isUnauthorized(error) {
|
|
149
|
+
return error instanceof SimCaptureError && error.status === 401;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/application/resources/base.resource.ts
|
|
153
|
+
var BaseResource = class {
|
|
154
|
+
constructor(auth) {
|
|
155
|
+
this.auth = auth;
|
|
156
|
+
}
|
|
157
|
+
auth;
|
|
158
|
+
async exec(req) {
|
|
159
|
+
const response = await this.auth.authorizedRequest(req);
|
|
160
|
+
return response.data;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/application/resources/courses.resource.ts
|
|
165
|
+
var CoursesResource = class extends BaseResource {
|
|
166
|
+
constructor(auth) {
|
|
167
|
+
super(auth);
|
|
168
|
+
}
|
|
169
|
+
/** `POST /courses` — one page of results. */
|
|
170
|
+
find(query) {
|
|
171
|
+
return this.exec({ method: "POST", path: "/courses", body: query });
|
|
172
|
+
}
|
|
173
|
+
/** `GET /courses/{courseId}/items` — scenarios / evaluation templates. */
|
|
174
|
+
getItems(courseId, params = {}) {
|
|
175
|
+
return this.exec({
|
|
176
|
+
method: "GET",
|
|
177
|
+
path: `/courses/${encodeURIComponent(courseId)}/items`,
|
|
178
|
+
query: {
|
|
179
|
+
includeScenarios: params.includeScenarios,
|
|
180
|
+
includeEvaluationTemplates: params.includeEvaluationTemplates,
|
|
181
|
+
hasScenarioReflections: params.hasScenarioReflections,
|
|
182
|
+
hasScenarioEvaluations: params.hasScenarioEvaluations
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// src/application/resources/inventory.resource.ts
|
|
189
|
+
var InventoryResource = class extends BaseResource {
|
|
190
|
+
constructor(auth) {
|
|
191
|
+
super(auth);
|
|
192
|
+
}
|
|
193
|
+
/** `GET /items` (server B) */
|
|
194
|
+
findAll() {
|
|
195
|
+
return this.exec({ method: "GET", path: "/items", server: "inventory" });
|
|
196
|
+
}
|
|
197
|
+
/** `GET /item/{itemId}` (server B) */
|
|
198
|
+
findOne(itemId) {
|
|
199
|
+
return this.exec({
|
|
200
|
+
method: "GET",
|
|
201
|
+
path: `/item/${encodeURIComponent(itemId)}`,
|
|
202
|
+
server: "inventory"
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/** `POST /item/{itemId}/edit` (server B) — stock adjustment. */
|
|
206
|
+
edit(itemId, body) {
|
|
207
|
+
return this.exec({
|
|
208
|
+
method: "POST",
|
|
209
|
+
path: `/item/${encodeURIComponent(itemId)}/edit`,
|
|
210
|
+
server: "inventory",
|
|
211
|
+
body
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/application/resources/locations.resource.ts
|
|
217
|
+
var LocationsResource = class extends BaseResource {
|
|
218
|
+
constructor(auth) {
|
|
219
|
+
super(auth);
|
|
220
|
+
}
|
|
221
|
+
/** `GET /locations?order=...` */
|
|
222
|
+
findAll(params = {}) {
|
|
223
|
+
return this.exec({
|
|
224
|
+
method: "GET",
|
|
225
|
+
path: "/locations",
|
|
226
|
+
query: { order: params.order }
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/application/resources/notifications.resource.ts
|
|
232
|
+
var NotificationsResource = class extends BaseResource {
|
|
233
|
+
constructor(auth) {
|
|
234
|
+
super(auth);
|
|
235
|
+
}
|
|
236
|
+
/** `POST /notifications` — send an email / notification. */
|
|
237
|
+
send(body) {
|
|
238
|
+
return this.exec({ method: "POST", path: "/notifications", body });
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// src/application/resources/organizations.resource.ts
|
|
243
|
+
var OrganizationsResource = class extends BaseResource {
|
|
244
|
+
constructor(auth) {
|
|
245
|
+
super(auth);
|
|
246
|
+
}
|
|
247
|
+
/** `GET /organizations` */
|
|
248
|
+
findAll() {
|
|
249
|
+
return this.exec({ method: "GET", path: "/organizations" });
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// src/application/resources/reservations.resource.ts
|
|
254
|
+
var ReservationsResource = class extends BaseResource {
|
|
255
|
+
constructor(auth) {
|
|
256
|
+
super(auth);
|
|
257
|
+
}
|
|
258
|
+
/** `GET /reservations?endAfterTs=&startBeforeTs=&limitFetchedReservationDetails=` */
|
|
259
|
+
findAll(params) {
|
|
260
|
+
return this.exec({
|
|
261
|
+
method: "GET",
|
|
262
|
+
path: "/reservations",
|
|
263
|
+
query: {
|
|
264
|
+
endAfterTs: params.endAfterTs,
|
|
265
|
+
startBeforeTs: params.startBeforeTs,
|
|
266
|
+
limitFetchedReservationDetails: params.limitFetchedReservationDetails ?? false
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
/** `GET /reservation/{id}` */
|
|
271
|
+
findOne(reservationId) {
|
|
272
|
+
return this.exec({
|
|
273
|
+
method: "GET",
|
|
274
|
+
path: `/reservation/${encodeURIComponent(reservationId)}`
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/** `PUT /reservation/{id}` — overview or setup update. */
|
|
278
|
+
update(reservationId, body) {
|
|
279
|
+
return this.exec({
|
|
280
|
+
method: "PUT",
|
|
281
|
+
path: `/reservation/${encodeURIComponent(reservationId)}`,
|
|
282
|
+
body
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/** `PUT /reservation/{id}/details` (ENGINEERING-MS flow). */
|
|
286
|
+
updateDetails(reservationId, body) {
|
|
287
|
+
return this.exec({
|
|
288
|
+
method: "PUT",
|
|
289
|
+
path: `/reservation/${encodeURIComponent(reservationId)}/details`,
|
|
290
|
+
body
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
/** `DELETE /reservation/{id}` */
|
|
294
|
+
delete(reservationId) {
|
|
295
|
+
return this.exec({
|
|
296
|
+
method: "DELETE",
|
|
297
|
+
path: `/reservation/${encodeURIComponent(reservationId)}`
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/application/resources/scenarios.resource.ts
|
|
303
|
+
var ScenariosResource = class extends BaseResource {
|
|
304
|
+
constructor(auth) {
|
|
305
|
+
super(auth);
|
|
306
|
+
}
|
|
307
|
+
/** `GET /scenarios/{id}` */
|
|
308
|
+
findOne(scenarioId) {
|
|
309
|
+
return this.exec({
|
|
310
|
+
method: "GET",
|
|
311
|
+
path: `/scenarios/${encodeURIComponent(scenarioId)}`
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/** `PUT /scenarios/{id}` — setup update. */
|
|
315
|
+
updateSetup(scenarioId, body) {
|
|
316
|
+
return this.exec({
|
|
317
|
+
method: "PUT",
|
|
318
|
+
path: `/scenarios/${encodeURIComponent(scenarioId)}`,
|
|
319
|
+
body
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
/** `PUT /scenarios/{id}/details` — detail update. */
|
|
323
|
+
updateDetails(scenarioId, body) {
|
|
324
|
+
return this.exec({
|
|
325
|
+
method: "PUT",
|
|
326
|
+
path: `/scenarios/${encodeURIComponent(scenarioId)}/details`,
|
|
327
|
+
body
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* `GET /scenarios/{id}/attachments/{assetName}` — returns the raw PDF bytes.
|
|
332
|
+
* The token is sent both as a query param and the header (matching the API).
|
|
333
|
+
*/
|
|
334
|
+
async getAttachment(scenarioId, assetName, params) {
|
|
335
|
+
const token = await this.auth.getToken();
|
|
336
|
+
const response = await this.auth.authorizedRequest({
|
|
337
|
+
method: "GET",
|
|
338
|
+
path: `/scenarios/${encodeURIComponent(scenarioId)}/attachments/${encodeURIComponent(assetName)}`,
|
|
339
|
+
query: { "client-id": params.clientId, token: token.value },
|
|
340
|
+
responseType: "blob"
|
|
341
|
+
});
|
|
342
|
+
return response.data;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// src/application/resources/simulators.resource.ts
|
|
347
|
+
var SimulatorsResource = class extends BaseResource {
|
|
348
|
+
constructor(auth) {
|
|
349
|
+
super(auth);
|
|
350
|
+
}
|
|
351
|
+
/** `GET /simulators` */
|
|
352
|
+
findAll() {
|
|
353
|
+
return this.exec({ method: "GET", path: "/simulators" });
|
|
354
|
+
}
|
|
355
|
+
/** `GET /simulators/{id}` */
|
|
356
|
+
findOne(simulatorId) {
|
|
357
|
+
return this.exec({
|
|
358
|
+
method: "GET",
|
|
359
|
+
path: `/simulators/${encodeURIComponent(simulatorId)}`
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/** `PUT /simulators/{id}` */
|
|
363
|
+
update(simulatorId, body) {
|
|
364
|
+
return this.exec({
|
|
365
|
+
method: "PUT",
|
|
366
|
+
path: `/simulators/${encodeURIComponent(simulatorId)}`,
|
|
367
|
+
body
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
/** `GET /simulator-configs/{id}` */
|
|
371
|
+
getConfig(simulatorId) {
|
|
372
|
+
return this.exec({
|
|
373
|
+
method: "GET",
|
|
374
|
+
path: `/simulator-configs/${encodeURIComponent(simulatorId)}`
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// src/config/sdk-config.ts
|
|
380
|
+
var SIMCAPTURE_DEFAULTS = {
|
|
381
|
+
apiUrl: "https://api.simcapture.com",
|
|
382
|
+
inventoryUrl: "https://inventory-management-production.us-east-1.simcapture.com",
|
|
383
|
+
tokenTtlMs: 30 * 60 * 1e3,
|
|
384
|
+
timeoutMs: 30 * 1e3
|
|
385
|
+
};
|
|
386
|
+
function resolveConfig(config) {
|
|
387
|
+
const apiUrl = stripTrailingSlash(config.apiUrl || SIMCAPTURE_DEFAULTS.apiUrl);
|
|
388
|
+
const inventoryUrl = stripTrailingSlash(config.inventoryUrl || SIMCAPTURE_DEFAULTS.inventoryUrl);
|
|
389
|
+
if (!config.credentials?.username || !config.credentials?.password) {
|
|
390
|
+
throw new SimCaptureError({
|
|
391
|
+
message: "SimCapture config: `credentials.username` and `credentials.password` are required.",
|
|
392
|
+
status: 0,
|
|
393
|
+
code: "CONFIG_INVALID"
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
apiUrl,
|
|
398
|
+
inventoryUrl,
|
|
399
|
+
credentials: config.credentials,
|
|
400
|
+
tokenTtlMs: config.tokenTtlMs ?? SIMCAPTURE_DEFAULTS.tokenTtlMs,
|
|
401
|
+
timeoutMs: config.timeoutMs ?? SIMCAPTURE_DEFAULTS.timeoutMs
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function stripTrailingSlash(url) {
|
|
405
|
+
return url.replace(/\/+$/, "");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/infrastructure/cache/in-memory-token-store.ts
|
|
409
|
+
var InMemoryTokenStore = class {
|
|
410
|
+
token = null;
|
|
411
|
+
get() {
|
|
412
|
+
return this.token;
|
|
413
|
+
}
|
|
414
|
+
set(token) {
|
|
415
|
+
this.token = token;
|
|
416
|
+
}
|
|
417
|
+
clear() {
|
|
418
|
+
this.token = null;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
var AxiosHttpClient = class {
|
|
422
|
+
instances;
|
|
423
|
+
constructor(options) {
|
|
424
|
+
this.instances = {
|
|
425
|
+
api: axios__default.default.create({ baseURL: options.apiUrl, timeout: options.timeoutMs }),
|
|
426
|
+
inventory: axios__default.default.create({ baseURL: options.inventoryUrl, timeout: options.timeoutMs })
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async request(req) {
|
|
430
|
+
const instance = this.instances[req.server ?? "api"];
|
|
431
|
+
const config = {
|
|
432
|
+
method: req.method,
|
|
433
|
+
url: req.path,
|
|
434
|
+
responseType: req.responseType ?? "json"
|
|
435
|
+
};
|
|
436
|
+
const params = pruneUndefined(req.query);
|
|
437
|
+
if (params) config.params = params;
|
|
438
|
+
if (req.body !== void 0) config.data = req.body;
|
|
439
|
+
if (req.headers) config.headers = req.headers;
|
|
440
|
+
try {
|
|
441
|
+
const response = await instance.request(config);
|
|
442
|
+
return { status: response.status, data: response.data };
|
|
443
|
+
} catch (error) {
|
|
444
|
+
throw toSimCaptureError(error);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
function pruneUndefined(query) {
|
|
449
|
+
if (!query) return void 0;
|
|
450
|
+
const out = {};
|
|
451
|
+
for (const [key, value] of Object.entries(query)) {
|
|
452
|
+
if (value !== void 0) out[key] = value;
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
}
|
|
456
|
+
function toSimCaptureError(error) {
|
|
457
|
+
if (error instanceof SimCaptureError) return error;
|
|
458
|
+
if (axios.isAxiosError(error)) {
|
|
459
|
+
const status = error.response?.status ?? 0;
|
|
460
|
+
if (status === 0) {
|
|
461
|
+
return new SimCaptureError({
|
|
462
|
+
message: `SimCapture request failed: ${error.message}`,
|
|
463
|
+
status: 0,
|
|
464
|
+
code: "NETWORK",
|
|
465
|
+
cause: error
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
return new SimCaptureError({
|
|
469
|
+
message: `SimCapture responded ${status}: ${error.message}`,
|
|
470
|
+
status,
|
|
471
|
+
code: status === 401 ? "UNAUTHORIZED" : "UPSTREAM_ERROR",
|
|
472
|
+
body: error.response?.data,
|
|
473
|
+
cause: error
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return new SimCaptureError({
|
|
477
|
+
message: error instanceof Error ? error.message : "Unknown SimCapture error",
|
|
478
|
+
status: 0,
|
|
479
|
+
code: "NETWORK",
|
|
480
|
+
cause: error
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/client.ts
|
|
485
|
+
var SimCaptureClient = class {
|
|
486
|
+
organizations;
|
|
487
|
+
locations;
|
|
488
|
+
reservations;
|
|
489
|
+
courses;
|
|
490
|
+
scenarios;
|
|
491
|
+
simulators;
|
|
492
|
+
notifications;
|
|
493
|
+
inventory;
|
|
494
|
+
authenticator;
|
|
495
|
+
constructor(config, deps = {}) {
|
|
496
|
+
const resolved = resolveConfig(config);
|
|
497
|
+
const http = deps.httpClient ?? new AxiosHttpClient({
|
|
498
|
+
apiUrl: resolved.apiUrl,
|
|
499
|
+
inventoryUrl: resolved.inventoryUrl,
|
|
500
|
+
timeoutMs: resolved.timeoutMs
|
|
501
|
+
});
|
|
502
|
+
const store = deps.tokenStore ?? new InMemoryTokenStore();
|
|
503
|
+
const authenticatorOptions = {
|
|
504
|
+
http,
|
|
505
|
+
store,
|
|
506
|
+
credentials: new AuthCredentials(resolved.credentials),
|
|
507
|
+
tokenTtlMs: resolved.tokenTtlMs,
|
|
508
|
+
...deps.now ? { now: deps.now } : {}
|
|
509
|
+
};
|
|
510
|
+
this.authenticator = new Authenticator(authenticatorOptions);
|
|
511
|
+
this.organizations = new OrganizationsResource(this.authenticator);
|
|
512
|
+
this.locations = new LocationsResource(this.authenticator);
|
|
513
|
+
this.reservations = new ReservationsResource(this.authenticator);
|
|
514
|
+
this.courses = new CoursesResource(this.authenticator);
|
|
515
|
+
this.scenarios = new ScenariosResource(this.authenticator);
|
|
516
|
+
this.simulators = new SimulatorsResource(this.authenticator);
|
|
517
|
+
this.notifications = new NotificationsResource(this.authenticator);
|
|
518
|
+
this.inventory = new InventoryResource(this.authenticator);
|
|
519
|
+
}
|
|
520
|
+
/** Force a fresh login. Rarely needed — auth is handled lazily per request. */
|
|
521
|
+
login() {
|
|
522
|
+
return this.authenticator.login().then(() => void 0);
|
|
523
|
+
}
|
|
524
|
+
/** Returns a valid session token, logging in only if none is cached. */
|
|
525
|
+
async getToken() {
|
|
526
|
+
const token = await this.authenticator.getToken();
|
|
527
|
+
return { token: token.value };
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
exports.AuthCredentials = AuthCredentials;
|
|
532
|
+
exports.Authenticator = Authenticator;
|
|
533
|
+
exports.AxiosHttpClient = AxiosHttpClient;
|
|
534
|
+
exports.CoursesResource = CoursesResource;
|
|
535
|
+
exports.InMemoryTokenStore = InMemoryTokenStore;
|
|
536
|
+
exports.InventoryResource = InventoryResource;
|
|
537
|
+
exports.LocationsResource = LocationsResource;
|
|
538
|
+
exports.NotificationsResource = NotificationsResource;
|
|
539
|
+
exports.OrganizationsResource = OrganizationsResource;
|
|
540
|
+
exports.ReservationsResource = ReservationsResource;
|
|
541
|
+
exports.SIMCAPTURE_DEFAULTS = SIMCAPTURE_DEFAULTS;
|
|
542
|
+
exports.ScenariosResource = ScenariosResource;
|
|
543
|
+
exports.SimCaptureClient = SimCaptureClient;
|
|
544
|
+
exports.SimCaptureError = SimCaptureError;
|
|
545
|
+
exports.SimulatorsResource = SimulatorsResource;
|
|
546
|
+
exports.Token = Token;
|
|
547
|
+
exports.resolveConfig = resolveConfig;
|
|
548
|
+
//# sourceMappingURL=index.cjs.map
|
|
549
|
+
//# sourceMappingURL=index.cjs.map
|