@trillboards/edge-sdk 0.2.137 → 0.2.138

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.
@@ -49,7 +49,12 @@ delivered **only by pairing** — it is never baked into the `.pkg`.
49
49
  **Start setup**. Follow **Welcome → Account → Account setup → Plan & Payment**.
50
50
  Sign in with Google or email, choose the account if you manage more than one,
51
51
  accept the current agreements, then choose a plan
52
- and finish secure Stripe checkout. Cancelling checkout preserves account setup;
52
+ and finish secure Stripe checkout. The first browser step opens a **Sense Anything
53
+ setup** tab in the system default browser. Keep it open: Google and checkout
54
+ links originate from this tab so they stay in that browser/profile. **Reopen
55
+ setup tab** is an explicit recovery through the current default browser; it
56
+ revokes the old tab's connection. Agreement review and the authenticated
57
+ recorder/camera wizard open in owned Connector windows on the Mac. Cancelling checkout preserves account setup;
53
58
  use **Back to plans** in the Connector to retry. Existing subscribed accounts
54
59
  resume at recorder setup. The Connector checks subscription access automatically
55
60
  before opening the signed-in Sense Anything camera wizard,
@@ -180,3 +185,18 @@ runs the local model:
180
185
  The pairing endpoint (`POST /v2/earner/cameras/pair`), the config shape, and the
181
186
  local-first sensing runtime are shared across all platforms — only the
182
187
  service-supervisor wrapper differs.
188
+
189
+ ### Browser continuity implementation
190
+
191
+ `installerBrowserSession.ts` serves one loopback-only setup tab. Its capability
192
+ is carried in the initial fragment and tab session storage, then authenticated
193
+ in request headers. Host/origin checks reject rebinding and cross-origin access;
194
+ only one tab owner receives pending actions. Later stages never use the OS opener
195
+ unless the user explicitly requests recovery. Links use a user gesture and
196
+ `noopener noreferrer`; Google and Stripe stay in the real external browser.
197
+
198
+ The Mac shell receives the one-use camera grant over inherited pipe descriptor
199
+ 198, passed explicitly through `pair.command`. It is never placed in logs or the
200
+ progress file. Camera and legal views use separate nonpersistent WebKit stores
201
+ and restricted navigation hosts. The native release build smoke-tests pipe
202
+ delivery, cancellation, and temporary-directory handling.
@@ -1,4 +1,5 @@
1
1
  #import <Cocoa/Cocoa.h>
2
+ #import <WebKit/WebKit.h>
2
3
  #import <errno.h>
3
4
  #import <fcntl.h>
4
5
  #import <signal.h>
@@ -37,8 +38,14 @@ static BOOL acquireSetupInstanceLock(void) {
37
38
  return NO;
38
39
  }
39
40
 
40
- @interface TrillboardsSetupDelegate : NSObject <NSApplicationDelegate>
41
+ @interface TrillboardsSetupDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate, WKNavigationDelegate, WKUIDelegate>
41
42
  @property(nonatomic, strong) NSWindow *window;
43
+ @property(nonatomic, strong) NSFileHandle *setupOutput;
44
+ @property(nonatomic, strong) NSMutableData *setupOutputBuffer;
45
+ @property(nonatomic, strong) NSWindow *cameraWindow;
46
+ @property(nonatomic, strong) WKWebView *cameraWebView;
47
+ @property(nonatomic, strong) NSWindow *legalWindow;
48
+ @property(nonatomic, strong) WKWebView *legalWebView;
42
49
  @property(nonatomic, strong) NSTextField *headline;
43
50
  @property(nonatomic, strong) NSTextField *detail;
44
51
  @property(nonatomic, strong) NSTextField *statusValue;
@@ -163,6 +170,11 @@ static BOOL acquireSetupInstanceLock(void) {
163
170
  - (void)stopPairProcess {
164
171
  pid_t pid = self.pairProcessIdentifier;
165
172
  self.pairProcessIdentifier = 0;
173
+ if (self.setupOutput) {
174
+ self.setupOutput.readabilityHandler = nil;
175
+ [self.setupOutput closeFile];
176
+ self.setupOutput = nil;
177
+ }
166
178
  if (self.setupInput) {
167
179
  [self.setupInput closeFile];
168
180
  self.setupInput = nil;
@@ -206,9 +218,18 @@ static BOOL acquireSetupInstanceLock(void) {
206
218
  return NO;
207
219
  }
208
220
 
221
+ int outputPipe[2];
222
+ if (pipe(outputPipe) != 0) {
223
+ close(logFd); close(inputPipe[0]); close(inputPipe[1]);
224
+ if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
225
+ return NO;
226
+ }
209
227
  posix_spawn_file_actions_t fileActions;
210
228
  posix_spawnattr_t attributes;
211
229
  posix_spawn_file_actions_init(&fileActions);
230
+ posix_spawn_file_actions_adddup2(&fileActions, outputPipe[1], 198);
231
+ posix_spawn_file_actions_addclose(&fileActions, outputPipe[0]);
232
+ posix_spawn_file_actions_addclose(&fileActions, outputPipe[1]);
212
233
  posix_spawn_file_actions_adddup2(&fileActions, logFd, STDOUT_FILENO);
213
234
  posix_spawn_file_actions_adddup2(&fileActions, logFd, STDERR_FILENO);
214
235
  posix_spawn_file_actions_adddup2(&fileActions, inputPipe[0], STDIN_FILENO);
@@ -239,12 +260,24 @@ static BOOL acquireSetupInstanceLock(void) {
239
260
  posix_spawn_file_actions_destroy(&fileActions);
240
261
  close(logFd);
241
262
  close(inputPipe[0]);
263
+ close(outputPipe[1]);
242
264
  if (spawnError != 0) {
243
265
  close(inputPipe[1]);
266
+ close(outputPipe[0]);
244
267
  if (error) *error = [NSError errorWithDomain:NSPOSIXErrorDomain code:spawnError userInfo:nil];
245
268
  return NO;
246
269
  }
247
270
 
271
+ self.setupOutputBuffer = [NSMutableData data];
272
+ self.setupOutput = [[NSFileHandle alloc] initWithFileDescriptor:outputPipe[0] closeOnDealloc:YES];
273
+ __weak typeof(self) outputSelf = self;
274
+ self.setupOutput.readabilityHandler = ^(NSFileHandle *handle) {
275
+ NSData *data = handle.availableData;
276
+ if (data.length == 0) { handle.readabilityHandler = nil; return; }
277
+ dispatch_async(dispatch_get_main_queue(), ^{
278
+ if (outputSelf.setupOutput == handle) [outputSelf receiveSetupOutput:data];
279
+ });
280
+ };
248
281
  self.setupInput = [[NSFileHandle alloc] initWithFileDescriptor:inputPipe[1] closeOnDealloc:YES];
249
282
  self.pairProcessIdentifier = pid;
250
283
  dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC,
@@ -265,6 +298,11 @@ static BOOL acquireSetupInstanceLock(void) {
265
298
  int status = 0;
266
299
  pid_t waited = waitpid(pid, &status, 0);
267
300
  self.pairProcessIdentifier = 0;
301
+ if (self.setupOutput) {
302
+ self.setupOutput.readabilityHandler = nil;
303
+ [self.setupOutput closeFile];
304
+ self.setupOutput = nil;
305
+ }
268
306
  if (self.setupInput) {
269
307
  [self.setupInput closeFile];
270
308
  self.setupInput = nil;
@@ -294,6 +332,7 @@ static BOOL acquireSetupInstanceLock(void) {
294
332
  backing:NSBackingStoreBuffered
295
333
  defer:NO];
296
334
  self.window.title = @"Sense Anything Connector";
335
+ self.window.delegate = self;
297
336
  self.window.minSize = NSMakeSize(760, 540);
298
337
  self.window.maxSize = NSMakeSize(760, 540);
299
338
  [self.window center];
@@ -341,7 +380,7 @@ static BOOL acquireSetupInstanceLock(void) {
341
380
  [root addSubview:self.statusValue];
342
381
 
343
382
  self.googleReopenButton = [[NSButton alloc] initWithFrame:NSMakeRect(360, 219, 218, 28)];
344
- self.googleReopenButton.title = @"Open sign-in again";
383
+ self.googleReopenButton.title = @"Reopen setup tab";
345
384
  self.googleReopenButton.bezelStyle = NSBezelStyleRounded;
346
385
  self.googleReopenButton.target = self;
347
386
  self.googleReopenButton.action = @selector(googleReopenPressed);
@@ -537,7 +576,7 @@ static BOOL acquireSetupInstanceLock(void) {
537
576
  }
538
577
 
539
578
  - (void)googleReopenPressed {
540
- [self sendOnboardingCommand:@{ @"action": @"google_reopen" }];
579
+ [self sendOnboardingCommand:@{ @"action": @"browser_reopen" }];
541
580
  }
542
581
 
543
582
  - (void)googlePressed {
@@ -586,15 +625,15 @@ static BOOL acquireSetupInstanceLock(void) {
586
625
  }
587
626
 
588
627
  - (void)reviewTermsPressed {
589
- [self openURL:@"https://trillboards.com/legal/terms"];
628
+ [self showLegalURL:@"https://trillboards.com/legal/terms"];
590
629
  }
591
630
 
592
631
  - (void)reviewPrivacyPressed {
593
- [self openURL:@"https://trillboards.com/legal/privacy-policy"];
632
+ [self showLegalURL:@"https://trillboards.com/legal/privacy-policy"];
594
633
  }
595
634
 
596
635
  - (void)reviewDpaPressed {
597
- [self openURL:@"https://trillboards.com/legal/data-processing"];
636
+ [self showLegalURL:@"https://trillboards.com/legal/data-processing"];
598
637
  }
599
638
 
600
639
  - (void)termsSubmitPressed {
@@ -651,6 +690,14 @@ static BOOL acquireSetupInstanceLock(void) {
651
690
  self.googleReopenButton.hidden = YES;
652
691
  }
653
692
 
693
+ - (void)windowWillClose:(NSNotification *)notification {
694
+ if (notification.object == self.window) {
695
+ [self.cameraWindow close];
696
+ [self.legalWindow close];
697
+ [NSApp terminate:nil];
698
+ }
699
+ }
700
+
654
701
  - (void)closePressed {
655
702
  [self.window close];
656
703
  }
@@ -756,9 +803,11 @@ static BOOL acquireSetupInstanceLock(void) {
756
803
  [self sendOnboardingCommand:@{ @"action": @"refresh" }];
757
804
  } else if ([self.lastStage isEqualToString:@"waiting_for_account"] ||
758
805
  [self.lastStage isEqualToString:@"account_attention"]) {
759
- [self openURL:self.portalURL];
760
- } else if ([self.lastStage isEqualToString:@"ready"]) {
761
- [self openURL:@"https://app.senseanything.com/sense/cameras"];
806
+ [self sendOnboardingCommand:@{ @"action": @"camera_reopen" }];
807
+ if (self.cameraWindow) [self.cameraWindow makeKeyAndOrderFront:nil];
808
+ else [self showLocalError:@"Camera setup unavailable" message:@"Restart setup to reconnect the camera window to your account."];
809
+ } else if ([@[@"ready", @"account_connected", @"discovering", @"devices_found", @"validating"] containsObject:self.lastStage]) {
810
+ if (self.cameraWindow) [self.cameraWindow makeKeyAndOrderFront:nil];
762
811
  } else if ([self.lastStage isEqualToString:@"local_ai_error"]) {
763
812
  [NSApp terminate:nil];
764
813
  } else if ([self.lastStage isEqualToString:@"welcome"] || [self.lastStage isEqualToString:@"error"]) {
@@ -767,6 +816,9 @@ static BOOL acquireSetupInstanceLock(void) {
767
816
  }
768
817
 
769
818
  - (void)startSetup {
819
+ [self stopPairProcess];
820
+ [self.cameraWindow close]; self.cameraWindow = nil; self.cameraWebView = nil;
821
+ [self.legalWindow close]; self.legalWindow = nil; self.legalWebView = nil;
770
822
  NSString *pairer = [self homePath:@"Library/Trillboards/edge/bin/trillboards-pair"];
771
823
  NSString *stateDirectory = [self homePath:@"Library/Application Support/Trillboards"];
772
824
  NSString *statePath = [stateDirectory stringByAppendingPathComponent:@"setup-progress.json"];
@@ -793,7 +845,7 @@ static BOOL acquireSetupInstanceLock(void) {
793
845
 
794
846
  NSError *error = nil;
795
847
  if (![self launchSetupExecutable:pairer
796
- arguments:@[@"--setup", @"--status-file", statePath]
848
+ arguments:@[@"--setup", @"--status-file", statePath, @"--native-ui-fd", @"198"]
797
849
  logPath:logPath
798
850
  error:&error]) {
799
851
  [self showLocalError:@"Could not start setup" message:@"Reinstall the Sense Anything Connector, then try again."];
@@ -902,6 +954,7 @@ static BOOL acquireSetupInstanceLock(void) {
902
954
  self.googleReopenButton.hidden = NO;
903
955
  } else if ([stage isEqualToString:@"onboarding_checkout"]) {
904
956
  self.statusValue.stringValue = @"Waiting for Stripe";
957
+ self.googleReopenButton.hidden = NO;
905
958
  } else if ([stage isEqualToString:@"onboarding_install"]) {
906
959
  self.statusValue.stringValue = @"Onboarding complete";
907
960
  } else if (cameraCount.integerValue > 0) {
@@ -956,10 +1009,13 @@ static BOOL acquireSetupInstanceLock(void) {
956
1009
  self.actionButton.title = @"Preparing cameras…";
957
1010
  self.actionButton.enabled = NO;
958
1011
  } else if ([stage isEqualToString:@"waiting_for_account"]) {
959
- self.actionButton.title = @"Open Sense Anything";
1012
+ self.actionButton.title = @"Open camera setup";
960
1013
  self.actionButton.enabled = YES;
961
1014
  } else if ([stage isEqualToString:@"account_attention"]) {
962
- self.actionButton.title = @"Switch account";
1015
+ self.actionButton.title = @"Open camera setup";
1016
+ self.actionButton.enabled = YES;
1017
+ } else if ([@[@"account_connected", @"discovering", @"devices_found", @"validating"] containsObject:stage] && self.cameraWindow) {
1018
+ self.actionButton.title = @"Open camera setup";
963
1019
  self.actionButton.enabled = YES;
964
1020
  } else if ([stage isEqualToString:@"local_ai_error"]) {
965
1021
  self.actionButton.title = @"Close";
@@ -1013,6 +1069,98 @@ static BOOL acquireSetupInstanceLock(void) {
1013
1069
  }
1014
1070
  }
1015
1071
 
1072
+ - (void)receiveSetupOutput:(NSData *)data {
1073
+ if (self.terminating || !self.setupOutput) return;
1074
+ [self.setupOutputBuffer appendData:data];
1075
+ if (self.setupOutputBuffer.length > 16384) {
1076
+ [self.setupOutputBuffer setLength:0];
1077
+ [self showLocalError:@"Camera setup unavailable" message:@"Restart setup to reconnect the camera window."];
1078
+ return;
1079
+ }
1080
+ NSData *newline = [@"\n" dataUsingEncoding:NSUTF8StringEncoding];
1081
+ while (YES) {
1082
+ NSRange boundary = [self.setupOutputBuffer rangeOfData:newline options:0 range:NSMakeRange(0, self.setupOutputBuffer.length)];
1083
+ if (boundary.location == NSNotFound) break;
1084
+ NSData *line = [self.setupOutputBuffer subdataWithRange:NSMakeRange(0, boundary.location)];
1085
+ [self.setupOutputBuffer replaceBytesInRange:NSMakeRange(0, boundary.location + 1) withBytes:NULL length:0];
1086
+ NSDictionary *message = [NSJSONSerialization JSONObjectWithData:line options:0 error:nil];
1087
+ if (![message isKindOfClass:NSDictionary.class] || ![message[@"action"] isEqual:@"camera"] || ![message[@"url"] isKindOfClass:NSString.class]) continue;
1088
+ NSURL *url = [NSURL URLWithString:message[@"url"]];
1089
+ if (![url.scheme isEqual:@"https"] || ![url.host isEqual:@"app.senseanything.com"] || ![url.path isEqual:@"/sense/installer-return"]) {
1090
+ [self showLocalError:@"Camera setup unavailable" message:@"The camera setup destination was not recognized."];
1091
+ continue;
1092
+ }
1093
+ if (!self.cameraWebView) {
1094
+ self.cameraWebView = [self setupWebView];
1095
+ self.cameraWindow = [self contentWindow:@"Sense Anything — Recorder and cameras" webView:self.cameraWebView];
1096
+ }
1097
+ [self.cameraWebView loadRequest:[NSURLRequest requestWithURL:url]];
1098
+ [self.cameraWindow makeKeyAndOrderFront:nil];
1099
+ }
1100
+ }
1101
+
1102
+ - (WKWebView *)setupWebView {
1103
+ WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
1104
+ configuration.websiteDataStore = WKWebsiteDataStore.nonPersistentDataStore;
1105
+ WKWebView *view = [[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 960, 700) configuration:configuration];
1106
+ view.navigationDelegate = self;
1107
+ view.UIDelegate = self;
1108
+ return view;
1109
+ }
1110
+
1111
+ - (NSWindow *)contentWindow:(NSString *)title webView:(WKWebView *)view {
1112
+ NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 960, 700)
1113
+ styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable
1114
+ backing:NSBackingStoreBuffered defer:NO];
1115
+ window.title = title;
1116
+ window.releasedWhenClosed = NO;
1117
+ window.contentView = view;
1118
+ // An owned window stays with the installer rather than activating whichever
1119
+ // unrelated browser window happens to be on another desktop.
1120
+ [self.window addChildWindow:window ordered:NSWindowAbove];
1121
+ NSRect visible = (self.window.screen ?: NSScreen.mainScreen).visibleFrame;
1122
+ NSRect frame = window.frame;
1123
+ frame.size.width = MIN(frame.size.width, NSWidth(visible) - 40);
1124
+ frame.size.height = MIN(frame.size.height, NSHeight(visible) - 40);
1125
+ frame.origin.x = MAX(NSMinX(visible) + 20, MIN(NSMinX(self.window.frame) + 30, NSMaxX(visible) - NSWidth(frame) - 20));
1126
+ frame.origin.y = MAX(NSMinY(visible) + 20, MIN(NSMinY(self.window.frame) + 20, NSMaxY(visible) - NSHeight(frame) - 20));
1127
+ [window setFrame:frame display:NO];
1128
+ return window;
1129
+ }
1130
+
1131
+ - (void)showLegalURL:(NSString *)string {
1132
+ if (!self.legalWebView) {
1133
+ self.legalWebView = [self setupWebView];
1134
+ self.legalWindow = [self contentWindow:@"Sense Anything — Agreements" webView:self.legalWebView];
1135
+ }
1136
+ [self.legalWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:string]]];
1137
+ [self.legalWindow makeKeyAndOrderFront:nil];
1138
+ }
1139
+
1140
+ - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)action
1141
+ decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
1142
+ NSURL *url = action.request.URL;
1143
+ BOOL allowedHost = webView == self.cameraWebView
1144
+ ? [url.host isEqual:@"app.senseanything.com"]
1145
+ : ([url.host isEqual:@"trillboards.com"] || [url.host isEqual:@"www.trillboards.com"]);
1146
+ BOOL allowed = [url.scheme isEqual:@"https"] && allowedHost;
1147
+ if (allowed && !action.targetFrame) {
1148
+ [webView loadRequest:action.request];
1149
+ decisionHandler(WKNavigationActionPolicyCancel);
1150
+ return;
1151
+ }
1152
+ decisionHandler(allowed ? WKNavigationActionPolicyAllow : WKNavigationActionPolicyCancel);
1153
+ }
1154
+
1155
+ - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
1156
+ (void)webView; (void)navigation;
1157
+ if (error.code == NSURLErrorCancelled) return;
1158
+ NSAlert *alert = [[NSAlert alloc] init];
1159
+ alert.messageText = @"Could not load this setup page";
1160
+ alert.informativeText = @"Check your internet connection, then reopen the page from the Connector.";
1161
+ [alert beginSheetModalForWindow:self.window completionHandler:nil];
1162
+ }
1163
+
1016
1164
  - (void)openURL:(NSString *)string {
1017
1165
  NSURL *url = [NSURL URLWithString:string];
1018
1166
  if (url) [NSWorkspace.sharedWorkspace openURL:url];
@@ -1068,6 +1216,35 @@ static int runCancellationSmokeTest(void) {
1068
1216
  return 0;
1069
1217
  }
1070
1218
 
1219
+ @interface SetupPipeSmokeDelegate : TrillboardsSetupDelegate
1220
+ @property(nonatomic, strong) NSMutableData *received;
1221
+ @end
1222
+ @implementation SetupPipeSmokeDelegate
1223
+ - (void)receiveSetupOutput:(NSData *)data { [self.received appendData:data]; }
1224
+ @end
1225
+
1226
+ static int runNativePipeSmokeTest(NSString *nodePath) {
1227
+ SetupPipeSmokeDelegate *delegate = [[SetupPipeSmokeDelegate alloc] init];
1228
+ delegate.received = [NSMutableData data];
1229
+ NSError *error = nil;
1230
+ if (![delegate launchSetupExecutable:@"/bin/bash"
1231
+ arguments:@[@"-c", @"exec \"$1\" -e \"$2\"", @"pipe-smoke", nodePath,
1232
+ @"require('fs').writeSync(198, 'native-ui-pipe-ok\\n'); setTimeout(() => {}, 10000)"]
1233
+ logPath:@"/dev/null" error:&error]) return 1;
1234
+ NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:3];
1235
+ while (delegate.received.length == 0 && deadline.timeIntervalSinceNow > 0) {
1236
+ [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.02]];
1237
+ }
1238
+ NSString *received = [[NSString alloc] initWithData:delegate.received encoding:NSUTF8StringEncoding];
1239
+ [delegate stopPairProcess];
1240
+ if (![received isEqualToString:@"native-ui-pipe-ok\n"]) {
1241
+ fprintf(stderr, "Native setup private UI pipe did not deliver its message.\n");
1242
+ return 1;
1243
+ }
1244
+ fprintf(stdout, "Sense Anything Connector private UI pipe smoke test passed.\n");
1245
+ return 0;
1246
+ }
1247
+
1071
1248
  static int runTemporaryDirectorySmokeTest(void) {
1072
1249
  const char *environmentTemporaryDirectory = getenv("TMPDIR");
1073
1250
  if (environmentTemporaryDirectory != NULL &&
@@ -1108,6 +1285,9 @@ int main(int argc, const char *argv[]) {
1108
1285
  // setup app or its pairing child retain that now-stale TMPDIR.
1109
1286
  normalizeTemporaryDirectory();
1110
1287
  @autoreleasepool {
1288
+ if (argc == 3 && [[NSString stringWithUTF8String:argv[1]] isEqualToString:@"--smoke-test-ui-pipe"]) {
1289
+ return runNativePipeSmokeTest([NSString stringWithUTF8String:argv[2]]);
1290
+ }
1111
1291
  if (argc == 2 && [[NSString stringWithUTF8String:argv[1]] isEqualToString:@"--smoke-test-cancel"]) {
1112
1292
  return runCancellationSmokeTest();
1113
1293
  }
@@ -492,7 +492,7 @@ cp "$PKG_ROOT/deploy/branding/SenseAnythingConnector.icns" \
492
492
  "$SETUP_APP_RESOURCES/SenseAnythingConnector.icns"
493
493
  log "Building native Sense Anything Connector.app…"
494
494
  if [ "$UNIVERSAL" -eq 1 ]; then
495
- clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch arm64 -arch x86_64 -framework Cocoa \
495
+ clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch arm64 -arch x86_64 -framework Cocoa -framework WebKit \
496
496
  "$PKG_ROOT/deploy/macos/TrillboardsSetupApp.m" -o "$SETUP_APP_BIN" \
497
497
  || die "native setup app universal build failed"
498
498
  else
@@ -500,13 +500,15 @@ else
500
500
  arm64) SETUP_ARCH="arm64";;
501
501
  x64) SETUP_ARCH="x86_64";;
502
502
  esac
503
- clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch "$SETUP_ARCH" -framework Cocoa \
503
+ clang -fobjc-arc -Wall -Wextra -Werror -O2 -mmacosx-version-min=12.0 -arch "$SETUP_ARCH" -framework Cocoa -framework WebKit \
504
504
  "$PKG_ROOT/deploy/macos/TrillboardsSetupApp.m" -o "$SETUP_APP_BIN" \
505
505
  || die "native setup app build failed"
506
506
  fi
507
507
  chmod +x "$SETUP_APP_BIN"
508
508
  "$SETUP_APP_BIN" --smoke-test-cancel \
509
509
  || die "native setup app cancellation smoke test failed"
510
+ "$SETUP_APP_BIN" --smoke-test-ui-pipe "$PAYLOAD/bin/node" \
511
+ || die "native setup private UI pipe smoke test failed"
510
512
  INSTALLER_TMP="$STAGE/PKInstallSandbox.trillboards-test/tmp"
511
513
  mkdir -p "$INSTALLER_TMP"
512
514
  TMPDIR="$INSTALLER_TMP" "$SETUP_APP_BIN" --smoke-test-tempdir \
@@ -41,6 +41,7 @@ LABEL="com.trillboards.edge"
41
41
  API_BASE="${TRILLBOARDS_API_BASE:-}"
42
42
  HEADLESS_CODE=""
43
43
  UI_STATUS_FILE=""
44
+ NATIVE_UI_ARGS=()
44
45
 
45
46
  # Parse args. First non-flag positional == the code (headless mode).
46
47
  while [ $# -gt 0 ]; do
@@ -48,6 +49,7 @@ while [ $# -gt 0 ]; do
48
49
  --api-base) API_BASE="$2"; shift 2;;
49
50
  --setup) SETUP_MODE=1; shift;;
50
51
  --status-file) UI_STATUS_FILE="$2"; shift 2;;
52
+ --native-ui-fd) NATIVE_UI_ARGS=(--native-ui-fd "$2"); shift 2;;
51
53
  -h|--help) sed -n '2,20p' "${BASH_SOURCE[0]}"; exit 0;;
52
54
  *) if [ -z "$HEADLESS_CODE" ]; then HEADLESS_CODE="$1"; fi; shift;;
53
55
  esac
@@ -368,7 +370,7 @@ if [ "${SETUP_MODE:-0}" = "1" ]; then
368
370
  # says legal + entitlement are current, so cameras cannot open early.
369
371
  # The same process keeps the authenticated session in memory through camera
370
372
  # setup and mints a one-use signed-in browser handoff at first launch.
371
- "$LAUNCHER" installer-onboarding --config "$CONFIG" --status-file "$UI_STATUS_FILE"
373
+ "$LAUNCHER" installer-onboarding --config "$CONFIG" --status-file "$UI_STATUS_FILE" "${NATIVE_UI_ARGS[@]}"
372
374
  else
373
375
  "$LAUNCHER" setup --config "$CONFIG"
374
376
  fi
package/dist/EdgeAgent.js CHANGED
@@ -44,7 +44,7 @@ const edge_ads_1 = require("@trillboards/edge-ads");
44
44
  const CommandRouter_1 = require("./CommandRouter");
45
45
  const config_1 = require("./config");
46
46
  const dataDir_1 = require("./dataDir");
47
- const SDK_VERSION = '0.2.137';
47
+ const SDK_VERSION = '0.2.138';
48
48
  class EdgeAgent extends events_1.EventEmitter {
49
49
  constructor(config, deps) {
50
50
  super();
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA4dA;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAKvD;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAW/E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,kBAAkB,EAAE,MAAM,EAC1B,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,KAAK,IAC/B,GAChC,IAAI,CAWN;AAUD,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CASvE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,QAAQ,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAA2C,EACpE,MAAM,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAiC,GACvD;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAyB3F;AAgHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+M7D"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAsgBA;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAKvD;AAED;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAAC,kBAAkB,EAAE,MAAM,GAAG,MAAM,CAW/E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,kBAAkB,EAAE,MAAM,EAC1B,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,KAAK,IAC/B,GAChC,IAAI,CAWN;AAUD,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CASvE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,UAAU,EAAE,MAAM,EAClB,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,QAAQ,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAA2C,EACpE,MAAM,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAiC,GACvD;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAyB3F;AAgHD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+M7D"}
package/dist/cli.js CHANGED
@@ -239,22 +239,76 @@ async function main() {
239
239
  process.exit(2);
240
240
  return;
241
241
  }
242
- const rc = await runInstallerOnboardingMode({
243
- apiBaseUrl: getArg(onboardingArgs, '--api-base') || process.env.TRILLBOARDS_API_BASE || 'https://api.trillboards.com',
244
- progress: createSetupProgressReporter(statusFile),
245
- afterComplete: async (onboardingClient) => runSetupMode({
242
+ const { InstallerBrowserSession } = require('./installerBrowserSession');
243
+ const browserSession = new InstallerBrowserSession(openSetupPortal);
244
+ const nativeUiFd = getArg(onboardingArgs, '--native-ui-fd');
245
+ const nativeCameraView = process.platform === 'darwin' && nativeUiFd === '198';
246
+ // A private inherited pipe carries one-use grants directly to the native
247
+ // UI. They must never be written to logs or the progress file.
248
+ const reportSetup = createSetupProgressReporter(statusFile);
249
+ const cameraProgress = (update) => {
250
+ if (nativeCameraView && update.stage === 'waiting_for_account') {
251
+ const { portalUrl: _portalUrl, ...safe } = update;
252
+ reportSetup({ ...safe, title: 'Connect your recorder',
253
+ message: 'Continue in the camera setup window inside this Connector.' });
254
+ }
255
+ else
256
+ reportSetup(update);
257
+ };
258
+ let rc;
259
+ try {
260
+ rc = await runInstallerOnboardingMode({
246
261
  apiBaseUrl: getArg(onboardingArgs, '--api-base') || process.env.TRILLBOARDS_API_BASE || 'https://api.trillboards.com',
247
- portalBaseUrl: getArg(onboardingArgs, '--portal-base') || process.env.TRILLBOARDS_PORTAL_BASE || DEFAULT_PORTAL_BASE_URL,
248
- configPath: getArg(onboardingArgs, '--config') || 'trillboards.config.json',
249
- connectorVersion: VERSION,
250
- ffmpegPath: (0, runtimePaths_1.resolveFfmpegBinaryPath)(getArg(onboardingArgs, '--ffmpeg')),
251
- progress: createSetupProgressReporter(statusFile),
252
- openPortal: async (cameraUrl) => {
253
- const handoffUrl = await onboardingClient.createPortalHandoff(cameraUrl);
254
- return openSetupPortal(handoffUrl);
262
+ progress: reportSetup,
263
+ openBrowser: (url) => browserSession.open(url),
264
+ reopenBrowser: () => browserSession.reopen(),
265
+ clearBrowserStep: () => browserSession.clear(),
266
+ afterComplete: async (onboardingClient) => {
267
+ const { InstallerCameraHandoff } = require('./installerCameraHandoff');
268
+ const handoff = new InstallerCameraHandoff((url) => onboardingClient.createPortalHandoff(url), async (url) => {
269
+ if (nativeCameraView) {
270
+ fs.writeSync(198, JSON.stringify({ action: 'camera', url }) + '\n');
271
+ return true;
272
+ }
273
+ return browserSession.open(url);
274
+ });
275
+ const commands = require('readline').createInterface({ input: process.stdin });
276
+ commands.on('line', (line) => {
277
+ let command;
278
+ try {
279
+ command = JSON.parse(line);
280
+ }
281
+ catch {
282
+ return;
283
+ }
284
+ if (command?.action === 'camera_reopen') {
285
+ void handoff.reopen().catch(() => {
286
+ cameraProgress({ stage: 'account_attention', title: 'Camera setup unavailable',
287
+ message: 'Check your connection, then open camera setup again.', recoverable: true });
288
+ });
289
+ }
290
+ });
291
+ try {
292
+ return await runSetupMode({
293
+ apiBaseUrl: getArg(onboardingArgs, '--api-base') || process.env.TRILLBOARDS_API_BASE || 'https://api.trillboards.com',
294
+ portalBaseUrl: getArg(onboardingArgs, '--portal-base') || process.env.TRILLBOARDS_PORTAL_BASE || DEFAULT_PORTAL_BASE_URL,
295
+ configPath: getArg(onboardingArgs, '--config') || 'trillboards.config.json',
296
+ connectorVersion: VERSION,
297
+ ffmpegPath: (0, runtimePaths_1.resolveFfmpegBinaryPath)(getArg(onboardingArgs, '--ffmpeg')),
298
+ progress: cameraProgress,
299
+ refreshPortalOnCodeRenewal: true,
300
+ openPortal: (cameraUrl) => handoff.open(cameraUrl),
301
+ });
302
+ }
303
+ finally {
304
+ commands.close();
305
+ }
255
306
  },
256
- }),
257
- });
307
+ });
308
+ }
309
+ finally {
310
+ await browserSession.close();
311
+ }
258
312
  process.exit(rc);
259
313
  break;
260
314
  }