@aperture-browser/api-client 0.0.0-init.0 → 0.0.0-nightly.100.78b4e7ef6edc

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.js CHANGED
@@ -1,15 +1,22 @@
1
+ import { t as AuthApi } from "./service-riqZPQHq.js";
1
2
  import * as Effect from "effect/Effect";
2
3
  import * as Layer from "effect/Layer";
3
- import * as HttpBody from "effect/unstable/http/HttpBody";
4
- import * as HttpClient from "effect/unstable/http/HttpClient";
5
- import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
4
+ import * as HttpBody from "effect/http/HttpBody";
5
+ import * as HttpClient from "effect/http/HttpClient";
6
+ import * as HttpClientResponse from "effect/http/HttpClientResponse";
6
7
  import * as Api from "@aperture-browser/api-schema";
7
8
  import * as Context from "effect/Context";
8
9
  import * as Schema from "effect/Schema";
9
10
  import * as PubSub from "effect/PubSub";
11
+ import * as Redacted from "effect/Redacted";
10
12
  import * as Stream from "effect/Stream";
11
13
  import * as Function from "effect/Function";
12
- import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
14
+ import * as HttpClientRequest from "effect/http/HttpClientRequest";
15
+ import * as HttpClientError from "effect/http/HttpClientError";
16
+ import * as Option from "effect/Option";
17
+ import * as Arr from "effect/Array";
18
+ import * as SchemaTransformation from "effect/SchemaTransformation";
19
+ import * as Random from "effect/Random";
13
20
  //#region src/authorization/service.ts
14
21
  const TENANT_HEADER = "X-Aperture-Tenant-Id";
15
22
  /** The browser's own login session, sent as a cookie. */
@@ -172,10 +179,51 @@ const TOTPEnrollment = Schema.Struct({
172
179
  qrCodeDataUrl: Schema.String
173
180
  });
174
181
  const RecoveryCodes = Schema.Struct({ recoveryCodes: Schema.Array(Schema.String) });
175
- //#endregion
176
- //#region src/auth/service.ts
177
- /** Browser login, account security, and the current principal. */
178
- var AuthApi = class extends Context.Service()("@aperture-browser/api-client/AuthApi") {};
182
+ const OAuthClient = Schema.Struct({
183
+ id: Schema.String,
184
+ name: Schema.String,
185
+ uri: Schema.NullOr(Schema.String),
186
+ logoUri: Schema.NullOr(Schema.String),
187
+ kind: Schema.Literals(["registered", "metadata_document"])
188
+ });
189
+ /** A pending OAuth authorization request, as the consent page presents it. */
190
+ const OAuthAuthorizationRequest = Schema.Struct({
191
+ client: OAuthClient,
192
+ redirectUri: Schema.String,
193
+ requestedScopes: Schema.Array(Schema.String),
194
+ user: Schema.Struct({
195
+ id: Schema.String,
196
+ displayName: Schema.String,
197
+ isSystemAdmin: Schema.Boolean
198
+ }),
199
+ tenants: Schema.Array(Schema.Struct({
200
+ id: Schema.String,
201
+ displayName: Schema.String,
202
+ scopes: Schema.Array(Api.TenantScope)
203
+ })),
204
+ availableScopes: Schema.Array(Api.TenantScope),
205
+ canGrantSystemAdmin: Schema.Boolean
206
+ });
207
+ /** Where the browser goes after the user approves or denies an authorization request. */
208
+ const OAuthRedirect = Schema.Struct({ redirectUrl: Schema.String });
209
+ const OAuthGrant = Schema.Struct({
210
+ id: Schema.String,
211
+ client: OAuthClient,
212
+ authorityType: Api.AuthorityType,
213
+ tenants: Schema.Array(Schema.Struct({
214
+ id: Schema.String,
215
+ displayName: Schema.String
216
+ })),
217
+ scopes: Schema.Array(Schema.String),
218
+ resourceMode: Api.ResourceMode,
219
+ resourceGrants: Schema.Array(Schema.Struct({
220
+ resourceType: Api.ResourceType,
221
+ resourceId: Schema.String
222
+ })),
223
+ createdAt: Schema.String,
224
+ lastUsedAt: Schema.NullOr(Schema.String)
225
+ });
226
+ const OAuthGrants = Schema.Struct({ grants: Schema.Array(OAuthGrant) });
179
227
  //#endregion
180
228
  //#region src/auth/layer.ts
181
229
  const jsonBody = (body) => ({ body: HttpBody.jsonUnsafe(body) });
@@ -261,6 +309,21 @@ const makeAuthApi = Effect.gen(function* () {
261
309
  const disableTOTP = Effect.fn("AuthApi.disableTOTP")(function* (code) {
262
310
  yield* http.post("/auth/totp/disable", jsonBody({ code })).pipe(webSession);
263
311
  });
312
+ const getOAuthAuthorization = Effect.fn("AuthApi.getOAuthAuthorization")(function* (query) {
313
+ return yield* http.get(`/auth/oauth/authorization?${query}`).pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(OAuthAuthorizationRequest)), webSession);
314
+ });
315
+ const approveOAuthAuthorization = Effect.fn("AuthApi.approveOAuthAuthorization")(function* (approval) {
316
+ return yield* http.post("/auth/oauth/authorization/approve", jsonBody(approval)).pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(OAuthRedirect)), webSession);
317
+ });
318
+ const denyOAuthAuthorization = Effect.fn("AuthApi.denyOAuthAuthorization")(function* (query) {
319
+ return yield* http.post("/auth/oauth/authorization/deny", jsonBody({ query })).pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(OAuthRedirect)), webSession);
320
+ });
321
+ const listOAuthGrants = Effect.fn("AuthApi.listOAuthGrants")(function* () {
322
+ return yield* http.get("/auth/oauth/grants").pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(OAuthGrants)), webSession);
323
+ });
324
+ const revokeOAuthGrant = Effect.fn("AuthApi.revokeOAuthGrant")(function* (grantId) {
325
+ yield* http.del(`/auth/oauth/grants/${encodeURIComponent(grantId)}`).pipe(webSession);
326
+ });
264
327
  return AuthApi.of({
265
328
  listLoginMethods,
266
329
  loginWithPassword,
@@ -281,7 +344,12 @@ const makeAuthApi = Effect.gen(function* () {
281
344
  beginTOTPEnrollment,
282
345
  completeTOTPEnrollment,
283
346
  regenerateRecoveryCodes,
284
- disableTOTP
347
+ disableTOTP,
348
+ getOAuthAuthorization,
349
+ approveOAuthAuthorization,
350
+ denyOAuthAuthorization,
351
+ listOAuthGrants,
352
+ revokeOAuthGrant
285
353
  });
286
354
  });
287
355
  const authApiLayer = Layer.effect(AuthApi, makeAuthApi);
@@ -292,48 +360,64 @@ const ApiErrorBody = Schema.Struct({ error: Schema.Struct({
292
360
  message: Schema.String
293
361
  }) });
294
362
  /**
363
+ * The error body of a live-session route that failed inside the running session. It carries
364
+ * no stable code, so the resulting ApiRequestError uses `live_session_error` and callers
365
+ * branch on its status.
366
+ */
367
+ const LiveSessionErrorBody = Schema.Struct({ error: Schema.String });
368
+ const ErrorResponseBody = Schema.Union([ApiErrorBody, LiveSessionErrorBody]);
369
+ /**
295
370
  * A failed API request. `code` and `message` come from the server's error body when it
296
- * sent one; `status` is 0 when no HTTP status applies.
371
+ * sent one; `status` is 0 when no HTTP status applies. `cause` is the underlying HTTP or
372
+ * decoding failure when the server's error body alone does not explain it.
297
373
  */
298
374
  var ApiRequestError = class extends Schema.TaggedError()("ApiRequestError", {
299
375
  code: Schema.String,
300
376
  message: Schema.String,
301
- status: Schema.Number
377
+ status: Schema.Number,
378
+ cause: Schema.optional(Schema.Union([Schema.instanceOf(HttpClientError.HttpClientError), Schema.instanceOf(Schema.SchemaError)]))
302
379
  }) {};
303
- const invalidResponse = (status) => new ApiRequestError({
380
+ const invalidResponse = (status, cause) => new ApiRequestError({
304
381
  code: "internal_error",
305
382
  message: "Invalid response",
306
- status
307
- });
308
- const networkError = new ApiRequestError({
309
- code: "network_error",
310
- message: "The server could not be reached",
311
- status: 0
383
+ status,
384
+ cause
312
385
  });
313
386
  /**
314
387
  * Maps HTTP and decoding failures to ApiRequestError. Error responses keep the code and
315
388
  * message of the server's error body.
316
389
  */
317
390
  const toApiRequestError = (self) => self.pipe(Effect.catchReasons("HttpClientError", {
318
- StatusCodeError: ({ response }) => HttpClientResponse.schemaBodyJson(ApiErrorBody)(response).pipe(Effect.map(({ error }) => new ApiRequestError({
391
+ StatusCodeError: ({ response }, cause) => HttpClientResponse.schemaBodyJson(ErrorResponseBody)(response).pipe(Effect.map(({ error }) => typeof error === "string" ? new ApiRequestError({
392
+ code: "live_session_error",
393
+ message: error,
394
+ status: response.status
395
+ }) : new ApiRequestError({
319
396
  ...error,
320
397
  status: response.status
321
398
  })), Effect.orElseSucceed(() => new ApiRequestError({
322
399
  code: "internal_error",
323
400
  message: "Request failed",
324
- status: response.status
401
+ status: response.status,
402
+ cause
325
403
  })), Effect.flatMap(Effect.fail)),
326
- DecodeError: ({ response }) => Effect.fail(invalidResponse(response.status))
404
+ DecodeError: ({ response }, cause) => Effect.fail(invalidResponse(response.status, cause))
327
405
  }), Effect.catchTags({
328
- HttpClientError: () => Effect.fail(networkError),
329
- SchemaError: () => Effect.fail(invalidResponse(0))
406
+ HttpClientError: (cause) => Effect.fail(new ApiRequestError({
407
+ code: "network_error",
408
+ message: "The server could not be reached",
409
+ status: 0,
410
+ cause
411
+ })),
412
+ SchemaError: (cause) => Effect.fail(invalidResponse(0, cause))
330
413
  }));
331
414
  //#endregion
332
415
  //#region src/authorization/layer.ts
333
416
  /** The authorization of the call a request belongs to; set by `authorize`. */
334
417
  const CurrentAuthorization = Context.Reference("@aperture-browser/api-client/CurrentAuthorization", { defaultValue: () => ({}) });
335
418
  const authorizeRequest = (request) => CurrentAuthorization.useSync(({ credentials, bearerToken, tenantHeader = "none" }) => {
336
- const token = bearerToken ?? (credentials?.kind === "bearer" ? credentials.token.trim() : void 0);
419
+ const secret = bearerToken ?? (credentials?.kind === "bearer" ? credentials.token : void 0);
420
+ const token = secret === void 0 ? void 0 : Redacted.value(secret).trim();
337
421
  const tenantId = credentials ? resolveTenantHeader(credentials, tenantHeader) : void 0;
338
422
  return request.pipe(HttpClientRequest.acceptJson, token ? HttpClientRequest.bearerToken(token) : Function.identity, tenantId ? HttpClientRequest.setHeader(TENANT_HEADER, tenantId) : HttpClientRequest.removeHeader(TENANT_HEADER));
339
423
  });
@@ -359,6 +443,43 @@ const makeApiAuthorization = Effect.gen(function* () {
359
443
  });
360
444
  const apiAuthorizationLayer = Layer.effect(ApiAuthorization, makeApiAuthorization);
361
445
  //#endregion
446
+ //#region src/events/service.ts
447
+ /** The tenant's resource event log, and the deployment-wide audit log. */
448
+ var EventsApi = class extends Context.Service()("@aperture-browser/api-client/EventsApi") {};
449
+ //#endregion
450
+ //#region src/health/service.ts
451
+ /** The instance's unauthenticated health check. */
452
+ var HealthApi = class extends Context.Service()("@aperture-browser/api-client/HealthApi") {};
453
+ //#endregion
454
+ //#region src/pagination.ts
455
+ function getNextPageParam(page) {
456
+ return page.meta.hasMore ? page.meta.nextCursor : void 0;
457
+ }
458
+ function flattenInfinitePages(pages) {
459
+ return pages?.flatMap((page) => page.data) ?? [];
460
+ }
461
+ /**
462
+ * Derives `stream` and `listAll` from the call that fetches one page. That call must also
463
+ * accept a bare cursor, which every filter allows because all its fields are optional.
464
+ */
465
+ function paginated(list) {
466
+ const stream = (credentials, filter) => Stream.paginate(void 0, (cursor) => list(credentials, filter === void 0 ? { cursor } : {
467
+ ...filter,
468
+ cursor
469
+ }).pipe(Effect.map((page) => {
470
+ const next = getNextPageParam(page);
471
+ return [page.data, next === void 0 ? Option.none() : Option.some(next)];
472
+ })));
473
+ const listAll = (credentials, filter) => Stream.runCollect(stream(credentials, filter));
474
+ return {
475
+ list,
476
+ stream,
477
+ listAll
478
+ };
479
+ }
480
+ const defaultListLimit = 50;
481
+ const listQueryDefaults = { refetchOnWindowFocus: true };
482
+ //#endregion
362
483
  //#region src/query.ts
363
484
  /** Encodes tag filters as the parallel tagKey, tagOperator and tagValue query lists. */
364
485
  const tagQuery = (tags) => ({
@@ -374,10 +495,6 @@ const compactQuery = (query) => Object.fromEntries(Object.entries(query).flatMap
374
495
  return items.length > 0 ? [[key, items]] : [];
375
496
  }));
376
497
  //#endregion
377
- //#region src/events/service.ts
378
- /** The tenant's resource event log. */
379
- var EventsApi = class extends Context.Service()("@aperture-browser/api-client/EventsApi") {};
380
- //#endregion
381
498
  //#region src/events/layer.ts
382
499
  const makeEventsApi = Effect.gen(function* () {
383
500
  const httpClient = yield* HttpClient.HttpClient;
@@ -391,41 +508,98 @@ const makeEventsApi = Effect.gen(function* () {
391
508
  resourceId: params.resourceId
392
509
  }) }).pipe(authorize(Authorization.tenantScoped(credentials)));
393
510
  });
394
- return EventsApi.of({ listEvents });
511
+ const listAuditEvents = Effect.fn("EventsApi.listAuditEvents")(function* (credentials, params = {}) {
512
+ return yield* api.listAuditEvents({ params: compactQuery({
513
+ limit: params.limit,
514
+ cursor: params.cursor,
515
+ tenantId: params.tenantId,
516
+ actorType: params.actorType,
517
+ actorId: params.actorId,
518
+ action: params.action,
519
+ resourceType: params.resourceType,
520
+ resourceId: params.resourceId
521
+ }) }).pipe(authorize(Authorization.of(credentials)));
522
+ });
523
+ const events = paginated(listEvents);
524
+ const auditEvents = paginated(listAuditEvents);
525
+ return EventsApi.of({
526
+ listEvents,
527
+ streamEvents: events.stream,
528
+ listAllEvents: events.listAll,
529
+ listAuditEvents,
530
+ streamAuditEvents: auditEvents.stream,
531
+ listAllAuditEvents: auditEvents.listAll
532
+ });
395
533
  });
396
534
  const eventsApiLayer = Layer.effect(EventsApi, makeEventsApi);
397
535
  //#endregion
536
+ //#region src/health/layer.ts
537
+ const makeHealthApi = Effect.gen(function* () {
538
+ const httpClient = yield* HttpClient.HttpClient;
539
+ const { authorize } = yield* ApiAuthorization;
540
+ const api = Api.make(httpClient);
541
+ const getHealth = Effect.fn("HealthApi.getHealth")(function* () {
542
+ return yield* api.getHealth(void 0).pipe(authorize(Authorization.anonymous));
543
+ });
544
+ return HealthApi.of({ getHealth });
545
+ });
546
+ const healthApiLayer = Layer.effect(HealthApi, makeHealthApi);
547
+ //#endregion
398
548
  //#region src/sessions/schemas.ts
399
549
  const positiveInt = Schema.Number.check(Schema.isInt(), Schema.isGreaterThan(0));
400
550
  const emptyArray = Effect.succeed([]);
401
- const BrowserStatus = Schema.Struct({
551
+ const BrowserPage = Schema.Struct({
552
+ targetId: Schema.String,
553
+ state: Schema.Literals([
554
+ "pending",
555
+ "ready",
556
+ "unavailable",
557
+ "closed"
558
+ ]),
559
+ title: Schema.String,
560
+ url: Schema.String,
561
+ viewport: Schema.optionalKey(Schema.Struct({
562
+ width: positiveInt,
563
+ height: positiveInt,
564
+ deviceScaleFactor: Schema.Number.check(Schema.isGreaterThan(0)),
565
+ contentWidth: positiveInt,
566
+ contentHeight: positiveInt,
567
+ canvasWidth: positiveInt,
568
+ canvasHeight: positiveInt
569
+ })),
570
+ thumbnailAvailable: Schema.Boolean
571
+ });
572
+ const browserStatusCommon = {
402
573
  sessionId: Schema.String,
403
574
  cdpUrl: Schema.String,
404
575
  media: Api.SessionMedia,
405
- targets: Schema.Array(Schema.Struct({
406
- targetId: Schema.String,
407
- generation: positiveInt,
408
- state: Schema.Literals([
409
- "pending",
410
- "ready",
411
- "unavailable",
412
- "closed"
413
- ]),
414
- title: Schema.String,
415
- url: Schema.String,
416
- viewport: Schema.Struct({
417
- width: positiveInt,
418
- height: positiveInt,
419
- deviceScaleFactor: Schema.Number.check(Schema.isGreaterThan(0)),
420
- contentWidth: positiveInt,
421
- contentHeight: positiveInt,
422
- canvasWidth: positiveInt,
423
- canvasHeight: positiveInt
424
- })
425
- })).pipe(Schema.withDecodingDefaultKey(emptyArray))
426
- });
427
- /** A recording as the live session reports it. */
428
- const Recording = Schema.Struct({
576
+ thumbnailAvailable: Schema.Boolean,
577
+ pages: Schema.Array(BrowserPage).pipe(Schema.withDecodingDefaultKey(emptyArray))
578
+ };
579
+ const availableBrowserStatus = {
580
+ ...browserStatusCommon,
581
+ capturedAt: Schema.String,
582
+ representativeTargetId: Schema.optionalKey(Schema.String)
583
+ };
584
+ /** Passive page discovery. Reading it never wakes or keeps a browser session alive. */
585
+ const BrowserStatus = Schema.Union([
586
+ Schema.Struct({
587
+ ...availableBrowserStatus,
588
+ status: Schema.Literal("running"),
589
+ source: Schema.Literal("live")
590
+ }),
591
+ Schema.Struct({
592
+ ...availableBrowserStatus,
593
+ status: Schema.Literal("suspended"),
594
+ source: Schema.Literal("persisted")
595
+ }),
596
+ Schema.Struct({
597
+ ...browserStatusCommon,
598
+ status: Schema.Literal("suspended"),
599
+ source: Schema.Literal("unavailable")
600
+ })
601
+ ]);
602
+ const recordingFields = {
429
603
  recordingId: Schema.String,
430
604
  mode: Schema.Literals(["tab", "viewer"]),
431
605
  targetId: Schema.String,
@@ -437,21 +611,81 @@ const Recording = Schema.Struct({
437
611
  "failed"
438
612
  ]),
439
613
  stopReason: Schema.optionalKey(Schema.String),
440
- path: Schema.String,
614
+ sandboxPath: Schema.optionalKey(Schema.String),
615
+ /** @deprecated Read `relativePath`. Sessions started before it existed send only this. */
616
+ path: Schema.optionalKey(Schema.String),
441
617
  startedAt: Schema.String,
442
618
  stoppedAt: Schema.optionalKey(Schema.String),
443
619
  sizeBytes: Schema.optionalKey(Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))),
444
620
  fps: positiveInt,
445
621
  bitrateKbps: positiveInt,
446
- codec: Schema.String
447
- });
622
+ codec: Schema.String,
623
+ /** The stop returned and the edit is still running; `editedRelativePath` or `editError` follows. */
624
+ editing: Schema.Boolean,
625
+ editedRelativePath: Schema.optionalKey(Schema.String),
626
+ timelineRelativePath: Schema.optionalKey(Schema.String),
627
+ editError: Schema.optionalKey(Api.RecordingEditError)
628
+ };
629
+ /**
630
+ * A recording as the live session reports it. Sessions that are still running an
631
+ * older wrapper send only `path`, which decoding carries over into `relativePath`.
632
+ */
633
+ const Recording = Schema.Struct({
634
+ ...recordingFields,
635
+ relativePath: Schema.optionalKey(Schema.String)
636
+ }).pipe(Schema.decodeTo(Schema.Struct({
637
+ ...recordingFields,
638
+ relativePath: Schema.String
639
+ }), SchemaTransformation.transform({
640
+ decode: ({ relativePath, ...recording }) => ({
641
+ ...recording,
642
+ relativePath: relativePath ?? recording.path ?? ""
643
+ }),
644
+ encode: (recording) => recording
645
+ })));
448
646
  //#endregion
449
647
  //#region src/sessions/service.ts
450
648
  /** Browser sessions, their live browser, and their recordings. */
451
649
  var SessionsApi = class extends Context.Service()("@aperture-browser/api-client/SessionsApi") {};
452
650
  //#endregion
651
+ //#region src/sessions/uploads.ts
652
+ const encoder = new TextEncoder();
653
+ const quotedFilename = (name) => name.replaceAll("\"", "%22").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
654
+ const contentStream = (content) => {
655
+ if (content instanceof Blob) return Stream.fromReadableStream({
656
+ evaluate: () => content.stream(),
657
+ onError: (cause) => cause
658
+ });
659
+ if (content instanceof Uint8Array) return Stream.succeed(content);
660
+ return content;
661
+ };
662
+ const streamedMultipart = Effect.fn("SessionsApi.streamedMultipart")(function* (files) {
663
+ const boundary = `aperture-upload-${(yield* Random.nextIntBetween(0, Number.MAX_SAFE_INTEGER)).toString(36)}`;
664
+ const parts = files.map((file) => {
665
+ const header = `--${boundary}\r\nContent-Disposition: form-data; name="files"; filename="${quotedFilename(file.name)}"\r\nContent-Type: application/octet-stream\r
666
+ \r
667
+ `;
668
+ return Stream.succeed(encoder.encode(header)).pipe(Stream.concat(contentStream(file.content)), Stream.concat(Stream.succeed(encoder.encode("\r\n"))));
669
+ });
670
+ const body = Stream.fromIterable(parts).pipe(Stream.flatten(), Stream.concat(Stream.succeed(encoder.encode(`--${boundary}--\r\n`))));
671
+ return HttpBody.stream(body, `multipart/form-data; boundary=${boundary}`);
672
+ });
673
+ /**
674
+ * The multipart body of an upload. FormData works with every fetch, so it is used unless a
675
+ * content is a stream, which needs the body itself to be streamed.
676
+ */
677
+ const uploadBody = Effect.fn("SessionsApi.uploadBody")(function* (files) {
678
+ const form = new FormData();
679
+ for (const file of files) if (file.content instanceof Blob) form.append("files", file.content, file.name);
680
+ else if (file.content instanceof Uint8Array) form.append("files", new Blob([new Uint8Array(file.content)]), file.name);
681
+ else return yield* streamedMultipart(files);
682
+ return HttpBody.formData(form);
683
+ });
684
+ //#endregion
453
685
  //#region src/sessions/layer.ts
454
686
  const contentDispositionFilename = (header) => header?.match(/filename="([^"]+)"/)?.[1] ?? null;
687
+ const recordingContentPath = (sessionId, recordingId) => `/sessions/${encodeURIComponent(sessionId)}/recordings/${encodeURIComponent(recordingId)}/content`;
688
+ const BULK_SESSION_IDS_LIMIT = 100;
455
689
  const makeSessionsApi = Effect.gen(function* () {
456
690
  const httpClient = yield* HttpClient.HttpClient;
457
691
  const { authorize } = yield* ApiAuthorization;
@@ -464,19 +698,24 @@ const makeSessionsApi = Effect.gen(function* () {
464
698
  cursor: params.cursor,
465
699
  includeDeleted: params.includeDeleted || void 0,
466
700
  status: params.status,
701
+ query: params.query,
467
702
  ...tagQuery(params.tags)
468
703
  }) }).pipe(tenantScoped(credentials));
469
704
  });
705
+ const sessions = paginated(listSessions);
706
+ const exportSessionStorageState = Effect.fn("SessionsApi.exportSessionStorageState")(function* (credentials, sessionId, input) {
707
+ return yield* api.exportSessionStorageState(sessionId, { payload: input }).pipe(tenantScoped(credentials));
708
+ });
470
709
  const getSession = Effect.fn("SessionsApi.getSession")(function* (credentials, sessionId) {
471
710
  return yield* api.getSession(sessionId, void 0).pipe(tenantScoped(credentials));
472
711
  });
473
712
  const getSessionsBulk = Effect.fn("SessionsApi.getSessionsBulk")(function* (credentials, sessionIds) {
474
- return yield* api.getSessionsBulk({ payload: { ids: sessionIds } }).pipe(tenantScoped(credentials));
713
+ return { sessions: (yield* Effect.forEach(Arr.chunksOf(sessionIds, BULK_SESSION_IDS_LIMIT), (ids) => api.getSessionsBulk({ payload: { ids } }).pipe(tenantScoped(credentials)))).flatMap((response) => response.sessions) };
475
714
  });
476
715
  const createSession = Effect.fn("SessionsApi.createSession")(function* (credentials, input, options = {}) {
477
716
  return yield* api.createSession({
478
717
  params: compactQuery({ waitForReady: options.waitForReady }),
479
- payload: {
718
+ payload: Schema.encodeSync(Api.CreateSessionRequestJson)({
480
719
  baseSnapshotName: input.baseSnapshotName ?? null,
481
720
  label: input.label ?? null,
482
721
  browser: {
@@ -485,8 +724,9 @@ const makeSessionsApi = Effect.gen(function* () {
485
724
  },
486
725
  initialTargets: input.initialTargets ?? [],
487
726
  ...input.storageState === void 0 ? {} : { storageState: input.storageState },
488
- tags: input.tags ?? {}
489
- }
727
+ tags: input.tags ?? {},
728
+ ...input.proxy === void 0 ? {} : { proxy: input.proxy }
729
+ })
490
730
  }).pipe(tenantScoped(credentials));
491
731
  });
492
732
  const deleteSession = Effect.fn("SessionsApi.deleteSession")(function* (credentials, sessionId) {
@@ -507,6 +747,9 @@ const makeSessionsApi = Effect.gen(function* () {
507
747
  const replaceSessionTags = Effect.fn("SessionsApi.replaceSessionTags")(function* (credentials, sessionId, tags) {
508
748
  return yield* api.replaceSessionTags(sessionId, { payload: { tags } }).pipe(tenantScoped(credentials));
509
749
  });
750
+ const updateSessionProxy = Effect.fn("SessionsApi.updateSessionProxy")(function* (credentials, sessionId, proxy) {
751
+ return yield* api.updateSessionProxy(sessionId, { payload: Schema.encodeSync(Api.UpdateSessionProxyRequestJson)(proxy) }).pipe(tenantScoped(credentials));
752
+ });
510
753
  const promoteSession = Effect.fn("SessionsApi.promoteSession")(function* (credentials, sessionId, input) {
511
754
  return yield* api.promoteSession(sessionId, { payload: {
512
755
  name: input.name,
@@ -515,17 +758,45 @@ const makeSessionsApi = Effect.gen(function* () {
515
758
  tags: input.tags ?? {}
516
759
  } }).pipe(tenantScoped(credentials));
517
760
  });
761
+ const getSessionCursor = Effect.fn("SessionsApi.getSessionCursor")(function* (credentials, sessionId) {
762
+ return yield* api.getSessionCursor(sessionId, void 0).pipe(tenantScoped(credentials));
763
+ });
764
+ const setSessionCursor = Effect.fn("SessionsApi.setSessionCursor")(function* (credentials, sessionId, visible) {
765
+ return yield* api.setSessionCursor(sessionId, { payload: { visible } }).pipe(tenantScoped(credentials));
766
+ });
767
+ const listSessionRecordings = Effect.fn("SessionsApi.listSessionRecordings")(function* (credentials, sessionId) {
768
+ return yield* api.listSessionRecordings(sessionId, void 0).pipe(tenantScoped(credentials));
769
+ });
770
+ const createSessionRecording = Effect.fn("SessionsApi.createSessionRecording")(function* (credentials, sessionId, input) {
771
+ return yield* api.createSessionRecording(sessionId, { payload: input }).pipe(tenantScoped(credentials));
772
+ });
773
+ const getSessionRecording = Effect.fn("SessionsApi.getSessionRecording")(function* (credentials, sessionId, recordingId) {
774
+ return yield* api.getSessionRecording(sessionId, recordingId, void 0).pipe(tenantScoped(credentials));
775
+ });
776
+ const retargetSessionRecording = Effect.fn("SessionsApi.retargetSessionRecording")(function* (credentials, sessionId, recordingId, targetId) {
777
+ return yield* api.retargetSessionRecording(sessionId, recordingId, { payload: { targetId } }).pipe(tenantScoped(credentials));
778
+ });
779
+ const stopSessionRecording = Effect.fn("SessionsApi.stopSessionRecording")(function* (credentials, sessionId, recordingId) {
780
+ return yield* api.stopSessionRecording(sessionId, recordingId, void 0).pipe(tenantScoped(credentials));
781
+ });
782
+ const listSessionFiles = Effect.fn("SessionsApi.listSessionFiles")(function* (credentials, sessionId) {
783
+ return yield* api.listSessionFiles(sessionId, void 0).pipe(tenantScoped(credentials));
784
+ });
785
+ const createSessionFileDownloadURL = Effect.fn("SessionsApi.createSessionFileDownloadURL")(function* (credentials, sessionId, input) {
786
+ return yield* api.createSessionFileDownloadURL(sessionId, { payload: input }).pipe(tenantScoped(credentials));
787
+ });
518
788
  const getBrowserChannels = Effect.fn("SessionsApi.getBrowserChannels")(function* (credentials) {
519
789
  return yield* api.listBrowserChannels(void 0).pipe(tenantScoped(credentials));
520
790
  });
521
791
  const getBrowserStatus = Effect.fn("SessionsApi.getBrowserStatus")(function* (credentials, sessionId, sessionToken) {
522
792
  return yield* http.get(`/sessions/${encodeURIComponent(sessionId)}/browser/status`).pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(BrowserStatus)), authorize({
523
793
  credentials,
524
- bearerToken: sessionToken
794
+ bearerToken: sessionToken,
795
+ tenantHeader: "tenant-scoped"
525
796
  }));
526
797
  });
527
798
  const downloadSessionRecording = Effect.fn("SessionsApi.downloadSessionRecording")(function* (credentials, sessionId, recordingId, sessionToken) {
528
- return yield* http.get(`/sessions/${encodeURIComponent(sessionId)}/recordings/${encodeURIComponent(recordingId)}/content`).pipe(Effect.flatMap((response) => Effect.map(response.arrayBuffer, (body) => ({
799
+ return yield* http.get(recordingContentPath(sessionId, recordingId)).pipe(Effect.flatMap((response) => Effect.map(response.arrayBuffer, (body) => ({
529
800
  blob: new Blob([body], { type: response.headers["content-type"] ?? "" }),
530
801
  filename: contentDispositionFilename(response.headers["content-disposition"])
531
802
  }))), authorize({
@@ -534,9 +805,59 @@ const makeSessionsApi = Effect.gen(function* () {
534
805
  tenantHeader: "tenant-scoped"
535
806
  }));
536
807
  });
808
+ const getSessionThumbnail = Effect.fn("SessionsApi.getSessionThumbnail")(function* (credentials, sessionId, targetId) {
809
+ const session = `/api/sessions/${encodeURIComponent(sessionId)}`;
810
+ const path = targetId === void 0 ? `${session}/thumbnail` : `${session}/targets/${encodeURIComponent(targetId)}/thumbnail`;
811
+ return yield* http.get(path).pipe(Effect.flatMap((response) => Effect.map(response.arrayBuffer, (body) => new Blob([body], { type: "image/jpeg" }))), authorize({
812
+ credentials,
813
+ tenantHeader: "tenant-scoped"
814
+ }));
815
+ });
816
+ const streamSessionRecording = (credentials, sessionId, recordingId, sessionToken) => http.get(recordingContentPath(sessionId, recordingId)).pipe(authorize({
817
+ credentials,
818
+ bearerToken: sessionToken,
819
+ tenantHeader: "tenant-scoped"
820
+ }), Effect.map((response) => response.stream.pipe(Stream.catchTag("HttpClientError", (error) => Stream.fromEffect(toApiRequestError(Effect.fail(error)))))), Stream.unwrap);
821
+ const uploadSessionFiles = Effect.fn("SessionsApi.uploadSessionFiles")(function* (credentials, sessionId, files, options = {}) {
822
+ const body = yield* uploadBody(files);
823
+ return (yield* http.post(`/api/sessions/${encodeURIComponent(sessionId)}/files`, {
824
+ body,
825
+ urlParams: compactQuery({ directory: options.directory })
826
+ }).pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(Api.UploadSessionFiles201)), tenantScoped(credentials))).files;
827
+ });
828
+ const deleteSessionFile = Effect.fn("SessionsApi.deleteSessionFile")(function* (credentials, sessionId, relativePath, options = {}) {
829
+ yield* api.deleteSessionFile(sessionId, { params: compactQuery({
830
+ relativePath,
831
+ recursive: options.recursive
832
+ }) }).pipe(tenantScoped(credentials));
833
+ });
834
+ const createSessionDirectory = Effect.fn("SessionsApi.createSessionDirectory")(function* (credentials, sessionId, relativePath) {
835
+ return yield* api.createSessionDirectory(sessionId, { payload: { relativePath } }).pipe(tenantScoped(credentials));
836
+ });
837
+ const moveSessionFile = Effect.fn("SessionsApi.moveSessionFile")(function* (credentials, sessionId, input) {
838
+ return yield* api.moveSessionFile(sessionId, { payload: input }).pipe(tenantScoped(credentials));
839
+ });
840
+ const uploadLiveSessionFiles = Effect.fn("SessionsApi.uploadLiveSessionFiles")(function* (credentials, sessionId, files, sessionToken) {
841
+ const body = yield* uploadBody(files);
842
+ return (yield* http.post(`/sessions/${encodeURIComponent(sessionId)}/uploads`, { body }).pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(Api.UploadLiveSessionFiles201)), authorize({
843
+ credentials,
844
+ bearerToken: sessionToken,
845
+ tenantHeader: "tenant-scoped"
846
+ }))).files;
847
+ });
848
+ const setSessionViewport = Effect.fn("SessionsApi.setSessionViewport")(function* (credentials, sessionId, input, sessionToken) {
849
+ return yield* api.setSessionViewport(sessionId, { payload: input }).pipe(authorize({
850
+ credentials,
851
+ bearerToken: sessionToken,
852
+ tenantHeader: "tenant-scoped"
853
+ }));
854
+ });
537
855
  return SessionsApi.of({
538
856
  listSessions,
857
+ streamSessions: sessions.stream,
858
+ listAllSessions: sessions.listAll,
539
859
  getSession,
860
+ exportSessionStorageState,
540
861
  getSessionsBulk,
541
862
  createSession,
542
863
  deleteSession,
@@ -545,10 +866,28 @@ const makeSessionsApi = Effect.gen(function* () {
545
866
  rotateSessionToken,
546
867
  rotateCollaborationCapability,
547
868
  replaceSessionTags,
869
+ updateSessionProxy,
548
870
  promoteSession,
871
+ getSessionCursor,
872
+ setSessionCursor,
873
+ listSessionRecordings,
874
+ createSessionRecording,
875
+ getSessionRecording,
876
+ retargetSessionRecording,
877
+ stopSessionRecording,
878
+ listSessionFiles,
879
+ createSessionFileDownloadURL,
549
880
  getBrowserChannels,
550
881
  getBrowserStatus,
551
- downloadSessionRecording
882
+ downloadSessionRecording,
883
+ getSessionThumbnail,
884
+ streamSessionRecording,
885
+ uploadSessionFiles,
886
+ uploadLiveSessionFiles,
887
+ deleteSessionFile,
888
+ moveSessionFile,
889
+ createSessionDirectory,
890
+ setSessionViewport
552
891
  });
553
892
  });
554
893
  const sessionsApiLayer = Layer.effect(SessionsApi, makeSessionsApi);
@@ -573,6 +912,13 @@ const makeSnapshotsApi = Effect.gen(function* () {
573
912
  ...tagQuery(params.tags)
574
913
  }) }).pipe(tenantScoped(credentials));
575
914
  });
915
+ const snapshots = paginated(listSnapshots);
916
+ const getSnapshotByName = Effect.fn("SnapshotsApi.getSnapshotByName")(function* (credentials, name) {
917
+ return yield* snapshots.stream(credentials, {
918
+ name,
919
+ limit: 100
920
+ }).pipe(Stream.filter((candidate) => candidate.name === name), Stream.runHead);
921
+ });
576
922
  const updateSnapshot = Effect.fn("SnapshotsApi.updateSnapshot")(function* (credentials, name, input) {
577
923
  return yield* api.updateSnapshot(name, { payload: input }).pipe(tenantScoped(credentials));
578
924
  });
@@ -587,6 +933,9 @@ const makeSnapshotsApi = Effect.gen(function* () {
587
933
  });
588
934
  return SnapshotsApi.of({
589
935
  listSnapshots,
936
+ streamSnapshots: snapshots.stream,
937
+ listAllSnapshots: snapshots.listAll,
938
+ getSnapshotByName,
590
939
  updateSnapshot,
591
940
  replaceSnapshotTags,
592
941
  deleteSnapshot,
@@ -596,7 +945,7 @@ const makeSnapshotsApi = Effect.gen(function* () {
596
945
  const snapshotsApiLayer = Layer.effect(SnapshotsApi, makeSnapshotsApi);
597
946
  //#endregion
598
947
  //#region src/tenants/service.ts
599
- /** Tenant administration. */
948
+ /** Tenant administration, and the tenant a tenant token belongs to. */
600
949
  var TenantsApi = class extends Context.Service()("@aperture-browser/api-client/TenantsApi") {};
601
950
  //#endregion
602
951
  //#region src/tenants/layer.ts
@@ -609,9 +958,17 @@ const makeTenantsApi = Effect.gen(function* () {
609
958
  limit: params.limit,
610
959
  cursor: params.cursor,
611
960
  includeDeleted: params.includeDeleted || void 0,
612
- deleted: params.deleted
961
+ deleted: params.deleted,
962
+ query: params.query
613
963
  }) }).pipe(authorize(Authorization.of(credentials)));
614
964
  });
965
+ const tenants = paginated(listTenants);
966
+ const getCurrentTenant = Effect.fn("TenantsApi.getCurrentTenant")(function* (credentials) {
967
+ return yield* api.getTenant(void 0).pipe(authorize(Authorization.of(credentials)));
968
+ });
969
+ const updateCurrentTenant = Effect.fn("TenantsApi.updateCurrentTenant")(function* (credentials, input) {
970
+ return yield* api.updateSelectedTenant({ payload: input }).pipe(authorize(Authorization.of(credentials)));
971
+ });
615
972
  const createTenant = Effect.fn("TenantsApi.createTenant")(function* (credentials, input) {
616
973
  return yield* api.createTenant({ payload: input }).pipe(authorize(Authorization.of(credentials)));
617
974
  });
@@ -626,10 +983,14 @@ const makeTenantsApi = Effect.gen(function* () {
626
983
  });
627
984
  return TenantsApi.of({
628
985
  listTenants,
986
+ streamTenants: tenants.stream,
987
+ listAllTenants: tenants.listAll,
629
988
  createTenant,
630
989
  updateTenant,
631
990
  deleteTenant,
632
- restoreTenant
991
+ restoreTenant,
992
+ getCurrentTenant,
993
+ updateCurrentTenant
633
994
  });
634
995
  });
635
996
  const tenantsApiLayer = Layer.effect(TenantsApi, makeTenantsApi);
@@ -654,6 +1015,7 @@ const makeTokensApi = Effect.gen(function* () {
654
1015
  scope: params.scope
655
1016
  }) }).pipe(authorize(Authorization.of(credentials)));
656
1017
  });
1018
+ const adminTokens = paginated(listAdminTokens);
657
1019
  const createAdminToken = Effect.fn("TokensApi.createAdminToken")(function* (credentials, input) {
658
1020
  const systemAdmin = input.authorityType === "system_admin";
659
1021
  return yield* api.createAdminToken({ payload: {
@@ -678,6 +1040,7 @@ const makeTokensApi = Effect.gen(function* () {
678
1040
  scope: params.scope
679
1041
  }) }).pipe(authorize(Authorization.of(credentials)));
680
1042
  });
1043
+ const tenantTokens = paginated(listTenantTokens);
681
1044
  const createTenantToken = Effect.fn("TokensApi.createTenantToken")(function* (credentials, input) {
682
1045
  return yield* api.createTenantToken({ payload: {
683
1046
  name: input.name,
@@ -692,9 +1055,13 @@ const makeTokensApi = Effect.gen(function* () {
692
1055
  });
693
1056
  return TokensApi.of({
694
1057
  listAdminTokens,
1058
+ streamAdminTokens: adminTokens.stream,
1059
+ listAllAdminTokens: adminTokens.listAll,
695
1060
  createAdminToken,
696
1061
  revokeAdminToken,
697
1062
  listTenantTokens,
1063
+ streamTenantTokens: tenantTokens.stream,
1064
+ listAllTenantTokens: tenantTokens.listAll,
698
1065
  createTenantToken,
699
1066
  revokeTenantToken
700
1067
  });
@@ -718,6 +1085,7 @@ const makeUsersApi = Effect.gen(function* () {
718
1085
  disabled: params.disabled
719
1086
  }) }).pipe(authorize(Authorization.of(credentials)));
720
1087
  });
1088
+ const users = paginated(listUsers);
721
1089
  const createUser = Effect.fn("UsersApi.createUser")(function* (credentials, input) {
722
1090
  return yield* api.createUser({ payload: input }).pipe(authorize(Authorization.of(credentials)));
723
1091
  });
@@ -739,6 +1107,9 @@ const makeUsersApi = Effect.gen(function* () {
739
1107
  const listUserMemberships = Effect.fn("UsersApi.listUserMemberships")(function* (credentials, userId) {
740
1108
  return yield* api.listUserMemberships(userId, void 0).pipe(authorize(Authorization.of(credentials)));
741
1109
  });
1110
+ const listTenantMemberships = Effect.fn("UsersApi.listTenantMemberships")(function* (credentials, tenantId) {
1111
+ return yield* api.listTenantMemberships(tenantId, void 0).pipe(authorize(Authorization.of(credentials)));
1112
+ });
742
1113
  const upsertTenantMembership = Effect.fn("UsersApi.upsertTenantMembership")(function* (credentials, tenantId, userId, scopes) {
743
1114
  const payload = { scopes };
744
1115
  return yield* api.upsertTenantMembership(tenantId, userId, { payload }).pipe(authorize(Authorization.of(credentials)));
@@ -748,6 +1119,8 @@ const makeUsersApi = Effect.gen(function* () {
748
1119
  });
749
1120
  return UsersApi.of({
750
1121
  listUsers,
1122
+ streamUsers: users.stream,
1123
+ listAllUsers: users.listAll,
751
1124
  createUser,
752
1125
  getUser,
753
1126
  updateUser,
@@ -755,6 +1128,7 @@ const makeUsersApi = Effect.gen(function* () {
755
1128
  disableUser,
756
1129
  restoreUser,
757
1130
  listUserMemberships,
1131
+ listTenantMemberships,
758
1132
  upsertTenantMembership,
759
1133
  deleteTenantMembership
760
1134
  });
@@ -766,22 +1140,104 @@ const usersApiLayer = Layer.effect(UsersApi, makeUsersApi);
766
1140
  * Every API service, sharing one ApiAuthorization. The services send requests through the
767
1141
  * HttpClient the app provides, decorated with each call's authorization.
768
1142
  */
769
- const apiClientLayer = Layer.mergeAll(authApiLayer, tenantsApiLayer, usersApiLayer, sessionsApiLayer, snapshotsApiLayer, tokensApiLayer, eventsApiLayer).pipe(Layer.provide(authorizedHttpClientLayer), Layer.provideMerge(apiAuthorizationLayer));
1143
+ const apiClientLayer = Layer.mergeAll(authApiLayer, tenantsApiLayer, usersApiLayer, sessionsApiLayer, snapshotsApiLayer, tokensApiLayer, eventsApiLayer, healthApiLayer).pipe(Layer.provide(authorizedHttpClientLayer), Layer.provideMerge(apiAuthorizationLayer));
770
1144
  const baseUrlLayer = (baseUrl) => Layer.effect(HttpClient.HttpClient, Effect.map(HttpClient.HttpClient, HttpClient.mapRequest(HttpClientRequest.prependUrl(baseUrl))));
771
1145
  //#endregion
772
- //#region src/pagination.ts
773
- function getNextPageParam(page) {
774
- return page.meta.hasMore ? page.meta.nextCursor : void 0;
775
- }
776
- function flattenInfinitePages(pages) {
777
- return pages?.flatMap((page) => page.data) ?? [];
1146
+ //#region src/client.ts
1147
+ /** Binds `credentials` as the first argument of every method of `service`. */
1148
+ function withCredentials(service, credentials) {
1149
+ const methods = Object.entries(service);
1150
+ return Object.fromEntries(methods.map(([name, method]) => [name, (...args) => method(credentials, ...args)]));
778
1151
  }
779
- const defaultListLimit = 50;
780
- const listQueryDefaults = { refetchOnWindowFocus: true };
1152
+ /**
1153
+ * The credentialed API services with one set of credentials bound, for callers that always
1154
+ * act as the same principal, such as a server holding one API token.
1155
+ */
1156
+ var ApertureClient = class extends Context.Service()("@aperture-browser/api-client/ApertureClient") {};
1157
+ const makeApertureClient = Effect.fn("makeApertureClient")(function* (credentials) {
1158
+ const auth = yield* AuthApi;
1159
+ return ApertureClient.of({
1160
+ sessions: withCredentials(yield* SessionsApi, credentials),
1161
+ snapshots: withCredentials(yield* SnapshotsApi, credentials),
1162
+ tenants: withCredentials(yield* TenantsApi, credentials),
1163
+ users: withCredentials(yield* UsersApi, credentials),
1164
+ tokens: withCredentials(yield* TokensApi, credentials),
1165
+ events: withCredentials(yield* EventsApi, credentials),
1166
+ auth: { getAuthMe: () => auth.getAuthMe(credentials.selectedTenantId, credentials) },
1167
+ health: yield* HealthApi
1168
+ });
1169
+ });
1170
+ /** ApertureClient acting with `credentials`, over the HttpClient the app provides. */
1171
+ const apertureClientLayer = (credentials) => Layer.effect(ApertureClient, makeApertureClient(credentials)).pipe(Layer.provide(apiClientLayer));
781
1172
  //#endregion
782
1173
  //#region src/schemas.ts
783
1174
  const SessionStatus = Api.SessionStatus;
784
1175
  //#endregion
785
- export { ApiAuthorization, ApiErrorBody, ApiRequestError, AuthApi, Authorization, BrowserStatus, EventsApi, LoginMethods, Passkey, PasskeyLoginOptions, PasskeyMutation, PasskeyRegistrationOptions, Passkeys, PasswordLoginResponse, Recording, RecoveryCodes, SecurityStatus, SessionStatus, SessionsApi, SnapshotsApi, TENANT_HEADER, TOTPEnrollment, TenantsApi, TokensApi, UsersApi, apiAuthorizationLayer, apiClientLayer, authApiLayer, authorizedHttpClientLayer, baseUrlLayer, compactQuery, defaultListLimit, eventsApiLayer, flattenInfinitePages, getNextPageParam, listQueryDefaults, makeApiAuthorization, makeAuthApi, makeEventsApi, makeSessionsApi, makeSnapshotsApi, makeTenantsApi, makeTokensApi, makeUsersApi, resolveTenantHeader, sessionsApiLayer, snapshotsApiLayer, tagQuery, tenantsApiLayer, toApiRequestError, tokensApiLayer, usersApiLayer, webSessionCredentials };
1176
+ //#region src/sessions/tree.ts
1177
+ /**
1178
+ * The top-level directories the session writes into. They always exist and cannot be
1179
+ * deleted, moved or renamed.
1180
+ */
1181
+ const sessionFileRootDirectories = [
1182
+ "downloads",
1183
+ "recordings",
1184
+ "uploads",
1185
+ "outputs"
1186
+ ];
1187
+ const emptyDirectory = () => ({
1188
+ directory: void 0,
1189
+ directories: /* @__PURE__ */ new Map(),
1190
+ files: []
1191
+ });
1192
+ function directoryAt(root, segments) {
1193
+ let directory = root;
1194
+ for (const segment of segments) {
1195
+ let child = directory.directories.get(segment);
1196
+ if (child === void 0) {
1197
+ child = emptyDirectory();
1198
+ directory.directories.set(segment, child);
1199
+ }
1200
+ directory = child;
1201
+ }
1202
+ return directory;
1203
+ }
1204
+ function toNodes(directory, parentPath) {
1205
+ const directories = [...directory.directories.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([name, child]) => {
1206
+ const relativePath = parentPath === "" ? name : `${parentPath}/${name}`;
1207
+ return {
1208
+ kind: "directory",
1209
+ name,
1210
+ relativePath,
1211
+ directory: child.directory,
1212
+ children: toNodes(child, relativePath)
1213
+ };
1214
+ });
1215
+ const files = [...directory.files].sort((left, right) => left.name.localeCompare(right.name));
1216
+ return [...directories, ...files];
1217
+ }
1218
+ /**
1219
+ * Arranges `listSessionFiles` results into a tree: directories first, then files, each
1220
+ * sorted by name. Empty directories appear because the listing includes them.
1221
+ */
1222
+ function sessionFileTree(entries) {
1223
+ const root = emptyDirectory();
1224
+ for (const entry of entries) {
1225
+ const segments = entry.relativePath.split("/");
1226
+ if (entry.type === "directory") {
1227
+ directoryAt(root, segments).directory = entry;
1228
+ continue;
1229
+ }
1230
+ const name = segments.pop() ?? entry.relativePath;
1231
+ directoryAt(root, segments).files.push({
1232
+ kind: "file",
1233
+ name,
1234
+ relativePath: entry.relativePath,
1235
+ file: entry
1236
+ });
1237
+ }
1238
+ return toNodes(root, "");
1239
+ }
1240
+ //#endregion
1241
+ export { ApertureClient, ApiAuthorization, ApiErrorBody, ApiRequestError, AuthApi, Authorization, BrowserPage, BrowserStatus, EventsApi, HealthApi, LiveSessionErrorBody, LoginMethods, OAuthAuthorizationRequest, OAuthClient, OAuthGrant, OAuthGrants, OAuthRedirect, Passkey, PasskeyLoginOptions, PasskeyMutation, PasskeyRegistrationOptions, Passkeys, PasswordLoginResponse, Recording, RecoveryCodes, SecurityStatus, SessionStatus, SessionsApi, SnapshotsApi, TENANT_HEADER, TOTPEnrollment, TenantsApi, TokensApi, UsersApi, apertureClientLayer, apiAuthorizationLayer, apiClientLayer, authApiLayer, authorizedHttpClientLayer, baseUrlLayer, compactQuery, contentDispositionFilename, defaultListLimit, eventsApiLayer, flattenInfinitePages, getNextPageParam, healthApiLayer, listQueryDefaults, makeApertureClient, makeApiAuthorization, makeAuthApi, makeEventsApi, makeHealthApi, makeSessionsApi, makeSnapshotsApi, makeTenantsApi, makeTokensApi, makeUsersApi, paginated, resolveTenantHeader, sessionFileRootDirectories, sessionFileTree, sessionsApiLayer, snapshotsApiLayer, tagQuery, tenantsApiLayer, toApiRequestError, tokensApiLayer, usersApiLayer, webSessionCredentials, withCredentials };
786
1242
 
787
1243
  //# sourceMappingURL=index.js.map