local-browser-bridge 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.
Files changed (92) hide show
  1. package/README.md +724 -0
  2. package/dist/package.json +61 -0
  3. package/dist/src/browser/chrome.d.ts +19 -0
  4. package/dist/src/browser/chrome.js +778 -0
  5. package/dist/src/browser/index.d.ts +3 -0
  6. package/dist/src/browser/index.js +25 -0
  7. package/dist/src/browser/safari.d.ts +41 -0
  8. package/dist/src/browser/safari.js +827 -0
  9. package/dist/src/browser-attach-ux-helper.d.ts +39 -0
  10. package/dist/src/browser-attach-ux-helper.js +157 -0
  11. package/dist/src/capabilities.d.ts +3 -0
  12. package/dist/src/capabilities.js +182 -0
  13. package/dist/src/chrome-relay-error-helper.d.ts +19 -0
  14. package/dist/src/chrome-relay-error-helper.js +78 -0
  15. package/dist/src/chrome-relay-helper-cli.d.ts +2 -0
  16. package/dist/src/chrome-relay-helper-cli.js +97 -0
  17. package/dist/src/chrome-relay-helper.d.ts +29 -0
  18. package/dist/src/chrome-relay-helper.js +151 -0
  19. package/dist/src/chrome-relay-state.d.ts +23 -0
  20. package/dist/src/chrome-relay-state.js +108 -0
  21. package/dist/src/claude-code.d.ts +20 -0
  22. package/dist/src/claude-code.js +66 -0
  23. package/dist/src/cli-reference-adapter.d.ts +13 -0
  24. package/dist/src/cli-reference-adapter.js +48 -0
  25. package/dist/src/cli.d.ts +3 -0
  26. package/dist/src/cli.js +200 -0
  27. package/dist/src/codex.d.ts +17 -0
  28. package/dist/src/codex.js +25 -0
  29. package/dist/src/connection-ux.d.ts +61 -0
  30. package/dist/src/connection-ux.js +256 -0
  31. package/dist/src/errors.d.ts +12 -0
  32. package/dist/src/errors.js +58 -0
  33. package/dist/src/http-reference-adapter.d.ts +34 -0
  34. package/dist/src/http-reference-adapter.js +61 -0
  35. package/dist/src/http.d.ts +3 -0
  36. package/dist/src/http.js +161 -0
  37. package/dist/src/index.d.ts +17 -0
  38. package/dist/src/index.js +43 -0
  39. package/dist/src/mcp-stdio.d.ts +2 -0
  40. package/dist/src/mcp-stdio.js +10 -0
  41. package/dist/src/mcp.d.ts +25 -0
  42. package/dist/src/mcp.js +483 -0
  43. package/dist/src/reference-adapter.d.ts +32 -0
  44. package/dist/src/reference-adapter.js +42 -0
  45. package/dist/src/service/attach-service.d.ts +28 -0
  46. package/dist/src/service/attach-service.js +272 -0
  47. package/dist/src/session-metadata.d.ts +4 -0
  48. package/dist/src/session-metadata.js +88 -0
  49. package/dist/src/store/session-store.d.ts +14 -0
  50. package/dist/src/store/session-store.js +52 -0
  51. package/dist/src/target.d.ts +9 -0
  52. package/dist/src/target.js +61 -0
  53. package/dist/src/types.d.ts +397 -0
  54. package/dist/src/types.js +2 -0
  55. package/dist/tests/attach-service.test.d.ts +1 -0
  56. package/dist/tests/attach-service.test.js +1367 -0
  57. package/dist/tests/browser-attach-ux-helper.test.d.ts +1 -0
  58. package/dist/tests/browser-attach-ux-helper.test.js +139 -0
  59. package/dist/tests/chrome-relay-error-helper.test.d.ts +1 -0
  60. package/dist/tests/chrome-relay-error-helper.test.js +67 -0
  61. package/dist/tests/chrome-relay-helper.test.d.ts +1 -0
  62. package/dist/tests/chrome-relay-helper.test.js +142 -0
  63. package/dist/tests/chrome-relay-state-schema.test.d.ts +1 -0
  64. package/dist/tests/chrome-relay-state-schema.test.js +96 -0
  65. package/dist/tests/claude-code-wrapper.test.d.ts +1 -0
  66. package/dist/tests/claude-code-wrapper.test.js +170 -0
  67. package/dist/tests/codex.test.d.ts +1 -0
  68. package/dist/tests/codex.test.js +210 -0
  69. package/dist/tests/demo-client-smoke.test.d.ts +1 -0
  70. package/dist/tests/demo-client-smoke.test.js +405 -0
  71. package/dist/tests/docs-fixtures.test.d.ts +1 -0
  72. package/dist/tests/docs-fixtures.test.js +255 -0
  73. package/dist/tests/doctor-connect-wrapper.test.d.ts +1 -0
  74. package/dist/tests/doctor-connect-wrapper.test.js +62 -0
  75. package/dist/tests/fixtures/doctor-connect-cli-stub.d.ts +1 -0
  76. package/dist/tests/fixtures/doctor-connect-cli-stub.js +93 -0
  77. package/dist/tests/fixtures/public-root-cli-stub.d.ts +210 -0
  78. package/dist/tests/fixtures/public-root-cli-stub.js +143 -0
  79. package/dist/tests/fixtures/public-root-consumer.js +67 -0
  80. package/dist/tests/mcp.test.d.ts +1 -0
  81. package/dist/tests/mcp.test.js +345 -0
  82. package/dist/tests/public-consumer-helpers.test.d.ts +1 -0
  83. package/dist/tests/public-consumer-helpers.test.js +33 -0
  84. package/dist/tests/public-package-git-consumption.test.d.ts +1 -0
  85. package/dist/tests/public-package-git-consumption.test.js +56 -0
  86. package/dist/tests/public-root-consumer-smoke.test.d.ts +1 -0
  87. package/dist/tests/public-root-consumer-smoke.test.js +214 -0
  88. package/dist/tests/reference-adapter.test.d.ts +1 -0
  89. package/dist/tests/reference-adapter.test.js +220 -0
  90. package/dist/tests/transport-reference-adapters.test.d.ts +1 -0
  91. package/dist/tests/transport-reference-adapters.test.js +214 -0
  92. package/package.json +61 -0
@@ -0,0 +1,28 @@
1
+ import type { ActivationArtifact, AttachRequest, AttachmentSession, BrowserAdapter, BridgeCapabilitiesContract, BrowserDiagnostics, BrowserTabTarget, NavigationArtifact, ResumedSession, ScreenshotArtifact, NavigateOptions, ScreenshotOptions, SessionActivation, SessionNavigation, SessionScreenshot, SupportedBrowser, TabMetadata } from "../types";
2
+ import { SessionStore } from "../store/session-store";
3
+ interface AttachServiceOptions {
4
+ store?: SessionStore;
5
+ adapterFactory?: (browser: SupportedBrowser) => BrowserAdapter;
6
+ }
7
+ export declare class AttachService {
8
+ private readonly store;
9
+ private readonly adapterFactory;
10
+ constructor(options?: AttachServiceOptions);
11
+ inspectFrontTab(browser: SupportedBrowser): Promise<TabMetadata>;
12
+ inspectTab(browser: SupportedBrowser, target?: BrowserTabTarget): Promise<TabMetadata>;
13
+ listTabs(browser: SupportedBrowser): Promise<TabMetadata[]>;
14
+ activate(browser: SupportedBrowser, target?: BrowserTabTarget): Promise<ActivationArtifact>;
15
+ activateSession(id: string): Promise<SessionActivation>;
16
+ navigate(browser: SupportedBrowser, target: BrowserTabTarget, options: NavigateOptions): Promise<NavigationArtifact>;
17
+ navigateSession(id: string, options: NavigateOptions): Promise<SessionNavigation>;
18
+ screenshot(browser: SupportedBrowser, target?: BrowserTabTarget, options?: ScreenshotOptions): Promise<ScreenshotArtifact>;
19
+ attach(browser: SupportedBrowser, targetOrRequest?: BrowserTabTarget | AttachRequest): Promise<AttachmentSession>;
20
+ listSessions(): Promise<AttachmentSession[]>;
21
+ diagnostics(browser: SupportedBrowser): Promise<BrowserDiagnostics>;
22
+ getCapabilities(browser?: SupportedBrowser): BridgeCapabilitiesContract;
23
+ getSession(id: string): Promise<AttachmentSession>;
24
+ screenshotSession(id: string, options?: ScreenshotOptions): Promise<SessionScreenshot>;
25
+ resumeSession(id: string): Promise<ResumedSession>;
26
+ private buildScreenshotPath;
27
+ }
28
+ export {};
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AttachService = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_path_1 = require("node:path");
6
+ const browser_1 = require("../browser");
7
+ const capabilities_1 = require("../capabilities");
8
+ const chrome_1 = require("../browser/chrome");
9
+ const errors_1 = require("../errors");
10
+ const session_metadata_1 = require("../session-metadata");
11
+ const session_store_1 = require("../store/session-store");
12
+ const target_1 = require("../target");
13
+ class AttachService {
14
+ store;
15
+ adapterFactory;
16
+ constructor(options = {}) {
17
+ this.store = options.store ?? new session_store_1.SessionStore();
18
+ this.adapterFactory = options.adapterFactory ?? browser_1.getBrowserAdapter;
19
+ }
20
+ async inspectFrontTab(browser) {
21
+ return this.inspectTab(browser, { type: "front" });
22
+ }
23
+ async inspectTab(browser, target = { type: "front" }) {
24
+ return this.adapterFactory(browser).resolveTab(target);
25
+ }
26
+ async listTabs(browser) {
27
+ return this.adapterFactory(browser).listTabs();
28
+ }
29
+ async activate(browser, target = { type: "front" }) {
30
+ return this.adapterFactory(browser).performSessionAction({
31
+ action: "activate",
32
+ target
33
+ });
34
+ }
35
+ async activateSession(id) {
36
+ const session = await this.getSession(id);
37
+ const activation = await this.activate(session.browser, session.target);
38
+ return {
39
+ session,
40
+ activation
41
+ };
42
+ }
43
+ async navigate(browser, target, options) {
44
+ return this.adapterFactory(browser).performSessionAction({
45
+ action: "navigate",
46
+ target,
47
+ options
48
+ });
49
+ }
50
+ async navigateSession(id, options) {
51
+ const resumed = await this.resumeSession(id);
52
+ const navigation = await this.navigate(resumed.session.browser, {
53
+ type: "indexed",
54
+ windowIndex: navigationTargetWindowIndex(resumed.tab),
55
+ tabIndex: navigationTargetTabIndex(resumed.tab)
56
+ }, options);
57
+ const updatedSession = {
58
+ ...resumed.session,
59
+ target: (0, target_1.buildSignatureTargetFromTab)(navigation.tab),
60
+ tab: navigation.tab,
61
+ frontTab: navigation.tab
62
+ };
63
+ await this.store.update(updatedSession);
64
+ return {
65
+ session: (0, session_metadata_1.normalizeAttachmentSession)(updatedSession),
66
+ navigation
67
+ };
68
+ }
69
+ async screenshot(browser, target = { type: "front" }, options = {}) {
70
+ const outputPath = options.outputPath ?? this.buildScreenshotPath(browser);
71
+ return this.adapterFactory(browser).performSessionAction({
72
+ action: "screenshot",
73
+ target,
74
+ options: { outputPath }
75
+ });
76
+ }
77
+ async attach(browser, targetOrRequest = { type: "front" }) {
78
+ const request = isAttachRequest(targetOrRequest)
79
+ ? targetOrRequest
80
+ : { target: targetOrRequest };
81
+ const target = request.target ?? { type: "front" };
82
+ if (browser === "chrome" && request.attach?.mode === "relay") {
83
+ const relay = await (0, chrome_1.resolveChromeRelayAttach)(target);
84
+ const createdAt = new Date().toISOString();
85
+ const session = (0, session_metadata_1.normalizeAttachmentSession)({
86
+ id: (0, node_crypto_1.randomUUID)(),
87
+ browser,
88
+ target: (0, target_1.buildSignatureTargetFromTab)(relay.tab),
89
+ tab: relay.tab,
90
+ frontTab: relay.tab,
91
+ createdAt,
92
+ attach: {
93
+ mode: "relay",
94
+ source: "extension-relay",
95
+ scope: "tab",
96
+ resumable: relay.resumable,
97
+ expiresAt: relay.expiresAt,
98
+ resumeRequiresUserGesture: relay.resumeRequiresUserGesture,
99
+ trustedAt: relay.trustedAt
100
+ }
101
+ });
102
+ return this.store.create(session);
103
+ }
104
+ const tab = await this.inspectTab(browser, target);
105
+ const createdAt = new Date().toISOString();
106
+ const session = (0, session_metadata_1.normalizeAttachmentSession)({
107
+ id: (0, node_crypto_1.randomUUID)(),
108
+ browser,
109
+ target: (0, target_1.buildSignatureTargetFromTab)(tab),
110
+ tab,
111
+ frontTab: tab,
112
+ createdAt
113
+ });
114
+ return this.store.create(session);
115
+ }
116
+ async listSessions() {
117
+ return this.store.list();
118
+ }
119
+ async diagnostics(browser) {
120
+ return this.adapterFactory(browser).getDiagnostics();
121
+ }
122
+ getCapabilities(browser) {
123
+ return (0, capabilities_1.getBridgeCapabilities)(browser);
124
+ }
125
+ async getSession(id) {
126
+ const session = await this.store.get(id);
127
+ if (!session) {
128
+ throw new errors_1.AppError(`Session not found: ${id}`, 404, "session_not_found");
129
+ }
130
+ return session;
131
+ }
132
+ async screenshotSession(id, options = {}) {
133
+ const session = await this.getSession(id);
134
+ const screenshot = await this.screenshot(session.browser, session.target, { outputPath: options.outputPath ?? this.buildScreenshotPath(session.browser, session.id) });
135
+ return {
136
+ session,
137
+ screenshot
138
+ };
139
+ }
140
+ async resumeSession(id) {
141
+ const session = await this.getSession(id);
142
+ if (session.browser === "chrome" && session.attach.mode === "relay") {
143
+ const resumed = await (0, chrome_1.resumeChromeRelaySession)(session);
144
+ const refreshedSession = await this.store.update(resumed.session);
145
+ return {
146
+ ...resumed,
147
+ session: refreshedSession
148
+ };
149
+ }
150
+ const tabs = await this.listTabs(session.browser);
151
+ const { target } = session;
152
+ if (target.type === "front") {
153
+ const tab = await this.inspectTab(session.browser, target);
154
+ return {
155
+ session,
156
+ tab,
157
+ resumedAt: new Date().toISOString(),
158
+ resolution: {
159
+ strategy: "front",
160
+ matched: true,
161
+ attachMode: session.attach.mode,
162
+ semantics: session.semantics.resume
163
+ }
164
+ };
165
+ }
166
+ if (target.type === "indexed") {
167
+ const tab = await this.inspectTab(session.browser, target);
168
+ return {
169
+ session,
170
+ tab,
171
+ resumedAt: new Date().toISOString(),
172
+ resolution: {
173
+ strategy: "indexed",
174
+ matched: true,
175
+ attachMode: session.attach.mode,
176
+ semantics: session.semantics.resume
177
+ }
178
+ };
179
+ }
180
+ const byNativeIdentity = target.native?.kind === "chrome-devtools-target"
181
+ ? tabs.find((tab) => tab.identity.native?.kind === "chrome-devtools-target" &&
182
+ tab.identity.native.targetId === target.native?.targetId)
183
+ : undefined;
184
+ if (byNativeIdentity) {
185
+ return {
186
+ session,
187
+ tab: byNativeIdentity,
188
+ resumedAt: new Date().toISOString(),
189
+ resolution: {
190
+ strategy: "native_identity",
191
+ matched: true,
192
+ attachMode: session.attach.mode,
193
+ semantics: session.semantics.resume
194
+ }
195
+ };
196
+ }
197
+ const bySignature = tabs.find((tab) => tab.identity.signature === target.signature);
198
+ if (bySignature) {
199
+ return {
200
+ session,
201
+ tab: bySignature,
202
+ resumedAt: new Date().toISOString(),
203
+ resolution: {
204
+ strategy: "signature",
205
+ matched: true,
206
+ attachMode: session.attach.mode,
207
+ semantics: session.semantics.resume
208
+ }
209
+ };
210
+ }
211
+ const byUrlTitle = tabs.find((tab) => tab.url === target.url && tab.identity.titleKey === (session.tab.identity?.titleKey ?? ""));
212
+ if (byUrlTitle) {
213
+ return {
214
+ session,
215
+ tab: byUrlTitle,
216
+ resumedAt: new Date().toISOString(),
217
+ resolution: {
218
+ strategy: "url_title",
219
+ matched: true,
220
+ attachMode: session.attach.mode,
221
+ semantics: session.semantics.resume
222
+ }
223
+ };
224
+ }
225
+ const byUrl = target.url ? tabs.find((tab) => tab.url === target.url) : undefined;
226
+ if (byUrl) {
227
+ return {
228
+ session,
229
+ tab: byUrl,
230
+ resumedAt: new Date().toISOString(),
231
+ resolution: {
232
+ strategy: "url",
233
+ matched: true,
234
+ attachMode: session.attach.mode,
235
+ semantics: session.semantics.resume
236
+ }
237
+ };
238
+ }
239
+ const byLastKnown = target.lastKnownWindowIndex && target.lastKnownTabIndex
240
+ ? tabs.find((tab) => tab.windowIndex === target.lastKnownWindowIndex && tab.tabIndex === target.lastKnownTabIndex)
241
+ : undefined;
242
+ if (byLastKnown) {
243
+ return {
244
+ session,
245
+ tab: byLastKnown,
246
+ resumedAt: new Date().toISOString(),
247
+ resolution: {
248
+ strategy: "last_known_index",
249
+ matched: true,
250
+ attachMode: session.attach.mode,
251
+ semantics: session.semantics.resume
252
+ }
253
+ };
254
+ }
255
+ throw new errors_1.AppError(`Unable to resume session ${id}; the saved ${session.browser} tab can no longer be matched.`, 404, "tab_not_found");
256
+ }
257
+ buildScreenshotPath(browser, sessionId) {
258
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
259
+ const fileName = sessionId ? `${browser}-${sessionId}-${stamp}.png` : `${browser}-${stamp}.png`;
260
+ return (0, node_path_1.resolve)(process.cwd(), ".data", "screenshots", fileName);
261
+ }
262
+ }
263
+ exports.AttachService = AttachService;
264
+ function navigationTargetWindowIndex(tab) {
265
+ return tab.windowIndex;
266
+ }
267
+ function navigationTargetTabIndex(tab) {
268
+ return tab.tabIndex;
269
+ }
270
+ function isAttachRequest(value) {
271
+ return typeof value === "object" && value !== null && ("attach" in value || "target" in value);
272
+ }
@@ -0,0 +1,4 @@
1
+ import type { AttachmentSession } from "./types";
2
+ type AttachmentSessionRecord = Omit<AttachmentSession, "schemaVersion" | "kind" | "capabilities" | "status" | "attach" | "semantics"> & Partial<Pick<AttachmentSession, "schemaVersion" | "kind" | "capabilities" | "status" | "attach" | "semantics">>;
3
+ export declare function normalizeAttachmentSession(session: AttachmentSessionRecord): AttachmentSession;
4
+ export {};
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeAttachmentSession = normalizeAttachmentSession;
4
+ const capabilities_1 = require("./capabilities");
5
+ function buildSessionKind(browser) {
6
+ return (0, capabilities_1.getBrowserCapabilityDescriptor)(browser).kind;
7
+ }
8
+ function buildSessionCapabilities(browser) {
9
+ const descriptor = (0, capabilities_1.getBrowserCapabilityDescriptor)(browser);
10
+ return {
11
+ resume: true,
12
+ activate: descriptor.operations.activate,
13
+ navigate: descriptor.operations.navigate,
14
+ screenshot: descriptor.operations.screenshot
15
+ };
16
+ }
17
+ function buildSessionStatus(capabilities) {
18
+ const canAct = capabilities.activate || capabilities.navigate || capabilities.screenshot;
19
+ return {
20
+ state: canAct ? "actionable" : "read-only",
21
+ canAct
22
+ };
23
+ }
24
+ function buildSessionSemantics(browser, attach) {
25
+ if (browser === "chrome" && attach.mode === "relay") {
26
+ return {
27
+ inspect: "shared-tab-only",
28
+ list: "saved-session",
29
+ resume: "current-shared-tab",
30
+ tabReference: {
31
+ windowIndex: "synthetic-shared-tab-position",
32
+ tabIndex: "synthetic-shared-tab-position"
33
+ },
34
+ notes: [
35
+ "Relay sessions describe the last tab explicitly shared through the extension, not general Chrome tab visibility.",
36
+ "Resume only checks the currently shared relay tab and may require the user to share the tab again."
37
+ ]
38
+ };
39
+ }
40
+ return {
41
+ inspect: "browser-tabs",
42
+ list: "saved-session",
43
+ resume: "saved-browser-target",
44
+ tabReference: {
45
+ windowIndex: "browser-position",
46
+ tabIndex: "browser-position"
47
+ }
48
+ };
49
+ }
50
+ function defaultAttachMode(browser) {
51
+ return browser === "chrome" ? "direct" : "direct";
52
+ }
53
+ function buildSessionAttach(browser, existing) {
54
+ const mode = existing?.mode ?? defaultAttachMode(browser);
55
+ if (browser === "chrome") {
56
+ return {
57
+ mode,
58
+ source: existing?.source ?? (mode === "relay" ? "extension-relay" : "user-browser"),
59
+ scope: existing?.scope ?? (mode === "relay" ? "tab" : "browser"),
60
+ resumable: existing?.resumable,
61
+ expiresAt: existing?.expiresAt,
62
+ resumeRequiresUserGesture: existing?.resumeRequiresUserGesture,
63
+ trustedAt: existing?.trustedAt
64
+ };
65
+ }
66
+ return {
67
+ mode: "direct",
68
+ source: existing?.source ?? "user-browser",
69
+ scope: existing?.scope ?? "browser",
70
+ resumable: existing?.resumable,
71
+ expiresAt: existing?.expiresAt,
72
+ resumeRequiresUserGesture: existing?.resumeRequiresUserGesture,
73
+ trustedAt: existing?.trustedAt
74
+ };
75
+ }
76
+ function normalizeAttachmentSession(session) {
77
+ const attach = buildSessionAttach(session.browser, session.attach);
78
+ const capabilities = buildSessionCapabilities(session.browser);
79
+ return {
80
+ ...session,
81
+ schemaVersion: 1,
82
+ kind: buildSessionKind(session.browser),
83
+ attach,
84
+ semantics: buildSessionSemantics(session.browser, attach),
85
+ capabilities,
86
+ status: buildSessionStatus(capabilities)
87
+ };
88
+ }
@@ -0,0 +1,14 @@
1
+ import type { AttachmentSession } from "../types";
2
+ interface SessionStoreOptions {
3
+ filePath?: string;
4
+ }
5
+ export declare class SessionStore {
6
+ private readonly filePath;
7
+ constructor(options?: SessionStoreOptions);
8
+ list(): Promise<AttachmentSession[]>;
9
+ get(id: string): Promise<AttachmentSession | undefined>;
10
+ create(session: AttachmentSession): Promise<AttachmentSession>;
11
+ update(session: AttachmentSession): Promise<AttachmentSession>;
12
+ private ensureFile;
13
+ }
14
+ export {};
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionStore = void 0;
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
6
+ const session_metadata_1 = require("../session-metadata");
7
+ class SessionStore {
8
+ filePath;
9
+ constructor(options = {}) {
10
+ this.filePath = options.filePath ?? (0, node_path_1.resolve)(process.cwd(), ".data", "sessions.json");
11
+ }
12
+ async list() {
13
+ await this.ensureFile();
14
+ const raw = await (0, promises_1.readFile)(this.filePath, "utf8");
15
+ const parsed = JSON.parse(raw);
16
+ return parsed.map(session_metadata_1.normalizeAttachmentSession).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
17
+ }
18
+ async get(id) {
19
+ const sessions = await this.list();
20
+ return sessions.find((session) => session.id === id);
21
+ }
22
+ async create(session) {
23
+ const sessions = await this.list();
24
+ const normalizedSession = (0, session_metadata_1.normalizeAttachmentSession)(session);
25
+ sessions.unshift(normalizedSession);
26
+ await (0, promises_1.writeFile)(this.filePath, JSON.stringify(sessions, null, 2) + "\n", "utf8");
27
+ return normalizedSession;
28
+ }
29
+ async update(session) {
30
+ const sessions = await this.list();
31
+ const normalizedSession = (0, session_metadata_1.normalizeAttachmentSession)(session);
32
+ const index = sessions.findIndex((candidate) => candidate.id === normalizedSession.id);
33
+ if (index === -1) {
34
+ sessions.unshift(normalizedSession);
35
+ }
36
+ else {
37
+ sessions[index] = normalizedSession;
38
+ }
39
+ await (0, promises_1.writeFile)(this.filePath, JSON.stringify(sessions, null, 2) + "\n", "utf8");
40
+ return normalizedSession;
41
+ }
42
+ async ensureFile() {
43
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(this.filePath), { recursive: true });
44
+ try {
45
+ await (0, promises_1.readFile)(this.filePath, "utf8");
46
+ }
47
+ catch {
48
+ await (0, promises_1.writeFile)(this.filePath, "[]\n", "utf8");
49
+ }
50
+ }
51
+ }
52
+ exports.SessionStore = SessionStore;
@@ -0,0 +1,9 @@
1
+ import type { BrowserTabTarget, TabMetadata } from "./types";
2
+ export declare function buildTabTarget(input?: {
3
+ windowIndex?: string | number;
4
+ tabIndex?: string | number;
5
+ signature?: string;
6
+ url?: string;
7
+ title?: string;
8
+ }): BrowserTabTarget;
9
+ export declare function buildSignatureTargetFromTab(tab: TabMetadata): BrowserTabTarget;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildTabTarget = buildTabTarget;
4
+ exports.buildSignatureTargetFromTab = buildSignatureTargetFromTab;
5
+ const errors_1 = require("./errors");
6
+ function parseIndex(value, fieldName) {
7
+ if (value === undefined) {
8
+ return undefined;
9
+ }
10
+ const parsed = Number(value);
11
+ if (!Number.isInteger(parsed) || parsed < 1) {
12
+ throw new errors_1.AppError(`${fieldName} must be a positive integer.`, 400, "invalid_target");
13
+ }
14
+ return parsed;
15
+ }
16
+ function parseNonEmpty(value) {
17
+ if (value === undefined) {
18
+ return undefined;
19
+ }
20
+ const trimmed = value.trim();
21
+ return trimmed.length > 0 ? trimmed : undefined;
22
+ }
23
+ function buildTabTarget(input = {}) {
24
+ const windowIndex = parseIndex(input.windowIndex, "windowIndex");
25
+ const tabIndex = parseIndex(input.tabIndex, "tabIndex");
26
+ const signature = parseNonEmpty(input.signature);
27
+ const url = parseNonEmpty(input.url);
28
+ const title = parseNonEmpty(input.title);
29
+ if (signature) {
30
+ return {
31
+ type: "signature",
32
+ signature,
33
+ url,
34
+ title,
35
+ lastKnownWindowIndex: windowIndex,
36
+ lastKnownTabIndex: tabIndex
37
+ };
38
+ }
39
+ if (windowIndex === undefined && tabIndex === undefined) {
40
+ return { type: "front" };
41
+ }
42
+ if (windowIndex === undefined || tabIndex === undefined) {
43
+ throw new errors_1.AppError("windowIndex and tabIndex must be provided together for an explicit tab target.", 400, "invalid_target");
44
+ }
45
+ return {
46
+ type: "indexed",
47
+ windowIndex,
48
+ tabIndex
49
+ };
50
+ }
51
+ function buildSignatureTargetFromTab(tab) {
52
+ return {
53
+ type: "signature",
54
+ signature: tab.identity.signature,
55
+ url: tab.url,
56
+ title: tab.title,
57
+ lastKnownWindowIndex: tab.windowIndex,
58
+ lastKnownTabIndex: tab.tabIndex,
59
+ native: tab.identity.native
60
+ };
61
+ }