@opengeni/api-router 3.3.0 → 3.4.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/access-grant-rls.d.ts +15 -0
- package/dist/app.js +1 -1
- package/dist/billing-stripe-customer.d.ts +18 -0
- package/dist/billing-subscription-store.d.ts +34 -0
- package/dist/billing-subscriptions.d.ts +64 -0
- package/dist/{chunk-6CECHDZP.js → chunk-6DZ4H5JQ.js} +2684 -1410
- package/dist/{chunk-6CECHDZP.js.map → chunk-6DZ4H5JQ.js.map} +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/marketplace-plugin.d.ts +43 -43
- package/dist/mcp/scheduled-task-view.d.ts +4 -4
- package/dist/mcp/server.d.ts +11 -1
- package/dist/routes/browser-sessions.d.ts +23 -2
- package/dist/routes/sessions.d.ts +1 -0
- package/package.json +24 -24
- package/src/access-grant-rls.ts +98 -10
- package/src/app.ts +105 -43
- package/src/billing-stripe-customer.ts +40 -0
- package/src/billing-subscription-store.ts +121 -0
- package/src/billing-subscriptions.ts +759 -0
- package/src/index.ts +3 -0
- package/src/mcp/action-catalog.gen.ts +113 -0
- package/src/mcp/action-schema-imports.gen.ts +5 -1
- package/src/mcp/server.ts +28 -0
- package/src/routes/billing.ts +161 -21
- package/src/routes/browser-sessions.ts +359 -9
- package/src/routes/organization-memberships.ts +12 -0
- package/src/routes/sessions.ts +8 -1
|
@@ -40,8 +40,12 @@ import {
|
|
|
40
40
|
BrowserSessionLifecycleRequest,
|
|
41
41
|
BrowserSessionListResponse,
|
|
42
42
|
BrowserSessionMutationResponse,
|
|
43
|
+
type BrowserTarget,
|
|
43
44
|
BrowserTargetListResponse,
|
|
44
45
|
BrowserTargetState,
|
|
46
|
+
BrowserWorkingCopyAdoptionRequest,
|
|
47
|
+
BrowserWorkingCopyAdoptionResponse,
|
|
48
|
+
BrowserWorkingCopyInspection,
|
|
45
49
|
CreateBrowserSessionRequest,
|
|
46
50
|
ExternalAuthInteractiveRequest,
|
|
47
51
|
ExternalAuthInteractiveResponse,
|
|
@@ -57,6 +61,8 @@ import {
|
|
|
57
61
|
type BrowserActionRequest as BrowserActionRequestValue,
|
|
58
62
|
type BrowserDownload as BrowserDownloadValue,
|
|
59
63
|
type BrowserSession as BrowserSessionValue,
|
|
64
|
+
type BrowserWorkingCopyAdoption as BrowserWorkingCopyAdoptionValue,
|
|
65
|
+
type BrowserWorkingCopyRefusal as BrowserWorkingCopyRefusalValue,
|
|
60
66
|
type FileAsset,
|
|
61
67
|
type CreateBrowserSessionRequest as CreateBrowserSessionRequestValue,
|
|
62
68
|
type InteractionPlacement,
|
|
@@ -143,6 +149,7 @@ import {
|
|
|
143
149
|
verifyAuthRun,
|
|
144
150
|
settleBrowserDownloadSaveFailure,
|
|
145
151
|
terminalizeStaleConnectedInteractionPlacement,
|
|
152
|
+
recordAuditEvent,
|
|
146
153
|
type BrowserSessionControlRecord,
|
|
147
154
|
type BrowserPrivateCheckpointAuthority,
|
|
148
155
|
type BrowserRevisionArtifactAuthority,
|
|
@@ -182,6 +189,9 @@ import {
|
|
|
182
189
|
type BrowserControlScreenshotOptions,
|
|
183
190
|
type BrowserControlFrame,
|
|
184
191
|
type PlacementBrowserStateCaptureReceipt,
|
|
192
|
+
type PlacementBrowserWorkingCopyAdoption,
|
|
193
|
+
type PlacementBrowserWorkingCopyAuthority,
|
|
194
|
+
type PlacementBrowserWorkingCopyInspection,
|
|
185
195
|
type PlacementBrowserNetworkRoute,
|
|
186
196
|
type PlacementBrowserTransport,
|
|
187
197
|
type RestorePlacementBrowserStateInput,
|
|
@@ -352,22 +362,36 @@ export function registerBrowserSessionRoutes(
|
|
|
352
362
|
const request = await parseJsonBody(context, CreateBrowserSessionRequest);
|
|
353
363
|
const startedAtMs = performance.now();
|
|
354
364
|
const stages = new InteractionRouteStages("create");
|
|
355
|
-
|
|
365
|
+
// The replay lookup and the source session are read alongside the
|
|
366
|
+
// authorization check. A denial still wins, and neither read is used
|
|
367
|
+
// until the request is authorized; each failure surfaces where it did.
|
|
368
|
+
const [authorized, existingRead, sourceRead] = await Promise.allSettled([
|
|
369
|
+
authorizeSourceSession(deps, grant, request.sessionId, "session.control"),
|
|
370
|
+
findBrowserSessionControlRecordByOperation(deps.db, {
|
|
371
|
+
accountId: grant.accountId,
|
|
372
|
+
workspaceId,
|
|
373
|
+
operationId: request.operationId,
|
|
374
|
+
}),
|
|
375
|
+
requireSourceSession(deps, workspaceId, request.sessionId),
|
|
376
|
+
]);
|
|
377
|
+
if (authorized.status === "rejected") throw authorized.reason;
|
|
356
378
|
stages.mark("authorize");
|
|
357
379
|
const origin = requestOrigin(context, deps.settings);
|
|
358
380
|
const authority = browserAuthorityRoot(deps);
|
|
359
381
|
let prepared: Awaited<ReturnType<typeof prepareBrowserSessionCreate>> | null = null;
|
|
382
|
+
// What the controller showed when it launched this browser, returned so
|
|
383
|
+
// the caller can present the opened tab without two more reads.
|
|
384
|
+
const launch: { result: Awaited<ReturnType<BrowserControlClient["createSession"]>> | null } = {
|
|
385
|
+
result: null,
|
|
386
|
+
};
|
|
360
387
|
|
|
361
388
|
try {
|
|
362
389
|
assertEphemeralBrowserCreateEnabled(
|
|
363
390
|
request,
|
|
364
391
|
deps.settings.experimentalBrowserContextPoolEnabled,
|
|
365
392
|
);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
workspaceId,
|
|
369
|
-
operationId: request.operationId,
|
|
370
|
-
});
|
|
393
|
+
if (existingRead.status === "rejected") throw existingRead.reason;
|
|
394
|
+
const existing = existingRead.value;
|
|
371
395
|
if (existing && existing.sourceSessionId !== request.sessionId) {
|
|
372
396
|
throw new BrowserSessionOperationConflictError(
|
|
373
397
|
"BrowserSession create operation belongs to another source session",
|
|
@@ -401,7 +425,8 @@ export function registerBrowserSessionRoutes(
|
|
|
401
425
|
}
|
|
402
426
|
}
|
|
403
427
|
|
|
404
|
-
|
|
428
|
+
if (sourceRead.status === "rejected") throw sourceRead.reason;
|
|
429
|
+
const sourceSession = sourceRead.value;
|
|
405
430
|
stages.mark("source_session");
|
|
406
431
|
const response = await withBrowserPlacement(
|
|
407
432
|
sourceSession,
|
|
@@ -536,7 +561,7 @@ export function registerBrowserSessionRoutes(
|
|
|
536
561
|
linkedComputer !== null,
|
|
537
562
|
);
|
|
538
563
|
stages.mark("display");
|
|
539
|
-
|
|
564
|
+
launch.result = await client.createSession({
|
|
540
565
|
browserSessionId: preparedSession.id,
|
|
541
566
|
controllerGeneration,
|
|
542
567
|
tokenGeneration: record.tokenGeneration,
|
|
@@ -553,6 +578,7 @@ export function registerBrowserSessionRoutes(
|
|
|
553
578
|
...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
|
|
554
579
|
...(restore ? { restore } : {}),
|
|
555
580
|
});
|
|
581
|
+
return launch.result;
|
|
556
582
|
},
|
|
557
583
|
);
|
|
558
584
|
},
|
|
@@ -614,7 +640,7 @@ export function registerBrowserSessionRoutes(
|
|
|
614
640
|
}
|
|
615
641
|
},
|
|
616
642
|
);
|
|
617
|
-
const parsed = BrowserSessionMutationResponse.parse(response);
|
|
643
|
+
const parsed = BrowserSessionMutationResponse.parse(withInitialView(response, launch.result));
|
|
618
644
|
observeLifecycleResult(deps.observability, startedAtMs, parsed);
|
|
619
645
|
reportInteractionRouteStages(context, deps.observability, stages);
|
|
620
646
|
return context.json(
|
|
@@ -2612,6 +2638,217 @@ export function registerBrowserSessionRoutes(
|
|
|
2612
2638
|
},
|
|
2613
2639
|
);
|
|
2614
2640
|
|
|
2641
|
+
/** Run one working-copy request against an active browser's own controller
|
|
2642
|
+
* generation, with the same session.control and placement authority as any
|
|
2643
|
+
* browser control. The controller binds every request to the exact
|
|
2644
|
+
* controller authority the copy was launched under. Never replayed. */
|
|
2645
|
+
async function withPreservedWorkingCopy<T>(
|
|
2646
|
+
context: Context,
|
|
2647
|
+
callback: (input: {
|
|
2648
|
+
grant: AccessGrant;
|
|
2649
|
+
workspaceId: string;
|
|
2650
|
+
browserSessionId: string;
|
|
2651
|
+
client: BrowserControlClient;
|
|
2652
|
+
authority: PlacementBrowserWorkingCopyAuthority;
|
|
2653
|
+
}) => Promise<T>,
|
|
2654
|
+
): Promise<T> {
|
|
2655
|
+
const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
|
|
2656
|
+
context,
|
|
2657
|
+
"sessions:control",
|
|
2658
|
+
);
|
|
2659
|
+
return await withActiveBrowserController(
|
|
2660
|
+
context,
|
|
2661
|
+
grant,
|
|
2662
|
+
workspaceId,
|
|
2663
|
+
browserSessionId,
|
|
2664
|
+
"session.control",
|
|
2665
|
+
"browser.control",
|
|
2666
|
+
async ({ client, record, binding, placement }) => {
|
|
2667
|
+
const transport = browserRuntimeTransport(record.session, placement.transport);
|
|
2668
|
+
if (
|
|
2669
|
+
transport.kind !== "managed" ||
|
|
2670
|
+
transport.engine !== "chromium" ||
|
|
2671
|
+
transport.ephemeralPartition ||
|
|
2672
|
+
browserSessionStorageMode(record.session) !== "private_profile"
|
|
2673
|
+
) {
|
|
2674
|
+
throw new BrowserSessionStateError(WORKING_COPY_REFUSAL_EXPLANATIONS.unsupported_browser);
|
|
2675
|
+
}
|
|
2676
|
+
const tokens = deriveBrowserSessionControllerTokens({
|
|
2677
|
+
rootSecret: browserAuthorityRoot(deps),
|
|
2678
|
+
accountId: grant.accountId,
|
|
2679
|
+
workspaceId,
|
|
2680
|
+
placement: record.session.placement,
|
|
2681
|
+
placementInstanceId: placement.placementInstanceId,
|
|
2682
|
+
browserSessionId,
|
|
2683
|
+
controllerGeneration: binding.controllerGeneration,
|
|
2684
|
+
tokenGeneration: record.tokenGeneration,
|
|
2685
|
+
});
|
|
2686
|
+
return await callback({
|
|
2687
|
+
grant,
|
|
2688
|
+
workspaceId,
|
|
2689
|
+
browserSessionId,
|
|
2690
|
+
client,
|
|
2691
|
+
authority: {
|
|
2692
|
+
browserSessionId,
|
|
2693
|
+
controllerGeneration: binding.controllerGeneration,
|
|
2694
|
+
tokenGeneration: record.tokenGeneration,
|
|
2695
|
+
...tokens,
|
|
2696
|
+
},
|
|
2697
|
+
});
|
|
2698
|
+
},
|
|
2699
|
+
// A missing controller session is the state being inspected; never
|
|
2700
|
+
// launch anything to "recover" it here.
|
|
2701
|
+
false,
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
app.post(
|
|
2706
|
+
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/working-copy/inspection",
|
|
2707
|
+
async (context) => {
|
|
2708
|
+
await parseEmptyJsonBody(context);
|
|
2709
|
+
const result = await withPreservedWorkingCopy(
|
|
2710
|
+
context,
|
|
2711
|
+
async ({ grant, workspaceId, browserSessionId, client, authority }) => {
|
|
2712
|
+
const inspection = await client.inspectWorkingCopy(authority);
|
|
2713
|
+
const checkpoint = await getBrowserPrivateCheckpointAuthority(deps.db, {
|
|
2714
|
+
accountId: grant.accountId,
|
|
2715
|
+
workspaceId,
|
|
2716
|
+
browserSessionId,
|
|
2717
|
+
}).catch(() => null);
|
|
2718
|
+
return BrowserWorkingCopyInspection.parse({
|
|
2719
|
+
browserSessionId,
|
|
2720
|
+
controllerGeneration: authority.controllerGeneration,
|
|
2721
|
+
adoptable: inspection.refusal === null && inspection.manifest !== null,
|
|
2722
|
+
refusal: inspection.refusal,
|
|
2723
|
+
explanation: workingCopyInspectionExplanation(
|
|
2724
|
+
inspection,
|
|
2725
|
+
checkpoint?.createdAt ?? null,
|
|
2726
|
+
),
|
|
2727
|
+
manifest: inspection.manifest,
|
|
2728
|
+
platformSupported: inspection.platformSupported,
|
|
2729
|
+
profileIdentityMatches: inspection.profileIdentityMatches,
|
|
2730
|
+
profileInUse: inspection.profileInUse,
|
|
2731
|
+
receipt: inspection.receipt,
|
|
2732
|
+
freshness: { status: "unknown", latestCheckpointAt: checkpoint?.createdAt ?? null },
|
|
2733
|
+
activeAdoption: inspection.activeAdoption,
|
|
2734
|
+
});
|
|
2735
|
+
},
|
|
2736
|
+
);
|
|
2737
|
+
return context.json(result);
|
|
2738
|
+
},
|
|
2739
|
+
);
|
|
2740
|
+
|
|
2741
|
+
app.post(
|
|
2742
|
+
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/working-copy/adoptions",
|
|
2743
|
+
async (context) => {
|
|
2744
|
+
const request = await parseJsonBody(context, BrowserWorkingCopyAdoptionRequest);
|
|
2745
|
+
const result = await withPreservedWorkingCopy(
|
|
2746
|
+
context,
|
|
2747
|
+
async ({ grant, workspaceId, browserSessionId, client, authority }) => {
|
|
2748
|
+
// Durable attribution before any effect: who selected which exact
|
|
2749
|
+
// bytes as this browser's state. Freshness stays unproven.
|
|
2750
|
+
await recordAuditEvent(deps.db, {
|
|
2751
|
+
accountId: grant.accountId,
|
|
2752
|
+
workspaceId,
|
|
2753
|
+
subjectId: grant.subjectId,
|
|
2754
|
+
action: "browserSession.working_copy.adoption_requested",
|
|
2755
|
+
targetType: "browser_session",
|
|
2756
|
+
targetId: browserSessionId,
|
|
2757
|
+
metadata: {
|
|
2758
|
+
operationId: request.operationId,
|
|
2759
|
+
manifestDigest: request.manifestDigest,
|
|
2760
|
+
controllerGeneration: authority.controllerGeneration,
|
|
2761
|
+
},
|
|
2762
|
+
});
|
|
2763
|
+
const adoption = await client.adoptWorkingCopy({
|
|
2764
|
+
...authority,
|
|
2765
|
+
operationId: request.operationId,
|
|
2766
|
+
manifestDigest: request.manifestDigest,
|
|
2767
|
+
});
|
|
2768
|
+
await recordAuditEvent(deps.db, {
|
|
2769
|
+
accountId: grant.accountId,
|
|
2770
|
+
workspaceId,
|
|
2771
|
+
subjectId: grant.subjectId,
|
|
2772
|
+
action: "browserSession.working_copy.adoption_settled",
|
|
2773
|
+
targetType: "browser_session",
|
|
2774
|
+
targetId: browserSessionId,
|
|
2775
|
+
metadata: {
|
|
2776
|
+
operationId: adoption.operationId,
|
|
2777
|
+
manifestDigest: adoption.manifestDigest,
|
|
2778
|
+
controllerGeneration: adoption.controllerGeneration,
|
|
2779
|
+
phase: adoption.phase,
|
|
2780
|
+
failureCode: adoption.failureCode,
|
|
2781
|
+
},
|
|
2782
|
+
}).catch(() => undefined);
|
|
2783
|
+
return BrowserWorkingCopyAdoptionResponse.parse({
|
|
2784
|
+
inFlight: false,
|
|
2785
|
+
adoption: publicWorkingCopyAdoption(adoption),
|
|
2786
|
+
explanation: workingCopyAdoptionExplanation(false, adoption),
|
|
2787
|
+
});
|
|
2788
|
+
},
|
|
2789
|
+
);
|
|
2790
|
+
return context.json(result);
|
|
2791
|
+
},
|
|
2792
|
+
);
|
|
2793
|
+
|
|
2794
|
+
app.get(
|
|
2795
|
+
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/working-copy/adoptions/:operationId",
|
|
2796
|
+
async (context) => {
|
|
2797
|
+
const operationId = requireUuidParam(context, "operationId");
|
|
2798
|
+
const result = await withPreservedWorkingCopy(context, async ({ client, authority }) => {
|
|
2799
|
+
const status = await client.workingCopyAdoption(authority, operationId);
|
|
2800
|
+
return BrowserWorkingCopyAdoptionResponse.parse({
|
|
2801
|
+
inFlight: status.inFlight,
|
|
2802
|
+
adoption: status.adoption ? publicWorkingCopyAdoption(status.adoption) : null,
|
|
2803
|
+
explanation: workingCopyAdoptionExplanation(status.inFlight, status.adoption),
|
|
2804
|
+
});
|
|
2805
|
+
});
|
|
2806
|
+
return context.json(result);
|
|
2807
|
+
},
|
|
2808
|
+
);
|
|
2809
|
+
|
|
2810
|
+
app.post(
|
|
2811
|
+
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/working-copy/adoptions/:operationId/revert",
|
|
2812
|
+
async (context) => {
|
|
2813
|
+
const operationId = requireUuidParam(context, "operationId");
|
|
2814
|
+
const result = await withPreservedWorkingCopy(
|
|
2815
|
+
context,
|
|
2816
|
+
async ({ grant, workspaceId, browserSessionId, client, authority }) => {
|
|
2817
|
+
await recordAuditEvent(deps.db, {
|
|
2818
|
+
accountId: grant.accountId,
|
|
2819
|
+
workspaceId,
|
|
2820
|
+
subjectId: grant.subjectId,
|
|
2821
|
+
action: "browserSession.working_copy.revert_requested",
|
|
2822
|
+
targetType: "browser_session",
|
|
2823
|
+
targetId: browserSessionId,
|
|
2824
|
+
metadata: { operationId, controllerGeneration: authority.controllerGeneration },
|
|
2825
|
+
});
|
|
2826
|
+
const status = await client.revertWorkingCopyAdoption({ ...authority, operationId });
|
|
2827
|
+
await recordAuditEvent(deps.db, {
|
|
2828
|
+
accountId: grant.accountId,
|
|
2829
|
+
workspaceId,
|
|
2830
|
+
subjectId: grant.subjectId,
|
|
2831
|
+
action: "browserSession.working_copy.revert_settled",
|
|
2832
|
+
targetType: "browser_session",
|
|
2833
|
+
targetId: browserSessionId,
|
|
2834
|
+
metadata: {
|
|
2835
|
+
operationId,
|
|
2836
|
+
controllerGeneration: authority.controllerGeneration,
|
|
2837
|
+
phase: status.adoption?.phase ?? null,
|
|
2838
|
+
failureCode: status.adoption?.failureCode ?? null,
|
|
2839
|
+
},
|
|
2840
|
+
}).catch(() => undefined);
|
|
2841
|
+
return BrowserWorkingCopyAdoptionResponse.parse({
|
|
2842
|
+
inFlight: status.inFlight,
|
|
2843
|
+
adoption: status.adoption ? publicWorkingCopyAdoption(status.adoption) : null,
|
|
2844
|
+
explanation: workingCopyAdoptionExplanation(status.inFlight, status.adoption),
|
|
2845
|
+
});
|
|
2846
|
+
},
|
|
2847
|
+
);
|
|
2848
|
+
return context.json(result);
|
|
2849
|
+
},
|
|
2850
|
+
);
|
|
2851
|
+
|
|
2615
2852
|
app.post(
|
|
2616
2853
|
"/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/end",
|
|
2617
2854
|
async (context) => {
|
|
@@ -4457,6 +4694,34 @@ function requireOperationGeneration(record: BrowserSessionControlRecord): string
|
|
|
4457
4694
|
return generation;
|
|
4458
4695
|
}
|
|
4459
4696
|
|
|
4697
|
+
/** Attach the controller's launch view to a create that activated exactly that
|
|
4698
|
+
* launch in this request. Replays, failures and other bindings get nothing, so
|
|
4699
|
+
* a caller then reads the targets and observes as before. */
|
|
4700
|
+
export function withInitialView<
|
|
4701
|
+
T extends {
|
|
4702
|
+
session: { id: string; lifecycle: string; controller: { controllerGeneration: string } | null };
|
|
4703
|
+
operation: { state: string; replayed: boolean };
|
|
4704
|
+
},
|
|
4705
|
+
>(
|
|
4706
|
+
response: T,
|
|
4707
|
+
launched: Awaited<ReturnType<BrowserControlClient["createSession"]>> | null,
|
|
4708
|
+
): T | (T & { initialView: { targets: BrowserTarget[]; observation: BrowserObservation } }) {
|
|
4709
|
+
if (
|
|
4710
|
+
!launched?.targets ||
|
|
4711
|
+
response.operation.state !== "completed" ||
|
|
4712
|
+
response.operation.replayed ||
|
|
4713
|
+
response.session.lifecycle !== "active" ||
|
|
4714
|
+
response.session.id !== launched.browserSessionId ||
|
|
4715
|
+
response.session.controller?.controllerGeneration !== launched.controllerGeneration
|
|
4716
|
+
) {
|
|
4717
|
+
return response;
|
|
4718
|
+
}
|
|
4719
|
+
return {
|
|
4720
|
+
...response,
|
|
4721
|
+
initialView: { targets: launched.targets, observation: launched.observation },
|
|
4722
|
+
};
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4460
4725
|
async function provisionController(
|
|
4461
4726
|
deps: ApiRouteDeps,
|
|
4462
4727
|
grant: AccessGrant,
|
|
@@ -5376,6 +5641,91 @@ function recordAttachedBrowserUnavailable(
|
|
|
5376
5641
|
}
|
|
5377
5642
|
}
|
|
5378
5643
|
|
|
5644
|
+
const WORKING_COPY_REFUSAL_EXPLANATIONS: Record<BrowserWorkingCopyRefusalValue, string> = {
|
|
5645
|
+
unsupported_platform:
|
|
5646
|
+
"Adopting a working copy is supported only for browsers on Linux machines, where the controller can prove no process uses the copy. Nothing was changed.",
|
|
5647
|
+
unsupported_browser:
|
|
5648
|
+
"Only a managed Chromium browser with a private profile keeps a working copy that can be adopted. Nothing was changed.",
|
|
5649
|
+
browser_running:
|
|
5650
|
+
"The browser is running, starting or being ended on its controller, so there is no stopped working copy to adopt now. If it is running, use it normally; otherwise inspect again once it has stopped.",
|
|
5651
|
+
working_copy_invalid:
|
|
5652
|
+
"The browser's working directory on its machine is missing, incomplete or not a plain directory, so it cannot be adopted. Nothing was changed.",
|
|
5653
|
+
receipt_not_adoptable:
|
|
5654
|
+
"The working copy has no completed record of a browser launch, so it cannot be adopted. Its record and recovery state are preserved.",
|
|
5655
|
+
authority_changed:
|
|
5656
|
+
"The working copy was launched under a different controller authority than this browser now has, so it cannot be adopted for it. Nothing was changed.",
|
|
5657
|
+
process_state_unknown:
|
|
5658
|
+
"It cannot be proven that no process uses the copy's browser profile. Close any browser using it, then inspect again. Nothing was changed.",
|
|
5659
|
+
adoption_not_needed:
|
|
5660
|
+
"The working copy is the same directory the browser was launched in, so it needs no adoption; the browser continues from it when it is next used.",
|
|
5661
|
+
adoption_unresolved:
|
|
5662
|
+
"An earlier adoption for this browser has not finished. Check it with getBrowserSessionWorkingCopyAdoption and its operation id (see activeAdoption). Inspecting and checking never change it; retrying that same adoption with the same operationId and manifestDigest settles it. Do not start another.",
|
|
5663
|
+
};
|
|
5664
|
+
|
|
5665
|
+
function workingCopyInspectionExplanation(
|
|
5666
|
+
inspection: PlacementBrowserWorkingCopyInspection,
|
|
5667
|
+
latestCheckpointAt: string | null,
|
|
5668
|
+
): string {
|
|
5669
|
+
if (inspection.refusal) return WORKING_COPY_REFUSAL_EXPLANATIONS[inspection.refusal];
|
|
5670
|
+
const manifest = inspection.manifest!;
|
|
5671
|
+
return [
|
|
5672
|
+
`This working copy can be adopted: ${manifest.entries} entries, ${manifest.bytes} bytes, manifest digest ${manifest.digest}.`,
|
|
5673
|
+
manifest.excluded.length > 0
|
|
5674
|
+
? `Left out because the controller rebuilds them: ${manifest.excluded.join(", ")}. Everything else is in the digest.`
|
|
5675
|
+
: "Every entry is in the digest.",
|
|
5676
|
+
"It is not the directory the browser was launched in (it was moved, copied or restored), so the browser refuses to continue from it until it is adopted.",
|
|
5677
|
+
latestCheckpointAt
|
|
5678
|
+
? `How recent its contents are is not proven; the browser's latest saved checkpoint is from ${latestCheckpointAt}.`
|
|
5679
|
+
: "How recent its contents are is not proven.",
|
|
5680
|
+
"Adopting selects exactly these bytes as the browser's state and is never replaced by an older checkpoint afterwards; the directory it replaces is kept.",
|
|
5681
|
+
"To adopt, call adoptBrowserSessionWorkingCopy with this manifestDigest and a new operationId. If the bytes change first, the adoption is refused.",
|
|
5682
|
+
].join(" ");
|
|
5683
|
+
}
|
|
5684
|
+
|
|
5685
|
+
function publicWorkingCopyAdoption(
|
|
5686
|
+
adoption: PlacementBrowserWorkingCopyAdoption,
|
|
5687
|
+
): BrowserWorkingCopyAdoptionValue {
|
|
5688
|
+
return {
|
|
5689
|
+
operationId: adoption.operationId,
|
|
5690
|
+
browserSessionId: adoption.browserSessionId,
|
|
5691
|
+
controllerGeneration: adoption.controllerGeneration,
|
|
5692
|
+
manifestDigest: adoption.manifestDigest,
|
|
5693
|
+
phase: adoption.phase,
|
|
5694
|
+
failureCode: adoption.failureCode,
|
|
5695
|
+
retainedDirectories: adoption.retainedDirectories,
|
|
5696
|
+
createdAt: adoption.createdAt,
|
|
5697
|
+
updatedAt: adoption.updatedAt,
|
|
5698
|
+
};
|
|
5699
|
+
}
|
|
5700
|
+
|
|
5701
|
+
function workingCopyAdoptionExplanation(
|
|
5702
|
+
inFlight: boolean,
|
|
5703
|
+
adoption: PlacementBrowserWorkingCopyAdoption | null,
|
|
5704
|
+
): string {
|
|
5705
|
+
const kept = adoption?.retainedDirectories.length
|
|
5706
|
+
? ` Kept on the machine for separate cleanup: ${adoption.retainedDirectories.join(", ")}.`
|
|
5707
|
+
: "";
|
|
5708
|
+
if (inFlight)
|
|
5709
|
+
return "The adoption is still running on the controller. Check again with getBrowserSessionWorkingCopyAdoption; do not start another.";
|
|
5710
|
+
if (!adoption)
|
|
5711
|
+
return "The controller has no record of this adoption, which proves it changed nothing there.";
|
|
5712
|
+
switch (adoption.phase) {
|
|
5713
|
+
case "adopted":
|
|
5714
|
+
return `Adopted. The browser continues from the copy with manifest digest ${adoption.manifestDigest} the next time it is used, and is never replaced by an older checkpoint.${kept}`;
|
|
5715
|
+
case "interrupted":
|
|
5716
|
+
case "failed":
|
|
5717
|
+
return `The adoption stopped before the copy took the browser's place${adoption.failureCode ? ` (${adoption.failureCode})` : ""}; the browser's own directory is unchanged.${kept}`;
|
|
5718
|
+
case "collision":
|
|
5719
|
+
return `The adoption found the browser's directories in an unexpected state (${adoption.failureCode ?? "unknown"}) and stopped without moving or removing anything more. The browser stays unavailable until this adoption is reverted with revertBrowserSessionWorkingCopyAdoption, which puts the browser's own directory back and keeps the staged copy. If the revert refuses, the directories need a person on that machine.${kept}`;
|
|
5720
|
+
case "reverting":
|
|
5721
|
+
return "A revert of this adoption stopped part way and is not running. Retry revertBrowserSessionWorkingCopyAdoption with the same operationId to finish it; a retry repeats nothing already done.";
|
|
5722
|
+
case "reverted":
|
|
5723
|
+
return `Reverted. The browser's own directory is back in place and the staged copy was not used.${kept}`;
|
|
5724
|
+
default:
|
|
5725
|
+
return "The adoption stopped part way and is not running. Checking it never changes it. To finish it, retry the same adoption with the same operationId and manifestDigest under the authority that approved it; a retry repeats nothing already done. To undo it instead, revert it with revertBrowserSessionWorkingCopyAdoption. Do not start another.";
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5379
5729
|
function browserRouteError(error: unknown): HTTPException {
|
|
5380
5730
|
const tabDiagnosis = browserTabDiagnosisError(error);
|
|
5381
5731
|
if (tabDiagnosis instanceof ApiHttpError) return tabDiagnosis;
|
|
@@ -247,6 +247,18 @@ function parseId(schema: z.ZodString, value: string, label: string): string {
|
|
|
247
247
|
|
|
248
248
|
function rethrowMembershipError(error: unknown, resourceLimitMessage?: string): never {
|
|
249
249
|
const sqlState = nestedPostgresSqlState(error);
|
|
250
|
+
if (sqlState === "P0S01") {
|
|
251
|
+
throw new HTTPException(402, {
|
|
252
|
+
res: Response.json(
|
|
253
|
+
{
|
|
254
|
+
error: "seat_limit_reached",
|
|
255
|
+
message: "Organization seat limit reached. Upgrade your plan to add people.",
|
|
256
|
+
action: { type: "upgrade", href: "/billing" },
|
|
257
|
+
},
|
|
258
|
+
{ status: 402 },
|
|
259
|
+
),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
250
262
|
if (sqlState === "54000" && resourceLimitMessage) {
|
|
251
263
|
throw new HTTPException(409, {
|
|
252
264
|
message: resourceLimitMessage,
|
package/src/routes/sessions.ts
CHANGED
|
@@ -1004,6 +1004,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
|
|
|
1004
1004
|
...(query.includeTotals ? { includeTotals: true } : {}),
|
|
1005
1005
|
...(query.needsYouOnly ? { needsYouOnly: true } : {}),
|
|
1006
1006
|
...(query.contentArchivedOnly ? { contentArchivedOnly: true } : {}),
|
|
1007
|
+
...(query.mineOnly ? { mineOnly: true } : {}),
|
|
1007
1008
|
...(query.includePinned === false ? { includePinned: false } : {}),
|
|
1008
1009
|
...(query.archivedOnly ? { archivedOnly: true } : {}),
|
|
1009
1010
|
...(query.sortBy ? { sortBy: query.sortBy } : {}),
|
|
@@ -5835,6 +5836,7 @@ export function sessionListQuery(
|
|
|
5835
5836
|
includeTotals: boolean;
|
|
5836
5837
|
needsYouOnly: boolean;
|
|
5837
5838
|
contentArchivedOnly: boolean;
|
|
5839
|
+
mineOnly: boolean;
|
|
5838
5840
|
archivedOnly: boolean;
|
|
5839
5841
|
sortBy: "updatedAt" | "createdAt" | "name" | undefined;
|
|
5840
5842
|
archiveStatus: "active" | "archived" | "all" | undefined;
|
|
@@ -5884,17 +5886,20 @@ export function sessionListQuery(
|
|
|
5884
5886
|
throw new HTTPException(400, { message: 'includePinned must be "true" or "false"' });
|
|
5885
5887
|
}
|
|
5886
5888
|
const includePinned = query.includePinned !== "false";
|
|
5887
|
-
for (const key of ["includeTotals", "needsYouOnly", "contentArchivedOnly"]) {
|
|
5889
|
+
for (const key of ["includeTotals", "needsYouOnly", "contentArchivedOnly", "mineOnly"]) {
|
|
5888
5890
|
if (query[key] !== undefined && !["true", "false"].includes(query[key]!))
|
|
5889
5891
|
throw new HTTPException(400, { message: `${key} must be "true" or "false"` });
|
|
5890
5892
|
}
|
|
5891
5893
|
const includeTotals = query.includeTotals === "true";
|
|
5892
5894
|
const needsYouOnly = query.needsYouOnly === "true";
|
|
5893
5895
|
const contentArchivedOnly = query.contentArchivedOnly === "true";
|
|
5896
|
+
const mineOnly = query.mineOnly === "true";
|
|
5894
5897
|
if (includeTotals && (!allowCursor || (parentSessionId !== "null" && !pinsOnly)))
|
|
5895
5898
|
throw new HTTPException(400, { message: "includeTotals requires a root page" });
|
|
5896
5899
|
if (needsYouOnly && !allowCursor)
|
|
5897
5900
|
throw new HTTPException(400, { message: 'needsYouOnly requires view="page"' });
|
|
5901
|
+
if (mineOnly && !allowCursor)
|
|
5902
|
+
throw new HTTPException(400, { message: 'mineOnly requires view="page"' });
|
|
5898
5903
|
if (pinsOnly && !includePinned) {
|
|
5899
5904
|
throw new HTTPException(400, { message: "pinsOnly requires includePinned" });
|
|
5900
5905
|
}
|
|
@@ -5990,6 +5995,7 @@ export function sessionListQuery(
|
|
|
5990
5995
|
const hasPageFilters =
|
|
5991
5996
|
needsYouOnly ||
|
|
5992
5997
|
contentArchivedOnly ||
|
|
5998
|
+
mineOnly ||
|
|
5993
5999
|
originSiteId !== undefined ||
|
|
5994
6000
|
channelId !== undefined ||
|
|
5995
6001
|
createdByKind !== undefined ||
|
|
@@ -6025,6 +6031,7 @@ export function sessionListQuery(
|
|
|
6025
6031
|
includeTotals,
|
|
6026
6032
|
needsYouOnly,
|
|
6027
6033
|
contentArchivedOnly,
|
|
6034
|
+
mineOnly,
|
|
6028
6035
|
archivedOnly,
|
|
6029
6036
|
sortBy: sortBy.data,
|
|
6030
6037
|
archiveStatus: archiveStatus.data,
|