@agent-native/core 0.15.2 → 0.15.4
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/client/dev-mode.d.ts +14 -0
- package/dist/client/dev-mode.d.ts.map +1 -0
- package/dist/client/dev-mode.js +14 -0
- package/dist/client/dev-mode.js.map +1 -0
- package/dist/client/extensions/EmbeddedTool.d.ts +20 -0
- package/dist/client/extensions/EmbeddedTool.d.ts.map +1 -0
- package/dist/client/extensions/EmbeddedTool.js +199 -0
- package/dist/client/extensions/EmbeddedTool.js.map +1 -0
- package/dist/client/extensions/ToolEditor.d.ts +5 -0
- package/dist/client/extensions/ToolEditor.d.ts.map +1 -0
- package/dist/client/extensions/ToolEditor.js +129 -0
- package/dist/client/extensions/ToolEditor.js.map +1 -0
- package/dist/client/extensions/ToolViewer.d.ts +5 -0
- package/dist/client/extensions/ToolViewer.d.ts.map +1 -0
- package/dist/client/extensions/ToolViewer.js +400 -0
- package/dist/client/extensions/ToolViewer.js.map +1 -0
- package/dist/client/extensions/ToolViewerPage.d.ts +2 -0
- package/dist/client/extensions/ToolViewerPage.d.ts.map +1 -0
- package/dist/client/extensions/ToolViewerPage.js +24 -0
- package/dist/client/extensions/ToolViewerPage.js.map +1 -0
- package/dist/client/extensions/ToolsListPage.d.ts +2 -0
- package/dist/client/extensions/ToolsListPage.d.ts.map +1 -0
- package/dist/client/extensions/ToolsListPage.js +67 -0
- package/dist/client/extensions/ToolsListPage.js.map +1 -0
- package/dist/client/extensions/ToolsSidebarSection.d.ts +2 -0
- package/dist/client/extensions/ToolsSidebarSection.d.ts.map +1 -0
- package/dist/client/extensions/ToolsSidebarSection.js +236 -0
- package/dist/client/extensions/ToolsSidebarSection.js.map +1 -0
- package/dist/client/extensions/tool-order.d.ts +7 -0
- package/dist/client/extensions/tool-order.d.ts.map +1 -0
- package/dist/client/extensions/tool-order.js +47 -0
- package/dist/client/extensions/tool-order.js.map +1 -0
- package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
- package/dist/client/settings/useBuilderStatus.js +137 -20
- package/dist/client/settings/useBuilderStatus.js.map +1 -1
- package/dist/client/settings/useBuilderStatus.spec.js +28 -0
- package/dist/client/settings/useBuilderStatus.spec.js.map +1 -1
- package/dist/client/tools/EmbeddedTool.d.ts +20 -0
- package/dist/client/tools/EmbeddedTool.d.ts.map +1 -0
- package/dist/client/tools/EmbeddedTool.js +199 -0
- package/dist/client/tools/EmbeddedTool.js.map +1 -0
- package/dist/client/tools/ExtensionSlot.d.ts +27 -0
- package/dist/client/tools/ExtensionSlot.d.ts.map +1 -0
- package/dist/client/tools/ExtensionSlot.js +96 -0
- package/dist/client/tools/ExtensionSlot.js.map +1 -0
- package/dist/client/tools/ToolEditor.d.ts +5 -0
- package/dist/client/tools/ToolEditor.d.ts.map +1 -0
- package/dist/client/tools/ToolEditor.js +129 -0
- package/dist/client/tools/ToolEditor.js.map +1 -0
- package/dist/client/tools/ToolViewer.d.ts +5 -0
- package/dist/client/tools/ToolViewer.d.ts.map +1 -0
- package/dist/client/tools/ToolViewer.js +400 -0
- package/dist/client/tools/ToolViewer.js.map +1 -0
- package/dist/client/tools/ToolViewerPage.d.ts +2 -0
- package/dist/client/tools/ToolViewerPage.d.ts.map +1 -0
- package/dist/client/tools/ToolViewerPage.js +24 -0
- package/dist/client/tools/ToolViewerPage.js.map +1 -0
- package/dist/client/tools/ToolsListPage.d.ts +2 -0
- package/dist/client/tools/ToolsListPage.d.ts.map +1 -0
- package/dist/client/tools/ToolsListPage.js +67 -0
- package/dist/client/tools/ToolsListPage.js.map +1 -0
- package/dist/client/tools/ToolsSidebarSection.d.ts +2 -0
- package/dist/client/tools/ToolsSidebarSection.d.ts.map +1 -0
- package/dist/client/tools/ToolsSidebarSection.js +236 -0
- package/dist/client/tools/ToolsSidebarSection.js.map +1 -0
- package/dist/client/tools/iframe-bridge.d.ts +38 -0
- package/dist/client/tools/iframe-bridge.d.ts.map +1 -0
- package/dist/client/tools/iframe-bridge.js +207 -0
- package/dist/client/tools/iframe-bridge.js.map +1 -0
- package/dist/client/tools/index.d.ts +8 -0
- package/dist/client/tools/index.d.ts.map +1 -0
- package/dist/client/tools/index.js +8 -0
- package/dist/client/tools/index.js.map +1 -0
- package/dist/client/tools/tool-order.d.ts +7 -0
- package/dist/client/tools/tool-order.d.ts.map +1 -0
- package/dist/client/tools/tool-order.js +47 -0
- package/dist/client/tools/tool-order.js.map +1 -0
- package/dist/server/auth.d.ts +2 -2
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +20 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-browser.d.ts +7 -1
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +45 -4
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +67 -4
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/google-auth-mode.d.ts +3 -3
- package/dist/server/google-auth-mode.js.map +1 -1
- package/dist/server/google-auth-plugin.d.ts +3 -2
- package/dist/server/google-auth-plugin.d.ts.map +1 -1
- package/dist/server/google-auth-plugin.js +35 -8
- package/dist/server/google-auth-plugin.js.map +1 -1
- package/dist/server/local-migration.d.ts +41 -0
- package/dist/server/local-migration.d.ts.map +1 -0
- package/dist/server/local-migration.js +235 -0
- package/dist/server/local-migration.js.map +1 -0
- package/dist/server/onboarding-html.d.ts +2 -2
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +35 -8
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/tools/actions.d.ts +3 -0
- package/dist/tools/actions.d.ts.map +1 -0
- package/dist/tools/actions.js +272 -0
- package/dist/tools/actions.js.map +1 -0
- package/dist/tools/fetch-tool.d.ts +23 -0
- package/dist/tools/fetch-tool.d.ts.map +1 -0
- package/dist/tools/fetch-tool.js +178 -0
- package/dist/tools/fetch-tool.js.map +1 -0
- package/dist/tools/html-shell.d.ts +45 -0
- package/dist/tools/html-shell.d.ts.map +1 -0
- package/dist/tools/html-shell.js +514 -0
- package/dist/tools/html-shell.js.map +1 -0
- package/dist/tools/proxy-security.d.ts +12 -0
- package/dist/tools/proxy-security.d.ts.map +1 -0
- package/dist/tools/proxy-security.js +158 -0
- package/dist/tools/proxy-security.js.map +1 -0
- package/dist/tools/routes.d.ts +2 -0
- package/dist/tools/routes.d.ts.map +1 -0
- package/dist/tools/routes.js +627 -0
- package/dist/tools/routes.js.map +1 -0
- package/dist/tools/schema.d.ts +664 -0
- package/dist/tools/schema.d.ts.map +1 -0
- package/dist/tools/schema.js +146 -0
- package/dist/tools/schema.js.map +1 -0
- package/dist/tools/slots/routes.d.ts +15 -0
- package/dist/tools/slots/routes.d.ts.map +1 -0
- package/dist/tools/slots/routes.js +94 -0
- package/dist/tools/slots/routes.js.map +1 -0
- package/dist/tools/slots/schema.d.ts +303 -0
- package/dist/tools/slots/schema.d.ts.map +1 -0
- package/dist/tools/slots/schema.js +76 -0
- package/dist/tools/slots/schema.js.map +1 -0
- package/dist/tools/slots/store.d.ts +66 -0
- package/dist/tools/slots/store.d.ts.map +1 -0
- package/dist/tools/slots/store.js +227 -0
- package/dist/tools/slots/store.js.map +1 -0
- package/dist/tools/store.d.ts +40 -0
- package/dist/tools/store.d.ts.map +1 -0
- package/dist/tools/store.js +193 -0
- package/dist/tools/store.js.map +1 -0
- package/dist/tools/theme.d.ts +2 -0
- package/dist/tools/theme.d.ts.map +1 -0
- package/dist/tools/theme.js +67 -0
- package/dist/tools/theme.js.map +1 -0
- package/dist/tools/url-safety.d.ts +24 -0
- package/dist/tools/url-safety.d.ts.map +1 -0
- package/dist/tools/url-safety.js +224 -0
- package/dist/tools/url-safety.js.map +1 -0
- package/package.json +1 -1
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* the user stays on the current page.
|
|
7
7
|
* - `'redirect'`: full-page redirect to Google. Simpler, more reliable on
|
|
8
8
|
* mobile / inside in-app browsers / inside Electron.
|
|
9
|
-
* - `'auto'` (default): popup in normal browsers; redirect in
|
|
10
|
-
*
|
|
11
|
-
* `X-Frame-Options: DENY`).
|
|
9
|
+
* - `'auto'` (default): popup in normal browsers; redirect in Agent Native
|
|
10
|
+
* Desktop; redirect in top-level Builder preview/editor pages; popup inside
|
|
11
|
+
* Builder iframes (a redirect there hits Google's `X-Frame-Options: DENY`).
|
|
12
12
|
*/
|
|
13
13
|
export type GoogleAuthMode = "auto" | "popup" | "redirect";
|
|
14
14
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-auth-mode.js","sourceRoot":"","sources":["../../src/server/google-auth-mode.ts"],"names":[],"mappings":"AAcA,MAAM,KAAK,GAAgC,IAAI,GAAG,CAAC;IACjD,MAAM;IACN,OAAO;IACP,UAAU;CACX,CAAC,CAAC;AAEH,SAAS,OAAO;IACd,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACtE,OAAO,KAAK,CAAC,GAAG,CAAC,GAAqB,CAAC,CAAC,CAAC,CAAE,GAAsB,CAAC,CAAC,CAAC,SAAS,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAuB;IAC3D,IAAI,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAC/C,OAAO,OAAO,EAAE,IAAI,MAAM,CAAC;AAC7B,CAAC","sourcesContent":["/**\n * Google sign-in flow selection.\n *\n * - `'popup'`: open Google in a popup window; the parent polls the\n * `desktop-exchange` endpoint to retrieve the session token. Better UX —\n * the user stays on the current page.\n * - `'redirect'`: full-page redirect to Google. Simpler, more reliable on\n * mobile / inside in-app browsers / inside Electron.\n * - `'auto'` (default): popup in normal browsers; redirect in
|
|
1
|
+
{"version":3,"file":"google-auth-mode.js","sourceRoot":"","sources":["../../src/server/google-auth-mode.ts"],"names":[],"mappings":"AAcA,MAAM,KAAK,GAAgC,IAAI,GAAG,CAAC;IACjD,MAAM;IACN,OAAO;IACP,UAAU;CACX,CAAC,CAAC;AAEH,SAAS,OAAO;IACd,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACtE,OAAO,KAAK,CAAC,GAAG,CAAC,GAAqB,CAAC,CAAC,CAAC,CAAE,GAAsB,CAAC,CAAC,CAAC,SAAS,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAuB;IAC3D,IAAI,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAC/C,OAAO,OAAO,EAAE,IAAI,MAAM,CAAC;AAC7B,CAAC","sourcesContent":["/**\n * Google sign-in flow selection.\n *\n * - `'popup'`: open Google in a popup window; the parent polls the\n * `desktop-exchange` endpoint to retrieve the session token. Better UX —\n * the user stays on the current page.\n * - `'redirect'`: full-page redirect to Google. Simpler, more reliable on\n * mobile / inside in-app browsers / inside Electron.\n * - `'auto'` (default): popup in normal browsers; redirect in Agent Native\n * Desktop; redirect in top-level Builder preview/editor pages; popup inside\n * Builder iframes (a redirect there hits Google's `X-Frame-Options: DENY`).\n */\nexport type GoogleAuthMode = \"auto\" | \"popup\" | \"redirect\";\n\nconst VALID: ReadonlySet<GoogleAuthMode> = new Set([\n \"auto\",\n \"popup\",\n \"redirect\",\n]);\n\nfunction fromEnv(): GoogleAuthMode | undefined {\n const raw = (process.env.GOOGLE_AUTH_MODE || \"\").trim().toLowerCase();\n return VALID.has(raw as GoogleAuthMode) ? (raw as GoogleAuthMode) : undefined;\n}\n\n/**\n * Resolve the effective sign-in flow.\n *\n * Priority: explicit option > `GOOGLE_AUTH_MODE` env var > `'auto'`.\n */\nexport function resolveGoogleAuthMode(option?: GoogleAuthMode): GoogleAuthMode {\n if (option && VALID.has(option)) return option;\n return fromEnv() ?? \"auto\";\n}\n"]}
|
|
@@ -5,8 +5,9 @@ export interface GoogleAuthPluginOptions {
|
|
|
5
5
|
publicPaths?: string[];
|
|
6
6
|
/**
|
|
7
7
|
* Google sign-in flow: `'popup'`, `'redirect'`, or `'auto'` (default).
|
|
8
|
-
* Falls back to `GOOGLE_AUTH_MODE` env var, then `'auto'`. Builder
|
|
9
|
-
* preview/editor surfaces
|
|
8
|
+
* Falls back to `GOOGLE_AUTH_MODE` env var, then `'auto'`. Builder
|
|
9
|
+
* iframes use popup; top-level Builder preview/editor surfaces use
|
|
10
|
+
* redirect.
|
|
10
11
|
*/
|
|
11
12
|
googleAuthMode?: GoogleAuthMode;
|
|
12
13
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-auth-plugin.d.ts","sourceRoot":"","sources":["../../src/server/google-auth-plugin.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAE/B,KAAK,cAAc,GAAG,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE9D,MAAM,WAAW,uBAAuB;IACtC,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB
|
|
1
|
+
{"version":3,"file":"google-auth-plugin.d.ts","sourceRoot":"","sources":["../../src/server/google-auth-plugin.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAE/B,KAAK,cAAc,GAAG,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE9D,MAAM,WAAW,uBAAuB;IACtC,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AA2aD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,CAAC,EAAE,uBAAuB,GAChC,cAAc,CAYhB"}
|
|
@@ -100,6 +100,11 @@ function getGoogleLoginHtml(googleAuthMode) {
|
|
|
100
100
|
var origin = __anIsBuilderPreview() ? __anConfiguredOAuthOrigin() : '';
|
|
101
101
|
return origin ? origin + path : __anPath(path);
|
|
102
102
|
}
|
|
103
|
+
function __anGoogleAuthUrlPath() {
|
|
104
|
+
return __anIsBuilderPreview()
|
|
105
|
+
? __anAuthPath('/_agent-native/google/auth-url')
|
|
106
|
+
: __anPath('/_agent-native/google/auth-url');
|
|
107
|
+
}
|
|
103
108
|
function __anBuilderPreviewReturnOrigin() {
|
|
104
109
|
var candidates = [window.location.href, document.referrer || ''];
|
|
105
110
|
try {
|
|
@@ -161,8 +166,23 @@ function getGoogleLoginHtml(googleAuthMode) {
|
|
|
161
166
|
var origin = __anWorkspaceGatewayReturnOrigin();
|
|
162
167
|
return origin ? origin + path : path;
|
|
163
168
|
}
|
|
164
|
-
function
|
|
169
|
+
function __anSessionBridgeUrl(ret, sessionToken) {
|
|
170
|
+
try {
|
|
171
|
+
var url = new URL(ret || window.location.pathname + window.location.search, window.location.origin);
|
|
172
|
+
url.searchParams.set('_session', sessionToken);
|
|
173
|
+
return url.pathname + url.search + url.hash;
|
|
174
|
+
} catch(e) {
|
|
175
|
+
var sep = (ret || '/').indexOf('?') === -1 ? '?' : '&';
|
|
176
|
+
return (ret || '/') + sep + '_session=' + encodeURIComponent(sessionToken);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function __anFinishOAuthExchange(ret, flowId, sessionToken) {
|
|
165
180
|
if (__anIsBuilderPreview()) {
|
|
181
|
+
if (sessionToken) {
|
|
182
|
+
__anSetOAuthDebug('OAuth exchange redeemed; applying session bridge to embedded app', flowId);
|
|
183
|
+
window.location.replace(__anSessionBridgeUrl(ret, sessionToken));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
166
186
|
__anSetOAuthDebug('OAuth exchange redeemed; reloading the embedded app', flowId);
|
|
167
187
|
window.location.reload();
|
|
168
188
|
return;
|
|
@@ -219,6 +239,13 @@ function getGoogleLoginHtml(googleAuthMode) {
|
|
|
219
239
|
return false;
|
|
220
240
|
}
|
|
221
241
|
}
|
|
242
|
+
function __anIsInFrame() {
|
|
243
|
+
try {
|
|
244
|
+
return window.self !== window.top;
|
|
245
|
+
} catch(e) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
222
249
|
function __anIsElectron() {
|
|
223
250
|
try {
|
|
224
251
|
return (navigator.userAgent || '').indexOf('Electron') !== -1;
|
|
@@ -227,11 +254,11 @@ function getGoogleLoginHtml(googleAuthMode) {
|
|
|
227
254
|
}
|
|
228
255
|
}
|
|
229
256
|
function __anResolveAuthFlow() {
|
|
230
|
-
if (__anIsBuilderPreview()) return 'redirect';
|
|
257
|
+
if (__anIsBuilderPreview()) return __anIsInFrame() ? 'popup' : 'redirect';
|
|
231
258
|
var mode = __AN_GOOGLE_AUTH_MODE || 'auto';
|
|
232
259
|
if (mode === 'popup') return 'popup';
|
|
233
260
|
if (mode === 'redirect') return 'redirect';
|
|
234
|
-
return
|
|
261
|
+
return __anIsAgentNativeDesktop() ? 'redirect' : 'popup';
|
|
235
262
|
}
|
|
236
263
|
var __anOAuthPollTimer = null;
|
|
237
264
|
var __anOAuthPollCount = 0;
|
|
@@ -285,7 +312,7 @@ function getGoogleLoginHtml(googleAuthMode) {
|
|
|
285
312
|
if (data && (data.email || data.token)) {
|
|
286
313
|
if (__anOAuthPollTimer) clearInterval(__anOAuthPollTimer);
|
|
287
314
|
__anOAuthPollTimer = null;
|
|
288
|
-
__anFinishOAuthExchange(ret, flowId);
|
|
315
|
+
__anFinishOAuthExchange(ret, flowId, data.token);
|
|
289
316
|
return;
|
|
290
317
|
}
|
|
291
318
|
if (data && data.error) {
|
|
@@ -317,7 +344,7 @@ function getGoogleLoginHtml(googleAuthMode) {
|
|
|
317
344
|
params.set('desktop', '1');
|
|
318
345
|
params.set('flow_id', flowId);
|
|
319
346
|
params.set('redirect', '1');
|
|
320
|
-
var url =
|
|
347
|
+
var url = __anGoogleAuthUrlPath() + '?' + params.toString();
|
|
321
348
|
try { sessionStorage.setItem('__an_signin', '1'); } catch(e) {}
|
|
322
349
|
__anSetOAuthDebug('Opening Google sign-in popup', flowId);
|
|
323
350
|
try {
|
|
@@ -348,7 +375,7 @@ function getGoogleLoginHtml(googleAuthMode) {
|
|
|
348
375
|
params.set('desktop', '1');
|
|
349
376
|
params.set('flow_id', flowId);
|
|
350
377
|
params.set('redirect', '1');
|
|
351
|
-
var url =
|
|
378
|
+
var url = __anGoogleAuthUrlPath() + '?' + params.toString();
|
|
352
379
|
__anSetOAuthDebug('Opening Google sign-in in system browser', flowId);
|
|
353
380
|
__anOpenOAuthUrl(url);
|
|
354
381
|
__anWaitForOAuthExchange(flowId, ret, btn, err);
|
|
@@ -376,11 +403,11 @@ function getGoogleLoginHtml(googleAuthMode) {
|
|
|
376
403
|
if (ret) params.set('return', __anOAuthReturnTarget(ret));
|
|
377
404
|
params.set('redirect', '1');
|
|
378
405
|
__anSetOAuthDebug('Opening Google sign-in redirect');
|
|
379
|
-
__anOpenOAuthUrl(
|
|
406
|
+
__anOpenOAuthUrl(__anGoogleAuthUrlPath() + '?' + params.toString());
|
|
380
407
|
return;
|
|
381
408
|
}
|
|
382
409
|
try {
|
|
383
|
-
var res = await fetch(
|
|
410
|
+
var res = await fetch(__anGoogleAuthUrlPath() + '?return=' + encodeURIComponent(ret));
|
|
384
411
|
var data = await res.json();
|
|
385
412
|
if (data.url) {
|
|
386
413
|
__anOpenOAuthUrl(data.url);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-auth-plugin.js","sourceRoot":"","sources":["../../src/server/google-auth-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,+BAA+B,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EACL,qBAAqB,GAEtB,MAAM,uBAAuB,CAAC;AAe/B,SAAS,kBAAkB,CAAC,cAA8B;IACxD,MAAM,iBAAiB,GAAG,oBAAoB,EAAE,CAAC;IACjD,MAAM,4BAA4B,GAAG,+BAA+B,EAAE,CAAC;IACvE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCA+E0B,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC;+CACrB,IAAI,CAAC,SAAS,CAAC,4BAA4B,CAAC;gCAC3D,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAuTtD,CAAC;AACT,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,sBAAsB,CACpC,OAAiC;IAEjC,OAAO,gBAAgB,CAAC;QACtB,WAAW,EAAE;YACX,gCAAgC;YAChC,gCAAgC;YAChC,wBAAwB;YACxB,GAAG,CAAC,OAAO,EAAE,WAAW,IAAI,EAAE,CAAC;SAChC;QACD,SAAS,EAAE,kBAAkB,CAC3B,qBAAqB,CAAC,OAAO,EAAE,cAAc,CAAC,CAC/C;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { createAuthPlugin } from \"./auth-plugin.js\";\nimport { getPublicOAuthOrigin } from \"./oauth-public-origin.js\";\nimport { getWorkspaceGatewayReturnOrigin } from \"./oauth-return-url.js\";\nimport {\n resolveGoogleAuthMode,\n type GoogleAuthMode,\n} from \"./google-auth-mode.js\";\n\ntype NitroPluginDef = (nitroApp: any) => void | Promise<void>;\n\nexport interface GoogleAuthPluginOptions {\n /** Additional paths accessible without authentication */\n publicPaths?: string[];\n /**\n * Google sign-in flow: `'popup'`, `'redirect'`, or `'auto'` (default).\n * Falls back to `GOOGLE_AUTH_MODE` env var, then `'auto'`. Builder.io\n * preview/editor surfaces always use redirect.\n */\n googleAuthMode?: GoogleAuthMode;\n}\n\nfunction getGoogleLoginHtml(googleAuthMode: GoogleAuthMode): string {\n const publicOAuthOrigin = getPublicOAuthOrigin();\n const workspaceGatewayReturnOrigin = getWorkspaceGatewayReturnOrigin();\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n<title>Sign in</title>\n<style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0a0a0a;\n color: #e5e5e5;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n width: 100%;\n max-width: 360px;\n padding: 2rem;\n background: #141414;\n border: 1px solid rgba(255,255,255,0.08);\n border-radius: 12px;\n text-align: center;\n }\n h1 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: #fff; }\n .subtitle { font-size: 0.8125rem; color: #888; margin-bottom: 1.5rem; }\n button {\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.625rem;\n padding: 0.625rem;\n background: #fff;\n color: #000;\n border: none;\n border-radius: 8px;\n font-size: 0.9375rem;\n font-weight: 500;\n cursor: pointer;\n }\n button:hover { opacity: 0.85; }\n button:disabled { opacity: 0.5; cursor: wait; }\n .error { margin-top: 0.75rem; font-size: 0.8125rem; color: #f87171; display: none; }\n .error.show { display: block; }\n .debug {\n display: none;\n margin-top: 0.625rem;\n font-size: 0.6875rem;\n line-height: 1.45;\n color: #777;\n word-break: break-word;\n }\n .debug.show { display: block; }\n svg { width: 18px; height: 18px; }\n</style>\n</head>\n<body>\n<div class=\"card\">\n <h1>Sign in</h1>\n <p class=\"subtitle\">Continue with your Google account</p>\n <button id=\"btn\" onclick=\"signIn()\">\n <svg viewBox=\"0 0 24 24\"><path fill=\"#4285F4\" d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z\"/><path fill=\"#34A853\" d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/><path fill=\"#FBBC05\" d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"/><path fill=\"#EA4335\" d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"/></svg>\n Sign in with Google\n </button>\n <p class=\"error\" id=\"err\"></p>\n <p class=\"debug\" id=\"debug\"></p>\n</div>\n<script>\n function __anBasePath() {\n var marker = '/_agent-native';\n var idx = window.location.pathname.indexOf(marker);\n return idx > 0 ? window.location.pathname.slice(0, idx) : '';\n }\n function __anPath(path) {\n return __anBasePath() + path;\n }\n var __AN_PUBLIC_OAUTH_ORIGIN = ${JSON.stringify(publicOAuthOrigin)};\n var __AN_WORKSPACE_GATEWAY_RETURN_ORIGIN = ${JSON.stringify(workspaceGatewayReturnOrigin)};\n var __AN_GOOGLE_AUTH_MODE = ${JSON.stringify(googleAuthMode)};\n function __anConfiguredOAuthOrigin() {\n if (!__AN_PUBLIC_OAUTH_ORIGIN) return '';\n try {\n var origin = new URL(__AN_PUBLIC_OAUTH_ORIGIN).origin;\n return origin && origin !== window.location.origin ? origin : '';\n } catch(e) {\n return '';\n }\n }\n function __anAuthPath(path) {\n var origin = __anIsBuilderPreview() ? __anConfiguredOAuthOrigin() : '';\n return origin ? origin + path : __anPath(path);\n }\n function __anBuilderPreviewReturnOrigin() {\n var candidates = [window.location.href, document.referrer || ''];\n try {\n if (window.location.ancestorOrigins) {\n for (var j = 0; j < window.location.ancestorOrigins.length; j++) {\n candidates.push(window.location.ancestorOrigins[j]);\n }\n }\n } catch(e) {}\n for (var i = 0; i < candidates.length; i++) {\n try {\n var url = new URL(candidates[i]);\n var host = url.hostname.toLowerCase();\n var isPreviewHost =\n host === 'builderio.xyz' || host.slice(-14) === '.builderio.xyz' ||\n host === 'builderio.dev' || host.slice(-14) === '.builderio.dev' ||\n host === 'builder.codes' || host.slice(-14) === '.builder.codes' ||\n host === 'builder.my' || host.slice(-11) === '.builder.my';\n if (url.protocol === 'https:' && isPreviewHost) return url.origin;\n } catch(e) {}\n }\n return '';\n }\n function __anWorkspaceGatewayReturnOrigin() {\n var previewOrigin = __anBuilderPreviewReturnOrigin();\n if (previewOrigin) return previewOrigin;\n if (__AN_WORKSPACE_GATEWAY_RETURN_ORIGIN) return __AN_WORKSPACE_GATEWAY_RETURN_ORIGIN;\n return __anIsBuilderDesktop() ? 'http://127.0.0.1:8080' : '';\n }\n function __anNormalizeWorkspaceReturnPath(ret) {\n try {\n var url = new URL(ret || '/', window.location.origin);\n var path = url.pathname || '/';\n if (path === '/dispatch/dispatch') {\n path = '/dispatch';\n } else if (path.indexOf('/dispatch/') === 0) {\n var rest = path.slice('/dispatch/'.length);\n var first = rest.split('/')[0];\n var dispatchRoutes = {\n overview: true, apps: true, metrics: true, vault: true,\n integrations: true, messaging: true, workspace: true,\n agents: true, destinations: true, identities: true,\n approvals: true, audit: true, team: true, 'thread-debug': true,\n 'new-app': true\n };\n if (first === 'dispatch') {\n path = '/dispatch' + rest.slice(first.length);\n } else if (first && !dispatchRoutes[first]) {\n path = '/' + rest;\n }\n }\n return path + url.search + url.hash;\n } catch(e) {\n return ret || '/';\n }\n }\n function __anOAuthReturnTarget(ret) {\n var path = __anNormalizeWorkspaceReturnPath(ret);\n var origin = __anWorkspaceGatewayReturnOrigin();\n return origin ? origin + path : path;\n }\n function __anFinishOAuthExchange(ret, flowId) {\n if (__anIsBuilderPreview()) {\n __anSetOAuthDebug('OAuth exchange redeemed; reloading the embedded app', flowId);\n window.location.reload();\n return;\n }\n __anSetOAuthDebug('OAuth exchange redeemed; returning to the app', flowId);\n window.location.href = ret || '/';\n }\n var __anBuilderPreviewSeen = false;\n function __anRememberBuilderPreview() {\n __anBuilderPreviewSeen = true;\n try { sessionStorage.setItem('__an_builder_preview_seen', '1'); } catch(e) {}\n }\n function __anHasBuilderPreviewSignal() {\n try {\n var params = new URLSearchParams(window.location.search);\n if (params.has('builder.preview') || params.has('builder.frameEditing') || params.has('__builder_editing__')) return true;\n } catch(e) {}\n return false;\n }\n function __anIsBuilderPreview() {\n if (__anBuilderPreviewSeen) return true;\n if (__anHasBuilderPreviewSignal()) {\n __anRememberBuilderPreview();\n return true;\n }\n try {\n if (sessionStorage.getItem('__an_builder_preview_seen') === '1') {\n __anBuilderPreviewSeen = true;\n return true;\n }\n } catch(e) {}\n try {\n var ref = document.referrer || '';\n var fromBuilder = ref.indexOf('builder.io') !== -1 || ref.indexOf('builder.my') !== -1 || ref.indexOf('builderio.xyz') !== -1 || ref.indexOf('builderio.dev') !== -1 || ref.indexOf('builder.codes') !== -1;\n if (fromBuilder) __anRememberBuilderPreview();\n return fromBuilder;\n } catch(e) {\n return false;\n }\n }\n __anIsBuilderPreview();\n function __anIsBuilderDesktop() {\n try {\n var ua = navigator.userAgent || '';\n return ua.indexOf('Electron') !== -1 && ua.indexOf('AgentNativeDesktop') === -1;\n } catch(e) {\n return false;\n }\n }\n function __anIsAgentNativeDesktop() {\n try {\n return (navigator.userAgent || '').indexOf('AgentNativeDesktop') !== -1;\n } catch(e) {\n return false;\n }\n }\n function __anIsElectron() {\n try {\n return (navigator.userAgent || '').indexOf('Electron') !== -1;\n } catch(e) {\n return false;\n }\n }\n function __anResolveAuthFlow() {\n if (__anIsBuilderPreview()) return 'redirect';\n var mode = __AN_GOOGLE_AUTH_MODE || 'auto';\n if (mode === 'popup') return 'popup';\n if (mode === 'redirect') return 'redirect';\n return __anIsElectron() ? 'redirect' : 'popup';\n }\n var __anOAuthPollTimer = null;\n var __anOAuthPollCount = 0;\n function __anNewOAuthFlowId() {\n try {\n if (window.crypto && typeof window.crypto.randomUUID === 'function') {\n return window.crypto.randomUUID();\n }\n } catch(e) {}\n return 'builder-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);\n }\n function __anFlowDebugId(flowId) {\n return flowId ? String(flowId).slice(-10) : '';\n }\n function __anShouldShowOAuthDebug() {\n try {\n var loc = window.location || {};\n return (typeof loc.hash === 'string' && loc.hash.indexOf('oauth-debug') !== -1) ||\n (typeof loc.search === 'string' && loc.search.indexOf('oauth_debug=1') !== -1);\n } catch(e) { return false; }\n }\n function __anSetOAuthDebug(message, flowId) {\n var text = message + (flowId ? ' (flow ' + __anFlowDebugId(flowId) + ')' : '');\n try {\n console.info('[agent-native][google-oauth] ' + text);\n } catch(e) {}\n var debug = document.getElementById('debug');\n if (debug) {\n debug.textContent = text;\n if (__anShouldShowOAuthDebug()) debug.classList.add('show');\n }\n }\n function __anShowOAuthError(err, btn, message) {\n if (__anOAuthPollTimer) {\n clearInterval(__anOAuthPollTimer);\n __anOAuthPollTimer = null;\n }\n err.textContent = message;\n err.classList.add('show');\n btn.disabled = false;\n }\n function __anWaitForOAuthExchange(flowId, ret, btn, err) {\n var started = Date.now();\n var timeoutMs = 5 * 60 * 1000;\n __anOAuthPollCount = 0;\n async function check() {\n __anOAuthPollCount++;\n try {\n var res = await fetch(__anPath('/_agent-native/auth/desktop-exchange') + '?flow_id=' + encodeURIComponent(flowId), { credentials: 'include' });\n var data = await res.json().catch(function() { return {}; });\n if (data && (data.email || data.token)) {\n if (__anOAuthPollTimer) clearInterval(__anOAuthPollTimer);\n __anOAuthPollTimer = null;\n __anFinishOAuthExchange(ret, flowId);\n return;\n }\n if (data && data.error) {\n __anSetOAuthDebug('OAuth exchange returned an error: ' + (data.message || data.error), flowId);\n __anShowOAuthError(err, btn, data.message || data.error);\n return;\n }\n if (data && data.pending && (__anOAuthPollCount === 1 || __anOAuthPollCount % 5 === 0)) {\n __anSetOAuthDebug('Waiting for the Google callback; polling attempt ' + __anOAuthPollCount, flowId);\n }\n } catch(e) {\n if (__anOAuthPollCount === 1 || __anOAuthPollCount % 5 === 0) {\n __anSetOAuthDebug('Could not reach the OAuth exchange endpoint: ' + (e && e.message ? e.message : 'network error'), flowId);\n }\n }\n if (Date.now() - started > timeoutMs) {\n __anShowOAuthError(err, btn, 'Google sign-in did not finish. Flow ' + __anFlowDebugId(flowId) + ' never reached this app. Check the Google OAuth redirect URI and server logs for [agent-native][google-oauth].');\n }\n }\n if (__anOAuthPollTimer) clearInterval(__anOAuthPollTimer);\n __anOAuthPollTimer = setInterval(check, 1000);\n setTimeout(check, 500);\n }\n function __anStartPopupOAuth(ret, btn, err) {\n var flowId = __anNewOAuthFlowId();\n var oauthReturn = __anIsBuilderPreview() ? __anOAuthReturnTarget(ret) : ret;\n var params = new URLSearchParams();\n if (oauthReturn) params.set('return', oauthReturn);\n params.set('desktop', '1');\n params.set('flow_id', flowId);\n params.set('redirect', '1');\n var url = __anPath('/_agent-native/google/auth-url') + '?' + params.toString();\n try { sessionStorage.setItem('__an_signin', '1'); } catch(e) {}\n __anSetOAuthDebug('Opening Google sign-in popup', flowId);\n try {\n var popup = window.open('', '_blank', 'width=640,height=760');\n if (!popup) {\n __anShowOAuthError(err, btn, 'Google popup was blocked. Allow popups for this site and try again (flow ' + __anFlowDebugId(flowId) + ').');\n return;\n }\n try { popup.opener = null; } catch(e) {}\n try {\n popup.location.href = url;\n } catch(e) {\n try { popup.close(); } catch(closeErr) {}\n __anShowOAuthError(err, btn, 'Could not navigate Google popup for flow ' + __anFlowDebugId(flowId) + ': ' + (e && e.message ? e.message : 'unknown error'));\n return;\n }\n __anSetOAuthDebug('Google popup opened; waiting for callback', flowId);\n } catch(e) {\n __anShowOAuthError(err, btn, 'Could not open Google popup for flow ' + __anFlowDebugId(flowId) + ': ' + (e && e.message ? e.message : 'unknown error'));\n return;\n }\n __anWaitForOAuthExchange(flowId, ret, btn, err);\n }\n function __anStartNativeDesktopOAuth(ret, btn, err) {\n var flowId = __anNewOAuthFlowId();\n var params = new URLSearchParams();\n if (ret) params.set('return', ret);\n params.set('desktop', '1');\n params.set('flow_id', flowId);\n params.set('redirect', '1');\n var url = __anPath('/_agent-native/google/auth-url') + '?' + params.toString();\n __anSetOAuthDebug('Opening Google sign-in in system browser', flowId);\n __anOpenOAuthUrl(url);\n __anWaitForOAuthExchange(flowId, ret, btn, err);\n }\n function __anOpenOAuthUrl(url) {\n try { sessionStorage.setItem('__an_signin', '1'); } catch(e) {}\n window.location.href = url;\n }\n async function signIn() {\n var btn = document.getElementById('btn');\n var err = document.getElementById('err');\n var ret = window.location.pathname + window.location.search;\n btn.disabled = true;\n err.classList.remove('show');\n if (__anResolveAuthFlow() === 'popup') {\n __anStartPopupOAuth(ret, btn, err);\n return;\n }\n if (__anIsAgentNativeDesktop()) {\n __anStartNativeDesktopOAuth(ret, btn, err);\n return;\n }\n if (__anIsBuilderPreview()) {\n var params = new URLSearchParams();\n if (ret) params.set('return', __anOAuthReturnTarget(ret));\n params.set('redirect', '1');\n __anSetOAuthDebug('Opening Google sign-in redirect');\n __anOpenOAuthUrl(__anAuthPath('/_agent-native/google/auth-url') + '?' + params.toString());\n return;\n }\n try {\n var res = await fetch(__anPath('/_agent-native/google/auth-url') + '?return=' + encodeURIComponent(ret));\n var data = await res.json();\n if (data.url) {\n __anOpenOAuthUrl(data.url);\n } else {\n err.textContent = data.message || 'Google OAuth is not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.';\n err.classList.add('show');\n btn.disabled = false;\n }\n } catch (e) {\n err.textContent = 'Failed to connect. Please try again.';\n err.classList.add('show');\n btn.disabled = false;\n }\n }\n</script>\n</body>\n</html>`;\n}\n\n/**\n * Create an auth plugin that uses Google OAuth for authentication.\n *\n * When a user visits the app unauthenticated, they see a \"Sign in with Google\"\n * page. The Google OAuth callback (handled by the template) creates a session\n * tied to the user's Google email. `getSession()` then returns `{ email }` for\n * all subsequent requests.\n *\n * Better Auth handles Google OAuth internally when GOOGLE_CLIENT_ID and\n * GOOGLE_CLIENT_SECRET are set. The template's callback route at\n * /_agent-native/google/callback handles mobile deep linking.\n *\n * Usage in a template's `server/plugins/auth.ts`:\n * ```ts\n * import { createGoogleAuthPlugin } from \"@agent-native/core/server\";\n * export default createGoogleAuthPlugin();\n * ```\n */\nexport function createGoogleAuthPlugin(\n options?: GoogleAuthPluginOptions,\n): NitroPluginDef {\n return createAuthPlugin({\n publicPaths: [\n \"/_agent-native/google/callback\",\n \"/_agent-native/google/auth-url\",\n \"/_agent-native/auth/ba\",\n ...(options?.publicPaths ?? []),\n ],\n loginHtml: getGoogleLoginHtml(\n resolveGoogleAuthMode(options?.googleAuthMode),\n ),\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"google-auth-plugin.js","sourceRoot":"","sources":["../../src/server/google-auth-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,+BAA+B,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EACL,qBAAqB,GAEtB,MAAM,uBAAuB,CAAC;AAgB/B,SAAS,kBAAkB,CAAC,cAA8B;IACxD,MAAM,iBAAiB,GAAG,oBAAoB,EAAE,CAAC;IACjD,MAAM,4BAA4B,GAAG,+BAA+B,EAAE,CAAC;IACvE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCA+E0B,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC;+CACrB,IAAI,CAAC,SAAS,CAAC,4BAA4B,CAAC;gCAC3D,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAkVtD,CAAC;AACT,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,sBAAsB,CACpC,OAAiC;IAEjC,OAAO,gBAAgB,CAAC;QACtB,WAAW,EAAE;YACX,gCAAgC;YAChC,gCAAgC;YAChC,wBAAwB;YACxB,GAAG,CAAC,OAAO,EAAE,WAAW,IAAI,EAAE,CAAC;SAChC;QACD,SAAS,EAAE,kBAAkB,CAC3B,qBAAqB,CAAC,OAAO,EAAE,cAAc,CAAC,CAC/C;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { createAuthPlugin } from \"./auth-plugin.js\";\nimport { getPublicOAuthOrigin } from \"./oauth-public-origin.js\";\nimport { getWorkspaceGatewayReturnOrigin } from \"./oauth-return-url.js\";\nimport {\n resolveGoogleAuthMode,\n type GoogleAuthMode,\n} from \"./google-auth-mode.js\";\n\ntype NitroPluginDef = (nitroApp: any) => void | Promise<void>;\n\nexport interface GoogleAuthPluginOptions {\n /** Additional paths accessible without authentication */\n publicPaths?: string[];\n /**\n * Google sign-in flow: `'popup'`, `'redirect'`, or `'auto'` (default).\n * Falls back to `GOOGLE_AUTH_MODE` env var, then `'auto'`. Builder\n * iframes use popup; top-level Builder preview/editor surfaces use\n * redirect.\n */\n googleAuthMode?: GoogleAuthMode;\n}\n\nfunction getGoogleLoginHtml(googleAuthMode: GoogleAuthMode): string {\n const publicOAuthOrigin = getPublicOAuthOrigin();\n const workspaceGatewayReturnOrigin = getWorkspaceGatewayReturnOrigin();\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n<title>Sign in</title>\n<style>\n *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0a0a0a;\n color: #e5e5e5;\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 100vh;\n }\n .card {\n width: 100%;\n max-width: 360px;\n padding: 2rem;\n background: #141414;\n border: 1px solid rgba(255,255,255,0.08);\n border-radius: 12px;\n text-align: center;\n }\n h1 { font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: #fff; }\n .subtitle { font-size: 0.8125rem; color: #888; margin-bottom: 1.5rem; }\n button {\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 0.625rem;\n padding: 0.625rem;\n background: #fff;\n color: #000;\n border: none;\n border-radius: 8px;\n font-size: 0.9375rem;\n font-weight: 500;\n cursor: pointer;\n }\n button:hover { opacity: 0.85; }\n button:disabled { opacity: 0.5; cursor: wait; }\n .error { margin-top: 0.75rem; font-size: 0.8125rem; color: #f87171; display: none; }\n .error.show { display: block; }\n .debug {\n display: none;\n margin-top: 0.625rem;\n font-size: 0.6875rem;\n line-height: 1.45;\n color: #777;\n word-break: break-word;\n }\n .debug.show { display: block; }\n svg { width: 18px; height: 18px; }\n</style>\n</head>\n<body>\n<div class=\"card\">\n <h1>Sign in</h1>\n <p class=\"subtitle\">Continue with your Google account</p>\n <button id=\"btn\" onclick=\"signIn()\">\n <svg viewBox=\"0 0 24 24\"><path fill=\"#4285F4\" d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z\"/><path fill=\"#34A853\" d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/><path fill=\"#FBBC05\" d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"/><path fill=\"#EA4335\" d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"/></svg>\n Sign in with Google\n </button>\n <p class=\"error\" id=\"err\"></p>\n <p class=\"debug\" id=\"debug\"></p>\n</div>\n<script>\n function __anBasePath() {\n var marker = '/_agent-native';\n var idx = window.location.pathname.indexOf(marker);\n return idx > 0 ? window.location.pathname.slice(0, idx) : '';\n }\n function __anPath(path) {\n return __anBasePath() + path;\n }\n var __AN_PUBLIC_OAUTH_ORIGIN = ${JSON.stringify(publicOAuthOrigin)};\n var __AN_WORKSPACE_GATEWAY_RETURN_ORIGIN = ${JSON.stringify(workspaceGatewayReturnOrigin)};\n var __AN_GOOGLE_AUTH_MODE = ${JSON.stringify(googleAuthMode)};\n function __anConfiguredOAuthOrigin() {\n if (!__AN_PUBLIC_OAUTH_ORIGIN) return '';\n try {\n var origin = new URL(__AN_PUBLIC_OAUTH_ORIGIN).origin;\n return origin && origin !== window.location.origin ? origin : '';\n } catch(e) {\n return '';\n }\n }\n function __anAuthPath(path) {\n var origin = __anIsBuilderPreview() ? __anConfiguredOAuthOrigin() : '';\n return origin ? origin + path : __anPath(path);\n }\n function __anGoogleAuthUrlPath() {\n return __anIsBuilderPreview()\n ? __anAuthPath('/_agent-native/google/auth-url')\n : __anPath('/_agent-native/google/auth-url');\n }\n function __anBuilderPreviewReturnOrigin() {\n var candidates = [window.location.href, document.referrer || ''];\n try {\n if (window.location.ancestorOrigins) {\n for (var j = 0; j < window.location.ancestorOrigins.length; j++) {\n candidates.push(window.location.ancestorOrigins[j]);\n }\n }\n } catch(e) {}\n for (var i = 0; i < candidates.length; i++) {\n try {\n var url = new URL(candidates[i]);\n var host = url.hostname.toLowerCase();\n var isPreviewHost =\n host === 'builderio.xyz' || host.slice(-14) === '.builderio.xyz' ||\n host === 'builderio.dev' || host.slice(-14) === '.builderio.dev' ||\n host === 'builder.codes' || host.slice(-14) === '.builder.codes' ||\n host === 'builder.my' || host.slice(-11) === '.builder.my';\n if (url.protocol === 'https:' && isPreviewHost) return url.origin;\n } catch(e) {}\n }\n return '';\n }\n function __anWorkspaceGatewayReturnOrigin() {\n var previewOrigin = __anBuilderPreviewReturnOrigin();\n if (previewOrigin) return previewOrigin;\n if (__AN_WORKSPACE_GATEWAY_RETURN_ORIGIN) return __AN_WORKSPACE_GATEWAY_RETURN_ORIGIN;\n return __anIsBuilderDesktop() ? 'http://127.0.0.1:8080' : '';\n }\n function __anNormalizeWorkspaceReturnPath(ret) {\n try {\n var url = new URL(ret || '/', window.location.origin);\n var path = url.pathname || '/';\n if (path === '/dispatch/dispatch') {\n path = '/dispatch';\n } else if (path.indexOf('/dispatch/') === 0) {\n var rest = path.slice('/dispatch/'.length);\n var first = rest.split('/')[0];\n var dispatchRoutes = {\n overview: true, apps: true, metrics: true, vault: true,\n integrations: true, messaging: true, workspace: true,\n agents: true, destinations: true, identities: true,\n approvals: true, audit: true, team: true, 'thread-debug': true,\n 'new-app': true\n };\n if (first === 'dispatch') {\n path = '/dispatch' + rest.slice(first.length);\n } else if (first && !dispatchRoutes[first]) {\n path = '/' + rest;\n }\n }\n return path + url.search + url.hash;\n } catch(e) {\n return ret || '/';\n }\n }\n function __anOAuthReturnTarget(ret) {\n var path = __anNormalizeWorkspaceReturnPath(ret);\n var origin = __anWorkspaceGatewayReturnOrigin();\n return origin ? origin + path : path;\n }\n function __anSessionBridgeUrl(ret, sessionToken) {\n try {\n var url = new URL(ret || window.location.pathname + window.location.search, window.location.origin);\n url.searchParams.set('_session', sessionToken);\n return url.pathname + url.search + url.hash;\n } catch(e) {\n var sep = (ret || '/').indexOf('?') === -1 ? '?' : '&';\n return (ret || '/') + sep + '_session=' + encodeURIComponent(sessionToken);\n }\n }\n function __anFinishOAuthExchange(ret, flowId, sessionToken) {\n if (__anIsBuilderPreview()) {\n if (sessionToken) {\n __anSetOAuthDebug('OAuth exchange redeemed; applying session bridge to embedded app', flowId);\n window.location.replace(__anSessionBridgeUrl(ret, sessionToken));\n return;\n }\n __anSetOAuthDebug('OAuth exchange redeemed; reloading the embedded app', flowId);\n window.location.reload();\n return;\n }\n __anSetOAuthDebug('OAuth exchange redeemed; returning to the app', flowId);\n window.location.href = ret || '/';\n }\n var __anBuilderPreviewSeen = false;\n function __anRememberBuilderPreview() {\n __anBuilderPreviewSeen = true;\n try { sessionStorage.setItem('__an_builder_preview_seen', '1'); } catch(e) {}\n }\n function __anHasBuilderPreviewSignal() {\n try {\n var params = new URLSearchParams(window.location.search);\n if (params.has('builder.preview') || params.has('builder.frameEditing') || params.has('__builder_editing__')) return true;\n } catch(e) {}\n return false;\n }\n function __anIsBuilderPreview() {\n if (__anBuilderPreviewSeen) return true;\n if (__anHasBuilderPreviewSignal()) {\n __anRememberBuilderPreview();\n return true;\n }\n try {\n if (sessionStorage.getItem('__an_builder_preview_seen') === '1') {\n __anBuilderPreviewSeen = true;\n return true;\n }\n } catch(e) {}\n try {\n var ref = document.referrer || '';\n var fromBuilder = ref.indexOf('builder.io') !== -1 || ref.indexOf('builder.my') !== -1 || ref.indexOf('builderio.xyz') !== -1 || ref.indexOf('builderio.dev') !== -1 || ref.indexOf('builder.codes') !== -1;\n if (fromBuilder) __anRememberBuilderPreview();\n return fromBuilder;\n } catch(e) {\n return false;\n }\n }\n __anIsBuilderPreview();\n function __anIsBuilderDesktop() {\n try {\n var ua = navigator.userAgent || '';\n return ua.indexOf('Electron') !== -1 && ua.indexOf('AgentNativeDesktop') === -1;\n } catch(e) {\n return false;\n }\n }\n function __anIsAgentNativeDesktop() {\n try {\n return (navigator.userAgent || '').indexOf('AgentNativeDesktop') !== -1;\n } catch(e) {\n return false;\n }\n }\n function __anIsInFrame() {\n try {\n return window.self !== window.top;\n } catch(e) {\n return true;\n }\n }\n function __anIsElectron() {\n try {\n return (navigator.userAgent || '').indexOf('Electron') !== -1;\n } catch(e) {\n return false;\n }\n }\n function __anResolveAuthFlow() {\n if (__anIsBuilderPreview()) return __anIsInFrame() ? 'popup' : 'redirect';\n var mode = __AN_GOOGLE_AUTH_MODE || 'auto';\n if (mode === 'popup') return 'popup';\n if (mode === 'redirect') return 'redirect';\n return __anIsAgentNativeDesktop() ? 'redirect' : 'popup';\n }\n var __anOAuthPollTimer = null;\n var __anOAuthPollCount = 0;\n function __anNewOAuthFlowId() {\n try {\n if (window.crypto && typeof window.crypto.randomUUID === 'function') {\n return window.crypto.randomUUID();\n }\n } catch(e) {}\n return 'builder-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2);\n }\n function __anFlowDebugId(flowId) {\n return flowId ? String(flowId).slice(-10) : '';\n }\n function __anShouldShowOAuthDebug() {\n try {\n var loc = window.location || {};\n return (typeof loc.hash === 'string' && loc.hash.indexOf('oauth-debug') !== -1) ||\n (typeof loc.search === 'string' && loc.search.indexOf('oauth_debug=1') !== -1);\n } catch(e) { return false; }\n }\n function __anSetOAuthDebug(message, flowId) {\n var text = message + (flowId ? ' (flow ' + __anFlowDebugId(flowId) + ')' : '');\n try {\n console.info('[agent-native][google-oauth] ' + text);\n } catch(e) {}\n var debug = document.getElementById('debug');\n if (debug) {\n debug.textContent = text;\n if (__anShouldShowOAuthDebug()) debug.classList.add('show');\n }\n }\n function __anShowOAuthError(err, btn, message) {\n if (__anOAuthPollTimer) {\n clearInterval(__anOAuthPollTimer);\n __anOAuthPollTimer = null;\n }\n err.textContent = message;\n err.classList.add('show');\n btn.disabled = false;\n }\n function __anWaitForOAuthExchange(flowId, ret, btn, err) {\n var started = Date.now();\n var timeoutMs = 5 * 60 * 1000;\n __anOAuthPollCount = 0;\n async function check() {\n __anOAuthPollCount++;\n try {\n var res = await fetch(__anPath('/_agent-native/auth/desktop-exchange') + '?flow_id=' + encodeURIComponent(flowId), { credentials: 'include' });\n var data = await res.json().catch(function() { return {}; });\n if (data && (data.email || data.token)) {\n if (__anOAuthPollTimer) clearInterval(__anOAuthPollTimer);\n __anOAuthPollTimer = null;\n __anFinishOAuthExchange(ret, flowId, data.token);\n return;\n }\n if (data && data.error) {\n __anSetOAuthDebug('OAuth exchange returned an error: ' + (data.message || data.error), flowId);\n __anShowOAuthError(err, btn, data.message || data.error);\n return;\n }\n if (data && data.pending && (__anOAuthPollCount === 1 || __anOAuthPollCount % 5 === 0)) {\n __anSetOAuthDebug('Waiting for the Google callback; polling attempt ' + __anOAuthPollCount, flowId);\n }\n } catch(e) {\n if (__anOAuthPollCount === 1 || __anOAuthPollCount % 5 === 0) {\n __anSetOAuthDebug('Could not reach the OAuth exchange endpoint: ' + (e && e.message ? e.message : 'network error'), flowId);\n }\n }\n if (Date.now() - started > timeoutMs) {\n __anShowOAuthError(err, btn, 'Google sign-in did not finish. Flow ' + __anFlowDebugId(flowId) + ' never reached this app. Check the Google OAuth redirect URI and server logs for [agent-native][google-oauth].');\n }\n }\n if (__anOAuthPollTimer) clearInterval(__anOAuthPollTimer);\n __anOAuthPollTimer = setInterval(check, 1000);\n setTimeout(check, 500);\n }\n function __anStartPopupOAuth(ret, btn, err) {\n var flowId = __anNewOAuthFlowId();\n var oauthReturn = __anIsBuilderPreview() ? __anOAuthReturnTarget(ret) : ret;\n var params = new URLSearchParams();\n if (oauthReturn) params.set('return', oauthReturn);\n params.set('desktop', '1');\n params.set('flow_id', flowId);\n params.set('redirect', '1');\n var url = __anGoogleAuthUrlPath() + '?' + params.toString();\n try { sessionStorage.setItem('__an_signin', '1'); } catch(e) {}\n __anSetOAuthDebug('Opening Google sign-in popup', flowId);\n try {\n var popup = window.open('', '_blank', 'width=640,height=760');\n if (!popup) {\n __anShowOAuthError(err, btn, 'Google popup was blocked. Allow popups for this site and try again (flow ' + __anFlowDebugId(flowId) + ').');\n return;\n }\n try { popup.opener = null; } catch(e) {}\n try {\n popup.location.href = url;\n } catch(e) {\n try { popup.close(); } catch(closeErr) {}\n __anShowOAuthError(err, btn, 'Could not navigate Google popup for flow ' + __anFlowDebugId(flowId) + ': ' + (e && e.message ? e.message : 'unknown error'));\n return;\n }\n __anSetOAuthDebug('Google popup opened; waiting for callback', flowId);\n } catch(e) {\n __anShowOAuthError(err, btn, 'Could not open Google popup for flow ' + __anFlowDebugId(flowId) + ': ' + (e && e.message ? e.message : 'unknown error'));\n return;\n }\n __anWaitForOAuthExchange(flowId, ret, btn, err);\n }\n function __anStartNativeDesktopOAuth(ret, btn, err) {\n var flowId = __anNewOAuthFlowId();\n var params = new URLSearchParams();\n if (ret) params.set('return', ret);\n params.set('desktop', '1');\n params.set('flow_id', flowId);\n params.set('redirect', '1');\n var url = __anGoogleAuthUrlPath() + '?' + params.toString();\n __anSetOAuthDebug('Opening Google sign-in in system browser', flowId);\n __anOpenOAuthUrl(url);\n __anWaitForOAuthExchange(flowId, ret, btn, err);\n }\n function __anOpenOAuthUrl(url) {\n try { sessionStorage.setItem('__an_signin', '1'); } catch(e) {}\n window.location.href = url;\n }\n async function signIn() {\n var btn = document.getElementById('btn');\n var err = document.getElementById('err');\n var ret = window.location.pathname + window.location.search;\n btn.disabled = true;\n err.classList.remove('show');\n if (__anResolveAuthFlow() === 'popup') {\n __anStartPopupOAuth(ret, btn, err);\n return;\n }\n if (__anIsAgentNativeDesktop()) {\n __anStartNativeDesktopOAuth(ret, btn, err);\n return;\n }\n if (__anIsBuilderPreview()) {\n var params = new URLSearchParams();\n if (ret) params.set('return', __anOAuthReturnTarget(ret));\n params.set('redirect', '1');\n __anSetOAuthDebug('Opening Google sign-in redirect');\n __anOpenOAuthUrl(__anGoogleAuthUrlPath() + '?' + params.toString());\n return;\n }\n try {\n var res = await fetch(__anGoogleAuthUrlPath() + '?return=' + encodeURIComponent(ret));\n var data = await res.json();\n if (data.url) {\n __anOpenOAuthUrl(data.url);\n } else {\n err.textContent = data.message || 'Google OAuth is not configured. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.';\n err.classList.add('show');\n btn.disabled = false;\n }\n } catch (e) {\n err.textContent = 'Failed to connect. Please try again.';\n err.classList.add('show');\n btn.disabled = false;\n }\n }\n</script>\n</body>\n</html>`;\n}\n\n/**\n * Create an auth plugin that uses Google OAuth for authentication.\n *\n * When a user visits the app unauthenticated, they see a \"Sign in with Google\"\n * page. The Google OAuth callback (handled by the template) creates a session\n * tied to the user's Google email. `getSession()` then returns `{ email }` for\n * all subsequent requests.\n *\n * Better Auth handles Google OAuth internally when GOOGLE_CLIENT_ID and\n * GOOGLE_CLIENT_SECRET are set. The template's callback route at\n * /_agent-native/google/callback handles mobile deep linking.\n *\n * Usage in a template's `server/plugins/auth.ts`:\n * ```ts\n * import { createGoogleAuthPlugin } from \"@agent-native/core/server\";\n * export default createGoogleAuthPlugin();\n * ```\n */\nexport function createGoogleAuthPlugin(\n options?: GoogleAuthPluginOptions,\n): NitroPluginDef {\n return createAuthPlugin({\n publicPaths: [\n \"/_agent-native/google/callback\",\n \"/_agent-native/google/auth-url\",\n \"/_agent-native/auth/ba\",\n ...(options?.publicPaths ?? []),\n ],\n loginHtml: getGoogleLoginHtml(\n resolveGoogleAuthMode(options?.googleAuthMode),\n ),\n });\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migrate data owned by `local@localhost` to a real account.
|
|
3
|
+
*
|
|
4
|
+
* When a user starts an app in local mode and later signs in to create a real
|
|
5
|
+
* account, this function moves all of their existing data over to the new
|
|
6
|
+
* account so they don't lose anything.
|
|
7
|
+
*
|
|
8
|
+
* Scope of the migration:
|
|
9
|
+
* - `application_state`: rows with `session_id = 'local'`
|
|
10
|
+
* - `settings`: keys prefixed with `u:local@localhost:`
|
|
11
|
+
* - `oauth_tokens`: rows with `owner = 'local@localhost'`
|
|
12
|
+
* - Any template table that has an `owner_email` column: rows with
|
|
13
|
+
* `owner_email = 'local@localhost'`
|
|
14
|
+
*
|
|
15
|
+
* The operation is a no-op if the target email is itself `local@localhost`,
|
|
16
|
+
* empty, or if there is nothing to migrate.
|
|
17
|
+
*/
|
|
18
|
+
export interface LocalMigrationResult {
|
|
19
|
+
/** Whether anything was actually migrated. */
|
|
20
|
+
migrated: boolean;
|
|
21
|
+
/** Per-table row counts that were updated. Omits tables with zero updates. */
|
|
22
|
+
tables: Record<string, number>;
|
|
23
|
+
/** Target email the data now belongs to. */
|
|
24
|
+
targetEmail: string;
|
|
25
|
+
/**
|
|
26
|
+
* Non-fatal per-step errors encountered during migration. One bad table
|
|
27
|
+
* no longer fails the whole upgrade — we migrate everything we can and
|
|
28
|
+
* report any steps that threw here so the UI can surface them.
|
|
29
|
+
*/
|
|
30
|
+
errors?: Array<{
|
|
31
|
+
step: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Migrate every piece of local-mode data to the given real account email.
|
|
37
|
+
* Safe to call multiple times — it only touches rows that are still attached
|
|
38
|
+
* to `local@localhost`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function migrateLocalUserData(targetEmail: string): Promise<LocalMigrationResult>;
|
|
41
|
+
//# sourceMappingURL=local-migration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-migration.d.ts","sourceRoot":"","sources":["../../src/server/local-migration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,MAAM,WAAW,oBAAoB;IACnC,8CAA8C;IAC9C,QAAQ,EAAE,OAAO,CAAC;IAClB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnD;AAoLD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC,CAyD/B"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migrate data owned by `local@localhost` to a real account.
|
|
3
|
+
*
|
|
4
|
+
* When a user starts an app in local mode and later signs in to create a real
|
|
5
|
+
* account, this function moves all of their existing data over to the new
|
|
6
|
+
* account so they don't lose anything.
|
|
7
|
+
*
|
|
8
|
+
* Scope of the migration:
|
|
9
|
+
* - `application_state`: rows with `session_id = 'local'`
|
|
10
|
+
* - `settings`: keys prefixed with `u:local@localhost:`
|
|
11
|
+
* - `oauth_tokens`: rows with `owner = 'local@localhost'`
|
|
12
|
+
* - Any template table that has an `owner_email` column: rows with
|
|
13
|
+
* `owner_email = 'local@localhost'`
|
|
14
|
+
*
|
|
15
|
+
* The operation is a no-op if the target email is itself `local@localhost`,
|
|
16
|
+
* empty, or if there is nothing to migrate.
|
|
17
|
+
*/
|
|
18
|
+
import { getDbExec, isPostgres } from "../db/client.js";
|
|
19
|
+
const LOCAL_EMAIL = "local@localhost";
|
|
20
|
+
const LOCAL_SESSION_ID = "local";
|
|
21
|
+
const OWNER_COLUMN = "owner_email";
|
|
22
|
+
/**
|
|
23
|
+
* Error messages that indicate a missing/inaccessible table or column — the
|
|
24
|
+
* migration treats these as "feature not enabled" and skips silently.
|
|
25
|
+
*/
|
|
26
|
+
const SCHEMA_SKIP_ERR = /no such table|no such column|does not exist|undefined table|undefined column|relation .* does not exist|column .* does not exist|permission denied|is not a table|cannot update view|cannot change column in a view/i;
|
|
27
|
+
function shouldSkipSchemaError(err) {
|
|
28
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
+
return SCHEMA_SKIP_ERR.test(message);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Discover every table (not view) in `public` that has an `owner_email`
|
|
33
|
+
* column. Views and materialized views are excluded — they can't be updated
|
|
34
|
+
* directly and would 500 the migration.
|
|
35
|
+
*/
|
|
36
|
+
async function discoverOwnerEmailTables() {
|
|
37
|
+
const client = getDbExec();
|
|
38
|
+
if (isPostgres()) {
|
|
39
|
+
// Join against information_schema.tables to filter out views and
|
|
40
|
+
// materialized views (anything that isn't a plain BASE TABLE).
|
|
41
|
+
const { rows } = await client.execute({
|
|
42
|
+
sql: `SELECT c.table_name
|
|
43
|
+
FROM information_schema.columns c
|
|
44
|
+
JOIN information_schema.tables t
|
|
45
|
+
ON t.table_schema = c.table_schema
|
|
46
|
+
AND t.table_name = c.table_name
|
|
47
|
+
WHERE c.table_schema = 'public'
|
|
48
|
+
AND c.column_name = $1
|
|
49
|
+
AND t.table_type = 'BASE TABLE'`,
|
|
50
|
+
args: [OWNER_COLUMN],
|
|
51
|
+
});
|
|
52
|
+
return rows.map((r) => r.table_name ?? r[0]).filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
// SQLite path: iterate tables (type='table', not 'view') and inspect columns via PRAGMA
|
|
55
|
+
const tablesRes = await client.execute(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`);
|
|
56
|
+
const tables = tablesRes.rows
|
|
57
|
+
.map((r) => (r.name ?? r[0]))
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
const withOwner = [];
|
|
60
|
+
for (const table of tables) {
|
|
61
|
+
const escaped = table.replace(/"/g, '""');
|
|
62
|
+
const colsRes = await client.execute(`PRAGMA table_info("${escaped}")`);
|
|
63
|
+
const hasOwner = colsRes.rows.some((row) => (row.name ?? row[1]) === OWNER_COLUMN);
|
|
64
|
+
if (hasOwner)
|
|
65
|
+
withOwner.push(table);
|
|
66
|
+
}
|
|
67
|
+
return withOwner;
|
|
68
|
+
}
|
|
69
|
+
/** Replace `?` placeholders with `$1`, `$2`, … for Postgres. */
|
|
70
|
+
function sqlWithParams(sql) {
|
|
71
|
+
if (!isPostgres())
|
|
72
|
+
return sql;
|
|
73
|
+
let i = 0;
|
|
74
|
+
return sql.replace(/\?/g, () => `$${++i}`);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Rename `settings` keys so a user's config carries over. Keys are prefixed
|
|
78
|
+
* with `u:<email>:` — moving from one email to another is a prefix swap.
|
|
79
|
+
*
|
|
80
|
+
* If a destination key already exists (unlikely but possible if the user had
|
|
81
|
+
* previously signed in with the same email) we leave the destination alone
|
|
82
|
+
* and drop the local row, so we never clobber real-account state.
|
|
83
|
+
*/
|
|
84
|
+
async function migrateSettings(targetEmail) {
|
|
85
|
+
const client = getDbExec();
|
|
86
|
+
const oldPrefix = `u:${LOCAL_EMAIL}:`;
|
|
87
|
+
const newPrefix = `u:${targetEmail}:`;
|
|
88
|
+
const { rows } = await client.execute({
|
|
89
|
+
sql: sqlWithParams(`SELECT key FROM settings WHERE key LIKE ? ESCAPE '\\'`),
|
|
90
|
+
args: [oldPrefix.replace(/([\\%_])/g, "\\$1") + "%"],
|
|
91
|
+
});
|
|
92
|
+
let updated = 0;
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
const oldKey = (row.key ?? row[0]);
|
|
95
|
+
if (!oldKey.startsWith(oldPrefix))
|
|
96
|
+
continue;
|
|
97
|
+
const newKey = newPrefix + oldKey.slice(oldPrefix.length);
|
|
98
|
+
// Skip if the destination already exists — don't overwrite real data.
|
|
99
|
+
const existsRes = await client.execute({
|
|
100
|
+
sql: sqlWithParams(`SELECT 1 FROM settings WHERE key = ?`),
|
|
101
|
+
args: [newKey],
|
|
102
|
+
});
|
|
103
|
+
if (existsRes.rows.length > 0) {
|
|
104
|
+
await client.execute({
|
|
105
|
+
sql: sqlWithParams(`DELETE FROM settings WHERE key = ?`),
|
|
106
|
+
args: [oldKey],
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
await client.execute({
|
|
111
|
+
sql: sqlWithParams(`UPDATE settings SET key = ? WHERE key = ?`),
|
|
112
|
+
args: [newKey, oldKey],
|
|
113
|
+
});
|
|
114
|
+
updated++;
|
|
115
|
+
}
|
|
116
|
+
return updated;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Move application_state rows from `session_id='local'` to the target email.
|
|
120
|
+
* Rows that already exist for the destination session are left alone.
|
|
121
|
+
*/
|
|
122
|
+
async function migrateApplicationState(targetEmail) {
|
|
123
|
+
const client = getDbExec();
|
|
124
|
+
// Only migrate keys that don't already exist under the destination session.
|
|
125
|
+
const { rows } = await client.execute({
|
|
126
|
+
sql: sqlWithParams(`SELECT key FROM application_state WHERE session_id = ?`),
|
|
127
|
+
args: [LOCAL_SESSION_ID],
|
|
128
|
+
});
|
|
129
|
+
let updated = 0;
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
const key = (row.key ?? row[0]);
|
|
132
|
+
const existsRes = await client.execute({
|
|
133
|
+
sql: sqlWithParams(`SELECT 1 FROM application_state WHERE session_id = ? AND key = ?`),
|
|
134
|
+
args: [targetEmail, key],
|
|
135
|
+
});
|
|
136
|
+
if (existsRes.rows.length > 0) {
|
|
137
|
+
await client.execute({
|
|
138
|
+
sql: sqlWithParams(`DELETE FROM application_state WHERE session_id = ? AND key = ?`),
|
|
139
|
+
args: [LOCAL_SESSION_ID, key],
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
await client.execute({
|
|
144
|
+
sql: sqlWithParams(`UPDATE application_state SET session_id = ? WHERE session_id = ? AND key = ?`),
|
|
145
|
+
args: [targetEmail, LOCAL_SESSION_ID, key],
|
|
146
|
+
});
|
|
147
|
+
updated++;
|
|
148
|
+
}
|
|
149
|
+
return updated;
|
|
150
|
+
}
|
|
151
|
+
/** Move oauth_tokens rows. `owner` is the user's email in core tables. */
|
|
152
|
+
async function migrateOauthTokens(targetEmail) {
|
|
153
|
+
const client = getDbExec();
|
|
154
|
+
const res = await client.execute({
|
|
155
|
+
sql: sqlWithParams(`UPDATE oauth_tokens SET owner = ? WHERE owner = ?`),
|
|
156
|
+
args: [targetEmail, LOCAL_EMAIL],
|
|
157
|
+
});
|
|
158
|
+
return res.rowsAffected ?? 0;
|
|
159
|
+
}
|
|
160
|
+
/** Move rows in a template table that uses the `owner_email` convention. */
|
|
161
|
+
async function migrateOwnerEmailTable(table, targetEmail) {
|
|
162
|
+
const client = getDbExec();
|
|
163
|
+
const escaped = table.replace(/"/g, '""');
|
|
164
|
+
const res = await client.execute({
|
|
165
|
+
sql: sqlWithParams(`UPDATE "${escaped}" SET owner_email = ? WHERE owner_email = ?`),
|
|
166
|
+
args: [targetEmail, LOCAL_EMAIL],
|
|
167
|
+
});
|
|
168
|
+
return res.rowsAffected ?? 0;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Migrate every piece of local-mode data to the given real account email.
|
|
172
|
+
* Safe to call multiple times — it only touches rows that are still attached
|
|
173
|
+
* to `local@localhost`.
|
|
174
|
+
*/
|
|
175
|
+
export async function migrateLocalUserData(targetEmail) {
|
|
176
|
+
const email = targetEmail?.trim().toLowerCase();
|
|
177
|
+
if (!email || email === LOCAL_EMAIL) {
|
|
178
|
+
return { migrated: false, tables: {}, targetEmail: email || "" };
|
|
179
|
+
}
|
|
180
|
+
const tables = {};
|
|
181
|
+
// Core tables — best-effort. A missing table just means the feature isn't
|
|
182
|
+
// enabled in this app (e.g. an app that doesn't use oauth_tokens).
|
|
183
|
+
const coreSteps = [
|
|
184
|
+
["settings", () => migrateSettings(email)],
|
|
185
|
+
["application_state", () => migrateApplicationState(email)],
|
|
186
|
+
["oauth_tokens", () => migrateOauthTokens(email)],
|
|
187
|
+
];
|
|
188
|
+
const errors = [];
|
|
189
|
+
for (const [name, fn] of coreSteps) {
|
|
190
|
+
try {
|
|
191
|
+
const count = await fn();
|
|
192
|
+
if (count > 0)
|
|
193
|
+
tables[name] = count;
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
// Missing table or column — skip silently. Other errors are logged
|
|
197
|
+
// per-step so one bad table doesn't 500 the entire migration.
|
|
198
|
+
if (!shouldSkipSchemaError(err)) {
|
|
199
|
+
const message = err?.message ?? String(err);
|
|
200
|
+
errors.push({ step: name, message });
|
|
201
|
+
console.error(`[local-migration] ${name} failed:`, err);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Template tables — discovered dynamically. If discovery itself fails,
|
|
206
|
+
// fall back to an empty list so the migration doesn't 500.
|
|
207
|
+
let templateTables = [];
|
|
208
|
+
try {
|
|
209
|
+
templateTables = await discoverOwnerEmailTables();
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
console.error("[local-migration] owner_email table discovery failed:", err);
|
|
213
|
+
templateTables = [];
|
|
214
|
+
}
|
|
215
|
+
for (const table of templateTables) {
|
|
216
|
+
try {
|
|
217
|
+
const count = await migrateOwnerEmailTable(table, email);
|
|
218
|
+
if (count > 0)
|
|
219
|
+
tables[table] = count;
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
if (!shouldSkipSchemaError(err)) {
|
|
223
|
+
const message = err?.message ?? String(err);
|
|
224
|
+
errors.push({ step: table, message });
|
|
225
|
+
console.error(`[local-migration] ${table} failed:`, err);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const migrated = Object.values(tables).some((n) => n > 0);
|
|
230
|
+
const result = { migrated, tables, targetEmail: email };
|
|
231
|
+
if (errors.length > 0)
|
|
232
|
+
result.errors = errors;
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
//# sourceMappingURL=local-migration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-migration.js","sourceRoot":"","sources":["../../src/server/local-migration.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Migrate data owned by `local@localhost` to a real account.\n *\n * When a user starts an app in local mode and later signs in to create a real\n * account, this function moves all of their existing data over to the new\n * account so they don't lose anything.\n *\n * Scope of the migration:\n * - `application_state`: rows with `session_id = 'local'`\n * - `settings`: keys prefixed with `u:local@localhost:`\n * - `oauth_tokens`: rows with `owner = 'local@localhost'`\n * - Any template table that has an `owner_email` column: rows with\n * `owner_email = 'local@localhost'`\n *\n * The operation is a no-op if the target email is itself `local@localhost`,\n * empty, or if there is nothing to migrate.\n */\n\nimport { getDbExec, isPostgres } from \"../db/client.js\";\n\nconst LOCAL_EMAIL = \"local@localhost\";\nconst LOCAL_SESSION_ID = \"local\";\nconst OWNER_COLUMN = \"owner_email\";\n\nexport interface LocalMigrationResult {\n /** Whether anything was actually migrated. */\n migrated: boolean;\n /** Per-table row counts that were updated. Omits tables with zero updates. */\n tables: Record<string, number>;\n /** Target email the data now belongs to. */\n targetEmail: string;\n /**\n * Non-fatal per-step errors encountered during migration. One bad table\n * no longer fails the whole upgrade — we migrate everything we can and\n * report any steps that threw here so the UI can surface them.\n */\n errors?: Array<{ step: string; message: string }>;\n}\n\n/**\n * Error messages that indicate a missing/inaccessible table or column — the\n * migration treats these as \"feature not enabled\" and skips silently.\n */\nconst SCHEMA_SKIP_ERR =\n /no such table|no such column|does not exist|undefined table|undefined column|relation .* does not exist|column .* does not exist|permission denied|is not a table|cannot update view|cannot change column in a view/i;\n\nfunction shouldSkipSchemaError(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n return SCHEMA_SKIP_ERR.test(message);\n}\n\n/**\n * Discover every table (not view) in `public` that has an `owner_email`\n * column. Views and materialized views are excluded — they can't be updated\n * directly and would 500 the migration.\n */\nasync function discoverOwnerEmailTables(): Promise<string[]> {\n const client = getDbExec();\n if (isPostgres()) {\n // Join against information_schema.tables to filter out views and\n // materialized views (anything that isn't a plain BASE TABLE).\n const { rows } = await client.execute({\n sql: `SELECT c.table_name\n FROM information_schema.columns c\n JOIN information_schema.tables t\n ON t.table_schema = c.table_schema\n AND t.table_name = c.table_name\n WHERE c.table_schema = 'public'\n AND c.column_name = $1\n AND t.table_type = 'BASE TABLE'`,\n args: [OWNER_COLUMN],\n });\n return rows.map((r: any) => r.table_name ?? r[0]).filter(Boolean);\n }\n\n // SQLite path: iterate tables (type='table', not 'view') and inspect columns via PRAGMA\n const tablesRes = await client.execute(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`,\n );\n const tables = tablesRes.rows\n .map((r: any) => (r.name ?? r[0]) as string)\n .filter(Boolean);\n\n const withOwner: string[] = [];\n for (const table of tables) {\n const escaped = table.replace(/\"/g, '\"\"');\n const colsRes = await client.execute(`PRAGMA table_info(\"${escaped}\")`);\n const hasOwner = colsRes.rows.some(\n (row: any) => (row.name ?? row[1]) === OWNER_COLUMN,\n );\n if (hasOwner) withOwner.push(table);\n }\n return withOwner;\n}\n\n/** Replace `?` placeholders with `$1`, `$2`, … for Postgres. */\nfunction sqlWithParams(sql: string): string {\n if (!isPostgres()) return sql;\n let i = 0;\n return sql.replace(/\\?/g, () => `$${++i}`);\n}\n\n/**\n * Rename `settings` keys so a user's config carries over. Keys are prefixed\n * with `u:<email>:` — moving from one email to another is a prefix swap.\n *\n * If a destination key already exists (unlikely but possible if the user had\n * previously signed in with the same email) we leave the destination alone\n * and drop the local row, so we never clobber real-account state.\n */\nasync function migrateSettings(targetEmail: string): Promise<number> {\n const client = getDbExec();\n const oldPrefix = `u:${LOCAL_EMAIL}:`;\n const newPrefix = `u:${targetEmail}:`;\n\n const { rows } = await client.execute({\n sql: sqlWithParams(`SELECT key FROM settings WHERE key LIKE ? ESCAPE '\\\\'`),\n args: [oldPrefix.replace(/([\\\\%_])/g, \"\\\\$1\") + \"%\"],\n });\n\n let updated = 0;\n for (const row of rows) {\n const oldKey = (row.key ?? (row as any)[0]) as string;\n if (!oldKey.startsWith(oldPrefix)) continue;\n const newKey = newPrefix + oldKey.slice(oldPrefix.length);\n\n // Skip if the destination already exists — don't overwrite real data.\n const existsRes = await client.execute({\n sql: sqlWithParams(`SELECT 1 FROM settings WHERE key = ?`),\n args: [newKey],\n });\n if (existsRes.rows.length > 0) {\n await client.execute({\n sql: sqlWithParams(`DELETE FROM settings WHERE key = ?`),\n args: [oldKey],\n });\n continue;\n }\n\n await client.execute({\n sql: sqlWithParams(`UPDATE settings SET key = ? WHERE key = ?`),\n args: [newKey, oldKey],\n });\n updated++;\n }\n return updated;\n}\n\n/**\n * Move application_state rows from `session_id='local'` to the target email.\n * Rows that already exist for the destination session are left alone.\n */\nasync function migrateApplicationState(targetEmail: string): Promise<number> {\n const client = getDbExec();\n // Only migrate keys that don't already exist under the destination session.\n const { rows } = await client.execute({\n sql: sqlWithParams(\n `SELECT key FROM application_state WHERE session_id = ?`,\n ),\n args: [LOCAL_SESSION_ID],\n });\n\n let updated = 0;\n for (const row of rows) {\n const key = (row.key ?? (row as any)[0]) as string;\n const existsRes = await client.execute({\n sql: sqlWithParams(\n `SELECT 1 FROM application_state WHERE session_id = ? AND key = ?`,\n ),\n args: [targetEmail, key],\n });\n if (existsRes.rows.length > 0) {\n await client.execute({\n sql: sqlWithParams(\n `DELETE FROM application_state WHERE session_id = ? AND key = ?`,\n ),\n args: [LOCAL_SESSION_ID, key],\n });\n continue;\n }\n await client.execute({\n sql: sqlWithParams(\n `UPDATE application_state SET session_id = ? WHERE session_id = ? AND key = ?`,\n ),\n args: [targetEmail, LOCAL_SESSION_ID, key],\n });\n updated++;\n }\n return updated;\n}\n\n/** Move oauth_tokens rows. `owner` is the user's email in core tables. */\nasync function migrateOauthTokens(targetEmail: string): Promise<number> {\n const client = getDbExec();\n const res = await client.execute({\n sql: sqlWithParams(`UPDATE oauth_tokens SET owner = ? WHERE owner = ?`),\n args: [targetEmail, LOCAL_EMAIL],\n });\n return res.rowsAffected ?? 0;\n}\n\n/** Move rows in a template table that uses the `owner_email` convention. */\nasync function migrateOwnerEmailTable(\n table: string,\n targetEmail: string,\n): Promise<number> {\n const client = getDbExec();\n const escaped = table.replace(/\"/g, '\"\"');\n const res = await client.execute({\n sql: sqlWithParams(\n `UPDATE \"${escaped}\" SET owner_email = ? WHERE owner_email = ?`,\n ),\n args: [targetEmail, LOCAL_EMAIL],\n });\n return res.rowsAffected ?? 0;\n}\n\n/**\n * Migrate every piece of local-mode data to the given real account email.\n * Safe to call multiple times — it only touches rows that are still attached\n * to `local@localhost`.\n */\nexport async function migrateLocalUserData(\n targetEmail: string,\n): Promise<LocalMigrationResult> {\n const email = targetEmail?.trim().toLowerCase();\n if (!email || email === LOCAL_EMAIL) {\n return { migrated: false, tables: {}, targetEmail: email || \"\" };\n }\n\n const tables: Record<string, number> = {};\n\n // Core tables — best-effort. A missing table just means the feature isn't\n // enabled in this app (e.g. an app that doesn't use oauth_tokens).\n const coreSteps: Array<[string, () => Promise<number>]> = [\n [\"settings\", () => migrateSettings(email)],\n [\"application_state\", () => migrateApplicationState(email)],\n [\"oauth_tokens\", () => migrateOauthTokens(email)],\n ];\n const errors: Array<{ step: string; message: string }> = [];\n for (const [name, fn] of coreSteps) {\n try {\n const count = await fn();\n if (count > 0) tables[name] = count;\n } catch (err: any) {\n // Missing table or column — skip silently. Other errors are logged\n // per-step so one bad table doesn't 500 the entire migration.\n if (!shouldSkipSchemaError(err)) {\n const message = err?.message ?? String(err);\n errors.push({ step: name, message });\n console.error(`[local-migration] ${name} failed:`, err);\n }\n }\n }\n\n // Template tables — discovered dynamically. If discovery itself fails,\n // fall back to an empty list so the migration doesn't 500.\n let templateTables: string[] = [];\n try {\n templateTables = await discoverOwnerEmailTables();\n } catch (err) {\n console.error(\"[local-migration] owner_email table discovery failed:\", err);\n templateTables = [];\n }\n for (const table of templateTables) {\n try {\n const count = await migrateOwnerEmailTable(table, email);\n if (count > 0) tables[table] = count;\n } catch (err: any) {\n if (!shouldSkipSchemaError(err)) {\n const message = err?.message ?? String(err);\n errors.push({ step: table, message });\n console.error(`[local-migration] ${table} failed:`, err);\n }\n }\n }\n\n const migrated = Object.values(tables).some((n) => n > 0);\n const result: LocalMigrationResult = { migrated, tables, targetEmail: email };\n if (errors.length > 0) result.errors = errors;\n return result;\n}\n"]}
|
|
@@ -40,8 +40,8 @@ export interface OnboardingHtmlOptions {
|
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
42
|
* Google sign-in flow: `'popup'`, `'redirect'`, or `'auto'` (default).
|
|
43
|
-
* Falls back to `GOOGLE_AUTH_MODE` env var, then `'auto'`. Builder
|
|
44
|
-
* preview/editor surfaces
|
|
43
|
+
* Falls back to `GOOGLE_AUTH_MODE` env var, then `'auto'`. Builder web
|
|
44
|
+
* iframes use popup; Builder desktop preview/editor surfaces use redirect.
|
|
45
45
|
*/
|
|
46
46
|
googleAuthMode?: GoogleAuthMode;
|
|
47
47
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"onboarding-html.d.ts","sourceRoot":"","sources":["../../src/server/onboarding-html.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAmC/B,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,SAAS,CAAC,EAAE;QACV,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF;;;;OAIG;IACH,kBAAkB,CAAC,EAAE;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QACxB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF;;;;OAIG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,qBAA0B,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"onboarding-html.d.ts","sourceRoot":"","sources":["../../src/server/onboarding-html.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAmC/B,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,SAAS,CAAC,EAAE;QACV,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;KAC1B,CAAC;IACF;;;;OAIG;IACH,kBAAkB,CAAC,EAAE;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;QACxB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF;;;;OAIG;IACH,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC;AAED,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,qBAA0B,GAAG,MAAM,CA+qD1E;AAED,kDAAkD;AAClD,eAAO,MAAM,eAAe,QAAsB,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CA0G7C"}
|