browser-cookie-bridge 1.0.0 → 1.2.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/README.md +67 -31
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/extension-template/manifest.json +1 -1
- package/macos-app/Info.plist +3 -3
- package/macos-app/Resources/BrowserIcons/browserless.svg +6 -0
- package/macos-app/Sources/BraveCodexSyncApp/BraveCodexSyncApp.swift +222 -43
- package/macos-app/Sources/BraveCodexSyncApp/SyncModel.swift +247 -27
- package/package.json +23 -7
- package/src/browserless-runner.js +13 -0
- package/src/browserless.js +108 -0
- package/src/cli.js +109 -4
- package/src/config.js +49 -14
- package/src/paths.js +1 -1
- package/src/updater.js +89 -27
|
@@ -3,6 +3,8 @@ import SwiftUI
|
|
|
3
3
|
|
|
4
4
|
private enum ProjectLinks {
|
|
5
5
|
static let repository = URL(string: "https://github.com/apoorvdarshan/browser-cookie-bridge")!
|
|
6
|
+
static let downloads = URL(string: "https://github.com/apoorvdarshan/browser-cookie-bridge/releases/latest")!
|
|
7
|
+
static let npm = URL(string: "https://www.npmjs.com/package/browser-cookie-bridge")!
|
|
6
8
|
static let issues = URL(string: "https://github.com/apoorvdarshan/browser-cookie-bridge/issues/new?template=bug_report.yml")!
|
|
7
9
|
static let license = URL(string: "https://github.com/apoorvdarshan/browser-cookie-bridge/blob/main/LICENSE")!
|
|
8
10
|
static let koFi = URL(string: "https://ko-fi.com/apoorvdarshan")!
|
|
@@ -168,6 +170,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
|
|
|
168
170
|
private func addProjectLinks(to menu: NSMenu) {
|
|
169
171
|
menu.addItem(.separator())
|
|
170
172
|
addLink("Open-source repository", url: ProjectLinks.repository, to: menu)
|
|
173
|
+
addLink("Download latest DMG", url: ProjectLinks.downloads, to: menu)
|
|
174
|
+
addLink("View on npm", url: ProjectLinks.npm, to: menu)
|
|
171
175
|
addLink("Report a bug…", url: ProjectLinks.issues, to: menu)
|
|
172
176
|
addLink("MIT license", url: ProjectLinks.license, to: menu)
|
|
173
177
|
menu.addItem(.separator())
|
|
@@ -209,6 +213,9 @@ struct ContentView: View {
|
|
|
209
213
|
.sheet(isPresented: $showingSetup) {
|
|
210
214
|
ExtensionSetupSheet().environmentObject(model)
|
|
211
215
|
}
|
|
216
|
+
.sheet(isPresented: $model.showingBrowserlessSetup) {
|
|
217
|
+
BrowserlessSetupSheet().environmentObject(model)
|
|
218
|
+
}
|
|
212
219
|
}
|
|
213
220
|
|
|
214
221
|
private var header: some View {
|
|
@@ -234,7 +241,10 @@ struct ContentView: View {
|
|
|
234
241
|
|
|
235
242
|
private var footer: some View {
|
|
236
243
|
HStack(spacing: 10) {
|
|
237
|
-
Label(
|
|
244
|
+
Label(
|
|
245
|
+
model.isBrowserlessTarget ? "Cloud upload only when you choose it" : "Data stays on this Mac",
|
|
246
|
+
systemImage: model.isBrowserlessTarget ? "cloud.fill" : "lock.shield.fill"
|
|
247
|
+
)
|
|
238
248
|
.foregroundStyle(.secondary)
|
|
239
249
|
Spacer()
|
|
240
250
|
if let version = model.availableUpdateVersion {
|
|
@@ -258,7 +268,7 @@ struct ContentView: View {
|
|
|
258
268
|
.buttonStyle(.plain)
|
|
259
269
|
.foregroundStyle(.secondary)
|
|
260
270
|
.help("Refresh status")
|
|
261
|
-
if model.selectedTargetID != "codex" {
|
|
271
|
+
if model.selectedTargetID != "codex" && !model.isBrowserlessTarget {
|
|
262
272
|
Button("Extension setup…") { showingSetup = true }
|
|
263
273
|
.controlSize(.small)
|
|
264
274
|
}
|
|
@@ -291,26 +301,32 @@ struct SyncPanel: View {
|
|
|
291
301
|
.help(model.secondaryStatus)
|
|
292
302
|
}
|
|
293
303
|
Spacer()
|
|
294
|
-
Button {
|
|
304
|
+
Button {
|
|
305
|
+
if model.isBrowserlessTarget && !model.browserlessConfigured {
|
|
306
|
+
model.showingBrowserlessSetup = true
|
|
307
|
+
} else {
|
|
308
|
+
model.syncNow()
|
|
309
|
+
}
|
|
310
|
+
} label: {
|
|
295
311
|
HStack(spacing: 7) {
|
|
296
312
|
if model.isSyncing { ProgressView().controlSize(.small) }
|
|
297
|
-
else { Image(systemName:
|
|
298
|
-
Text(
|
|
313
|
+
else { Image(systemName: syncButtonIcon) }
|
|
314
|
+
Text(syncButtonTitle)
|
|
299
315
|
}
|
|
300
|
-
.frame(minWidth: model.
|
|
316
|
+
.frame(minWidth: model.syncBlocked ? 116 : 92)
|
|
301
317
|
}
|
|
302
318
|
.buttonStyle(.borderedProminent)
|
|
303
319
|
.tint(Theme.accent)
|
|
304
|
-
.disabled(model.isSyncing || model.
|
|
320
|
+
.disabled(model.isSyncing || (model.syncBlocked && !(model.isBrowserlessTarget && !model.browserlessConfigured)))
|
|
305
321
|
.keyboardShortcut(.return, modifiers: .command)
|
|
306
322
|
}
|
|
307
|
-
.padding(model.
|
|
323
|
+
.padding(model.syncBlocked ? 7 : 0)
|
|
308
324
|
.background(
|
|
309
|
-
model.
|
|
325
|
+
model.syncBlocked ? Theme.active.opacity(0.10) : Color.clear,
|
|
310
326
|
in: RoundedRectangle(cornerRadius: 9, style: .continuous)
|
|
311
327
|
)
|
|
312
328
|
.overlay {
|
|
313
|
-
if model.
|
|
329
|
+
if model.syncBlocked {
|
|
314
330
|
RoundedRectangle(cornerRadius: 9, style: .continuous)
|
|
315
331
|
.stroke(Theme.active.opacity(0.38), lineWidth: 1)
|
|
316
332
|
}
|
|
@@ -319,6 +335,20 @@ struct SyncPanel: View {
|
|
|
319
335
|
}
|
|
320
336
|
}
|
|
321
337
|
|
|
338
|
+
private var syncButtonTitle: String {
|
|
339
|
+
if model.isSyncing { return model.isBrowserlessTarget ? "Uploading…" : "Syncing…" }
|
|
340
|
+
if model.codexBlocked { return "Close Codex first" }
|
|
341
|
+
if model.isBrowserlessTarget && !model.browserlessConfigured { return "Connect Browserless" }
|
|
342
|
+
if model.isBrowserlessTarget && model.selectedSourceID == "comet" { return "Choose another browser" }
|
|
343
|
+
if model.isBrowserlessTarget && model.sourceBrowserRunning { return "Close \(model.selectedBrowser.name) first" }
|
|
344
|
+
return model.isBrowserlessTarget ? "Upload now" : "Sync now"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private var syncButtonIcon: String {
|
|
348
|
+
if model.syncBlocked { return model.isBrowserlessTarget && !model.browserlessConfigured ? "key.fill" : "xmark.circle.fill" }
|
|
349
|
+
return model.isBrowserlessTarget ? "icloud.and.arrow.up.fill" : "arrow.triangle.2.circlepath"
|
|
350
|
+
}
|
|
351
|
+
|
|
322
352
|
}
|
|
323
353
|
|
|
324
354
|
struct SourcePicker: View {
|
|
@@ -374,7 +404,12 @@ struct TargetPicker: View {
|
|
|
374
404
|
VStack(spacing: 5) {
|
|
375
405
|
HStack(spacing: 5) {
|
|
376
406
|
ForEach(Array(model.browsers.prefix(4))) { browser in targetButton(browser) }
|
|
377
|
-
|
|
407
|
+
EndpointButton(
|
|
408
|
+
icon: model.browserlessIcon,
|
|
409
|
+
name: "Browserless Cloud",
|
|
410
|
+
selected: model.selectedTargetID == "browserless",
|
|
411
|
+
disabled: model.isWorking || model.isSyncing || model.selectedSourceID == "comet"
|
|
412
|
+
) { model.selectTarget("browserless") }
|
|
378
413
|
}
|
|
379
414
|
HStack(spacing: 5) {
|
|
380
415
|
ForEach(Array(model.browsers.dropFirst(4))) { browser in targetButton(browser) }
|
|
@@ -445,44 +480,69 @@ struct PreferencesPanel: View {
|
|
|
445
480
|
Surface(padding: 0) {
|
|
446
481
|
VStack(spacing: 0) {
|
|
447
482
|
SectionLabel(title: "Sync data", detail: "Saved automatically")
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
483
|
+
if model.isBrowserlessTarget {
|
|
484
|
+
PreferenceRow(icon: "person.crop.circle.badge.checkmark", color: Theme.accent, title: "Authenticated state", detail: "Cookies, local storage, and IndexedDB") {
|
|
485
|
+
FixedBadge("Included")
|
|
486
|
+
}
|
|
487
|
+
RowDivider()
|
|
488
|
+
PreferenceRow(icon: "clock.arrow.circlepath", color: .secondary, title: "History URLs", detail: "Not included in Browserless profiles", muted: true) {
|
|
489
|
+
FixedBadge("Excluded")
|
|
490
|
+
}
|
|
491
|
+
RowDivider()
|
|
492
|
+
PreferenceRow(icon: "key.slash", color: .secondary, title: "Saved passwords", detail: "Never read or uploaded", muted: true) {
|
|
493
|
+
FixedBadge("Excluded")
|
|
494
|
+
}
|
|
495
|
+
RowDivider()
|
|
496
|
+
PreferenceRow(icon: "cloud.fill", color: Theme.accent, title: "Browserless connection", detail: model.browserlessConfigured ? "\(model.browserlessProfileName) · \(model.browserlessRegion.uppercased()) · token in Keychain" : "Not connected") {
|
|
497
|
+
Button(model.browserlessConfigured ? "Configure…" : "Connect…") {
|
|
498
|
+
model.showingBrowserlessSetup = true
|
|
499
|
+
}
|
|
500
|
+
.controlSize(.small)
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
PreferenceRow(icon: "network", color: Theme.accent, title: "Cookies", detail: "Site sessions and sign-ins") {
|
|
504
|
+
Toggle("", isOn: Binding(get: { model.cookiesEnabled }, set: { model.setCookiesEnabled($0) }))
|
|
505
|
+
.labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking)
|
|
506
|
+
}
|
|
507
|
+
RowDivider()
|
|
508
|
+
PreferenceRow(icon: "clock.arrow.circlepath", color: Theme.accent, title: "History URLs", detail: "Original visit times are not preserved") {
|
|
509
|
+
Toggle("", isOn: Binding(get: { model.historyEnabled }, set: { model.setHistoryEnabled($0) }))
|
|
510
|
+
.labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking)
|
|
511
|
+
}
|
|
512
|
+
RowDivider()
|
|
513
|
+
PreferenceRow(icon: "key.slash", color: .secondary, title: "Passwords", detail: "Browser extensions cannot access passwords", muted: true) {
|
|
514
|
+
FixedBadge("Unavailable")
|
|
515
|
+
}
|
|
464
516
|
}
|
|
465
517
|
|
|
466
518
|
SectionLabel(title: "Automation", detail: "Runs in the background", separated: true)
|
|
467
|
-
PreferenceRow(icon: "clock.badge.checkmark", color: Theme.accent, title: "Daily sync", detail: model.dailyEnabled ? "At the selected local time" : "Off") {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
519
|
+
PreferenceRow(icon: "clock.badge.checkmark", color: Theme.accent, title: "Daily sync", detail: model.isBrowserlessTarget ? "Unavailable for cloud uploads" : (model.dailyEnabled ? "At the selected local time" : "Off")) {
|
|
520
|
+
if model.isBrowserlessTarget {
|
|
521
|
+
FixedBadge("Manual only")
|
|
522
|
+
} else {
|
|
523
|
+
HStack(spacing: 8) {
|
|
524
|
+
DatePicker("", selection: $model.scheduleTime, displayedComponents: .hourAndMinute)
|
|
525
|
+
.labelsHidden()
|
|
526
|
+
.datePickerStyle(.field)
|
|
527
|
+
.controlSize(.regular)
|
|
528
|
+
.disabled(!model.dailyEnabled || model.isWorking)
|
|
529
|
+
.frame(width: 112, alignment: .center)
|
|
530
|
+
.onChange(of: model.scheduleTime) { _ in
|
|
531
|
+
if model.dailyEnabled && !model.isWorking { model.saveSchedule() }
|
|
532
|
+
}
|
|
533
|
+
Toggle("", isOn: Binding(get: { model.dailyEnabled }, set: { model.setDailyEnabled($0) }))
|
|
534
|
+
.labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking)
|
|
535
|
+
}
|
|
480
536
|
}
|
|
481
537
|
}
|
|
482
538
|
RowDivider()
|
|
483
|
-
PreferenceRow(icon: "sunrise.fill", color: Theme.accent, title: "Sync at login", detail: model.loginSyncEnabled ? "Once whenever you sign in" : "Off") {
|
|
484
|
-
|
|
485
|
-
|
|
539
|
+
PreferenceRow(icon: "sunrise.fill", color: Theme.accent, title: "Sync at login", detail: model.isBrowserlessTarget ? "Unavailable for cloud uploads" : (model.loginSyncEnabled ? "Once whenever you sign in" : "Off")) {
|
|
540
|
+
if model.isBrowserlessTarget {
|
|
541
|
+
FixedBadge("Manual only")
|
|
542
|
+
} else {
|
|
543
|
+
Toggle("", isOn: Binding(get: { model.loginSyncEnabled }, set: { model.setLoginSyncEnabled($0) }))
|
|
544
|
+
.labelsHidden().toggleStyle(.switch).tint(Theme.active).disabled(model.isWorking)
|
|
545
|
+
}
|
|
486
546
|
}
|
|
487
547
|
RowDivider()
|
|
488
548
|
PreferenceRow(icon: "power", color: Theme.accent, title: "Open at login", detail: "Start the app after you sign in") {
|
|
@@ -503,6 +563,10 @@ struct PreferencesPanel: View {
|
|
|
503
563
|
SectionLabel(title: "Project & support", detail: "Open source", separated: true)
|
|
504
564
|
ProjectLinkRow(icon: "chevron.left.forwardslash.chevron.right", title: "Open-source repository", detail: "View the code on GitHub", url: ProjectLinks.repository)
|
|
505
565
|
RowDivider()
|
|
566
|
+
ProjectLinkRow(icon: "arrow.down.circle", title: "Download latest DMG", detail: "Apple silicon and Intel builds", url: ProjectLinks.downloads)
|
|
567
|
+
RowDivider()
|
|
568
|
+
ProjectLinkRow(icon: "shippingbox", title: "View on npm", detail: "Install the latest public release", url: ProjectLinks.npm)
|
|
569
|
+
RowDivider()
|
|
506
570
|
ProjectLinkRow(icon: "ladybug", title: "Report a bug", detail: "Open a GitHub issue", url: ProjectLinks.issues)
|
|
507
571
|
RowDivider()
|
|
508
572
|
ProjectLinkRow(icon: "doc.text", title: "MIT license", detail: "Read the open-source license", url: ProjectLinks.license)
|
|
@@ -517,6 +581,121 @@ struct PreferencesPanel: View {
|
|
|
517
581
|
}
|
|
518
582
|
}
|
|
519
583
|
|
|
584
|
+
struct FixedBadge: View {
|
|
585
|
+
let title: String
|
|
586
|
+
|
|
587
|
+
init(_ title: String) { self.title = title }
|
|
588
|
+
|
|
589
|
+
var body: some View {
|
|
590
|
+
Text(title)
|
|
591
|
+
.font(.system(size: 10, weight: .semibold))
|
|
592
|
+
.foregroundStyle(.tertiary)
|
|
593
|
+
.padding(.horizontal, 8).padding(.vertical, 4)
|
|
594
|
+
.background(Color.secondary.opacity(0.08), in: Capsule())
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
struct BrowserlessSetupSheet: View {
|
|
599
|
+
@EnvironmentObject private var model: SyncModel
|
|
600
|
+
@Environment(\.dismiss) private var dismiss
|
|
601
|
+
@State private var token = ""
|
|
602
|
+
@State private var profileName = "browser-cookie-bridge"
|
|
603
|
+
@State private var region = "sfo"
|
|
604
|
+
@State private var onlyDomains = ""
|
|
605
|
+
@State private var cloudConsent = false
|
|
606
|
+
|
|
607
|
+
var body: some View {
|
|
608
|
+
VStack(alignment: .leading, spacing: 17) {
|
|
609
|
+
HStack(alignment: .top, spacing: 12) {
|
|
610
|
+
Image(nsImage: model.browserlessIcon)
|
|
611
|
+
.resizable()
|
|
612
|
+
.scaledToFit()
|
|
613
|
+
.frame(width: 24, height: 24)
|
|
614
|
+
.frame(width: 40, height: 40)
|
|
615
|
+
.background(Theme.accent.opacity(0.11), in: RoundedRectangle(cornerRadius: 11, style: .continuous))
|
|
616
|
+
VStack(alignment: .leading, spacing: 3) {
|
|
617
|
+
Text("Connect Browserless Cloud")
|
|
618
|
+
.font(.system(size: 18, weight: .bold, design: .rounded))
|
|
619
|
+
Text("Optional authenticated-profile upload")
|
|
620
|
+
.font(.system(size: 11.5)).foregroundStyle(.secondary)
|
|
621
|
+
}
|
|
622
|
+
Spacer()
|
|
623
|
+
FixedBadge(model.browserlessConfigured ? "Connected" : "Not connected")
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
VStack(alignment: .leading, spacing: 12) {
|
|
627
|
+
LabeledContent("API token") {
|
|
628
|
+
SecureField(model.browserlessConfigured ? "Saved in Keychain — leave blank to keep" : "Browserless API token", text: $token)
|
|
629
|
+
.textFieldStyle(.roundedBorder)
|
|
630
|
+
.frame(width: 270)
|
|
631
|
+
}
|
|
632
|
+
LabeledContent("Cloud profile") {
|
|
633
|
+
TextField("browser-cookie-bridge", text: $profileName)
|
|
634
|
+
.textFieldStyle(.roundedBorder)
|
|
635
|
+
.frame(width: 270)
|
|
636
|
+
}
|
|
637
|
+
LabeledContent("Region") {
|
|
638
|
+
Picker("", selection: $region) {
|
|
639
|
+
Text("San Francisco (SFO)").tag("sfo")
|
|
640
|
+
Text("London (LON)").tag("lon")
|
|
641
|
+
Text("Amsterdam (AMS)").tag("ams")
|
|
642
|
+
}
|
|
643
|
+
.labelsHidden()
|
|
644
|
+
.frame(width: 270)
|
|
645
|
+
}
|
|
646
|
+
LabeledContent("Only domains") {
|
|
647
|
+
TextField("Optional: example.com, app.example.com", text: $onlyDomains)
|
|
648
|
+
.textFieldStyle(.roundedBorder)
|
|
649
|
+
.frame(width: 270)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
.font(.system(size: 11.5, weight: .medium))
|
|
653
|
+
.padding(14)
|
|
654
|
+
.background(Color(nsColor: .controlBackgroundColor).opacity(0.72), in: RoundedRectangle(cornerRadius: 12, style: .continuous))
|
|
655
|
+
.overlay(RoundedRectangle(cornerRadius: 12, style: .continuous).stroke(Color.primary.opacity(0.08)))
|
|
656
|
+
|
|
657
|
+
HStack(alignment: .top, spacing: 10) {
|
|
658
|
+
Image(systemName: "exclamationmark.cloud.fill")
|
|
659
|
+
.foregroundStyle(Theme.active)
|
|
660
|
+
Text("This destination is not local. Clicking Upload sends cookies, local storage, and IndexedDB from a temporary profile copy to Browserless. Upload only profiles you are authorized to share; use the domain allowlist for sensitive profiles. Background and login sync never perform cloud uploads. CLI telemetry is disabled.")
|
|
661
|
+
.font(.system(size: 10.5))
|
|
662
|
+
.foregroundStyle(.secondary)
|
|
663
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
664
|
+
}
|
|
665
|
+
.padding(12)
|
|
666
|
+
.background(Theme.active.opacity(0.08), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
|
|
667
|
+
|
|
668
|
+
Toggle("I understand the upload contents, have authority to share them, and accept responsibility for the Browserless copy.", isOn: $cloudConsent)
|
|
669
|
+
.toggleStyle(.checkbox)
|
|
670
|
+
.font(.system(size: 10.5, weight: .medium))
|
|
671
|
+
|
|
672
|
+
HStack {
|
|
673
|
+
Link("Get a Browserless API token", destination: URL(string: "https://www.browserless.io/account")!)
|
|
674
|
+
.font(.system(size: 10.5, weight: .medium))
|
|
675
|
+
Link("Privacy", destination: URL(string: "https://www.browserless.io/privacy-policy")!)
|
|
676
|
+
.font(.system(size: 10.5, weight: .medium))
|
|
677
|
+
if model.browserlessConfigured {
|
|
678
|
+
Button("Disconnect", role: .destructive) { model.disconnectBrowserless() }
|
|
679
|
+
}
|
|
680
|
+
Spacer()
|
|
681
|
+
Button("Cancel") { dismiss() }
|
|
682
|
+
Button("Save connection") {
|
|
683
|
+
model.saveBrowserlessSettings(token: token, profileName: profileName, region: region, onlyDomains: onlyDomains)
|
|
684
|
+
}
|
|
685
|
+
.keyboardShortcut(.defaultAction)
|
|
686
|
+
.disabled(profileName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || !cloudConsent)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
.padding(22)
|
|
690
|
+
.frame(width: 520)
|
|
691
|
+
.onAppear {
|
|
692
|
+
profileName = model.browserlessProfileName
|
|
693
|
+
region = model.browserlessRegion
|
|
694
|
+
onlyDomains = model.browserlessOnlyDomains
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
520
699
|
struct ExtensionSetupSheet: View {
|
|
521
700
|
@EnvironmentObject private var model: SyncModel
|
|
522
701
|
@Environment(\.dismiss) private var dismiss
|