@lotics/app-sdk 0.55.3 → 0.56.1
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/AGENTS.md +1 -1
- package/dist/src/rpc.js +34 -68
- package/dist/src/upload/optimize.d.ts +5 -4
- package/dist/src/upload/optimize.js +9 -6
- package/docs/ai.md +15 -3
- package/docs/runtime.md +10 -14
- package/package.json +1 -1
- package/dist/src/password_gate.d.ts +0 -29
- package/dist/src/password_gate.js +0 -142
package/AGENTS.md
CHANGED
|
@@ -20,7 +20,7 @@ signature; open the file.**
|
|
|
20
20
|
| [docs/files.md](./docs/files.md) | Files end to end — `useFileUpload`, `useAttachments`, `readFiles`/presigned URLs, workflow-generated files, preview pairing, filter operators, the server-side delivery bounds. |
|
|
21
21
|
| [docs/members_and_options.md](./docs/members_and_options.md) | People + select options + comments — `useMembers`, `useFieldOptions`, `useViewer`, `useComments`, and the `@lotics/ui` components they feed. |
|
|
22
22
|
| [docs/navigation_and_state.md](./docs/navigation_and_state.md) | `AppRouter` (embedded/standalone URL model), `useUrlState` + `urlParam` codecs, `useRecents`. |
|
|
23
|
-
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics, auto query-refetch on chat mutation). |
|
|
23
|
+
| [docs/ai.md](./docs/ai.md) | `useAgentRun` (structured vs free-text, streaming ai-sdk `parts` → `AgentRun`, the agent's ask-back — `pendingChoice`/`answerChoice` over the parked `awaiting_input` state), `askAi` — plus the fields-vs-file razor for choosing between them — and `useAiContext` (push the current screen's view state to the member's ambient chat agent; caps, push-only semantics, auto query-refetch on chat mutation). **A `file` input carries its own content** — images/PDFs are perceived natively, Word/Excel/CSV/text are materialized into the run; no reader tool to declare. |
|
|
24
24
|
| [docs/security.md](./docs/security.md) | **Read before shipping** — the owner-principal model, `is_current_member` scoping, write attribution, group gates, public-app bounds, what runtime refinement cannot widen. |
|
|
25
25
|
| [docs/runtime.md](./docs/runtime.md) | `mount()`, the two transports, `rpc()`, `openExternal`/`downloadFile`, geofencing, analytics, `useConfig` (App-Packages installation config), `getAppBinding` (package apps' runtime `F`/`OPT`/`ROLE` resolution via the generated `.lotics/app_fields.ts`), and the publish chain for package contributors. |
|
|
26
26
|
|
package/dist/src/rpc.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { promptForPassword } from "./password_gate.js";
|
|
2
1
|
import { runUploadPipeline } from "./upload/pipeline.js";
|
|
3
2
|
import { parseSearch, serializeMerge, } from "./url_params.js";
|
|
4
3
|
/**
|
|
@@ -346,44 +345,23 @@ let sessionToken = null;
|
|
|
346
345
|
* `APP_PUBLIC_SESSION_HEADER`; both sides are pinned by tests.
|
|
347
346
|
*/
|
|
348
347
|
export const APP_PUBLIC_SESSION_HEADER = "x-lotics-app-session";
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
return null;
|
|
348
|
+
/**
|
|
349
|
+
* The cookie the app-host Worker sets after the visitor clears the password
|
|
350
|
+
* gate. Readable by design — the SDK forwards its value as the session header
|
|
351
|
+
* on every data call. Same exposure as the `localStorage` token it replaced,
|
|
352
|
+
* minus the password prompt the SDK no longer owns.
|
|
353
|
+
*/
|
|
354
|
+
const SESSION_COOKIE = "lotics_app_session";
|
|
355
|
+
function readSessionCookie() {
|
|
356
|
+
for (const part of document.cookie.split(";")) {
|
|
357
|
+
const eq = part.indexOf("=");
|
|
358
|
+
if (eq < 0)
|
|
359
|
+
continue;
|
|
360
|
+
if (part.slice(0, eq).trim() === SESSION_COOKIE) {
|
|
361
|
+
return decodeURIComponent(part.slice(eq + 1).trim()) || null;
|
|
363
362
|
}
|
|
364
|
-
return parsed.token;
|
|
365
|
-
}
|
|
366
|
-
catch {
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
function writeStoredSession(appId, token, expiresAt) {
|
|
371
|
-
try {
|
|
372
|
-
window.localStorage.setItem(sessionStorageKey(appId), JSON.stringify({ token, expires_at: expiresAt }));
|
|
373
|
-
}
|
|
374
|
-
catch {
|
|
375
|
-
// localStorage unavailable (Safari private mode, quota): the token still
|
|
376
|
-
// lives in memory for this page lifetime — that's good enough for a single
|
|
377
|
-
// session, the next reload simply re-prompts.
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
function clearStoredSession(appId) {
|
|
381
|
-
try {
|
|
382
|
-
window.localStorage.removeItem(sessionStorageKey(appId));
|
|
383
|
-
}
|
|
384
|
-
catch {
|
|
385
|
-
// ignore
|
|
386
363
|
}
|
|
364
|
+
return null;
|
|
387
365
|
}
|
|
388
366
|
/**
|
|
389
367
|
* Resolve the app's identity + analytics config from its own subdomain. Shared
|
|
@@ -412,13 +390,12 @@ async function boot() {
|
|
|
412
390
|
const attempt = (async () => {
|
|
413
391
|
const info = await resolveAppInfo();
|
|
414
392
|
if (info.requires_password) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
393
|
+
// The visitor already passed the gate — the app-host Worker refuses to
|
|
394
|
+
// serve a single byte of this bundle otherwise — and handed us the session
|
|
395
|
+
// in a cookie on this origin. The SDK does not prompt: a password screen
|
|
396
|
+
// is the serving layer's job, which is why it can be one styled, localized
|
|
397
|
+
// page for every public app instead of hand-rolled DOM in a data library.
|
|
398
|
+
sessionToken = readSessionCookie();
|
|
422
399
|
}
|
|
423
400
|
return info;
|
|
424
401
|
})();
|
|
@@ -429,23 +406,6 @@ async function boot() {
|
|
|
429
406
|
bootPromise = attempt;
|
|
430
407
|
return attempt;
|
|
431
408
|
}
|
|
432
|
-
async function acquireSessionToken(appId) {
|
|
433
|
-
await promptForPassword({
|
|
434
|
-
authenticate: async (password, controller) => {
|
|
435
|
-
try {
|
|
436
|
-
const res = (await apiCall("POST", `/v1/apps/${appId}/public/authenticate`, { password }, { skipAuth: true }));
|
|
437
|
-
sessionToken = res.session_token;
|
|
438
|
-
writeStoredSession(appId, res.session_token, res.expires_at);
|
|
439
|
-
return { ok: true };
|
|
440
|
-
}
|
|
441
|
-
catch (err) {
|
|
442
|
-
const message = err instanceof Error && err.message ? err.message : "Incorrect password.";
|
|
443
|
-
controller.showError(message);
|
|
444
|
-
return { ok: false };
|
|
445
|
-
}
|
|
446
|
-
},
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
409
|
/**
|
|
450
410
|
* A user-facing message for a transport/gateway failure — derived from the HTTP
|
|
451
411
|
* status, never from the response body. A 524 (Cloudflare edge timeout on a long
|
|
@@ -528,15 +488,21 @@ async function apiCall(method, path, body, opts) {
|
|
|
528
488
|
}
|
|
529
489
|
if (!res.ok) {
|
|
530
490
|
const errorBody = parsed;
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
491
|
+
// The session went stale — the owner rotated or cleared the password, so the
|
|
492
|
+
// token's fingerprint no longer matches. Reloading is the whole recovery:
|
|
493
|
+
// the app-host Worker re-checks its own cookie and, if that is stale too,
|
|
494
|
+
// serves the gate. The SDK deliberately owns no re-prompt — the gate is a
|
|
495
|
+
// page at the serving layer, not a modal inside the data layer.
|
|
534
496
|
if (res.status === 401 && errorBody?.error_code === PASSWORD_REQUIRED_CODE && !opts?.skipAuth) {
|
|
535
|
-
const appId = opts?.appId ?? (await boot()).app_id;
|
|
536
497
|
sessionToken = null;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
498
|
+
// `/__gate`, not a plain reload: the edge cookie outlives the API session
|
|
499
|
+
// (rotation kills the session immediately, the cookie only on expiry), so
|
|
500
|
+
// reloading would be served the bundle again and loop forever. This route
|
|
501
|
+
// clears both cookies and re-presents the gate.
|
|
502
|
+
window.location.assign("/__gate");
|
|
503
|
+
// Never resolves — navigation replaces the document. Returning would let
|
|
504
|
+
// callers render an error state during teardown.
|
|
505
|
+
return new Promise(() => { });
|
|
540
506
|
}
|
|
541
507
|
// Never surface a non-JSON body (a gateway HTML error page) or a 5xx body as
|
|
542
508
|
// the message — emit a body-free, status-derived message instead.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser-side image compression for app uploads.
|
|
3
3
|
*
|
|
4
|
-
* Phone-camera photos are huge (4–10 MB HEIC/JPEG)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Phone-camera photos are huge (4–10 MB HEIC/JPEG) and no document needs
|
|
5
|
+
* megapixels. Resize to ≤1568px on the long edge — the vision model's own
|
|
6
|
+
* working resolution, above which the provider downsamples anyway — and
|
|
7
|
+
* re-encode as JPEG at q=0.9, which keeps small glyphs (container numbers,
|
|
8
|
+
* invoice lines) legible to a reader and to an extraction agent.
|
|
8
9
|
*
|
|
9
10
|
* HEIC/HEIF / PNG / WebP are converted to JPEG. Non-image files (PDF, etc.)
|
|
10
11
|
* pass through unchanged. Falls through gracefully on any browser API gap
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser-side image compression for app uploads.
|
|
3
3
|
*
|
|
4
|
-
* Phone-camera photos are huge (4–10 MB HEIC/JPEG)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Phone-camera photos are huge (4–10 MB HEIC/JPEG) and no document needs
|
|
5
|
+
* megapixels. Resize to ≤1568px on the long edge — the vision model's own
|
|
6
|
+
* working resolution, above which the provider downsamples anyway — and
|
|
7
|
+
* re-encode as JPEG at q=0.9, which keeps small glyphs (container numbers,
|
|
8
|
+
* invoice lines) legible to a reader and to an extraction agent.
|
|
8
9
|
*
|
|
9
10
|
* HEIC/HEIF / PNG / WebP are converted to JPEG. Non-image files (PDF, etc.)
|
|
10
11
|
* pass through unchanged. Falls through gracefully on any browser API gap
|
|
@@ -15,8 +16,10 @@
|
|
|
15
16
|
* dependency-free (no logger, no UploadIntent abstraction) so the SDK ships
|
|
16
17
|
* as a single drop-in.
|
|
17
18
|
*/
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
// MIRRORS `@lotics/shared/image_policy` — the SDK ships dependency-free so it
|
|
20
|
+
// cannot import it. Change both together.
|
|
21
|
+
const MAX_IMAGE_DIMENSION_PX = 1568;
|
|
22
|
+
const JPEG_QUALITY = 0.9;
|
|
20
23
|
const CONVERTIBLE_EXTENSION_PATTERN = /\.(heic|heif|png|webp)$/i;
|
|
21
24
|
export async function optimizeImageForUpload(file) {
|
|
22
25
|
const format = getOptimizableFormat(file);
|
package/docs/ai.md
CHANGED
|
@@ -20,7 +20,7 @@ A declaration carries:
|
|
|
20
20
|
| Field | Meaning |
|
|
21
21
|
|---|---|
|
|
22
22
|
| `instructions` | System instructions — the task the agent performs per run |
|
|
23
|
-
| `tool_names` | The tools the agent may call, resolved against the platform's automation tool registry. **This is the capability boundary** — the run can use nothing else. May be empty for a
|
|
23
|
+
| `tool_names` | The tools the agent may call, resolved against the platform's automation tool registry. **This is the capability boundary** — the run can use nothing else. May be empty — including for an agent that reads documents, since a [`file` input carries its own content](#file-inputs--what-the-agent-can-actually-see) |
|
|
24
24
|
| `model_id` | The chat model the agent runs on |
|
|
25
25
|
| `effort_level` | Optional reasoning depth for adaptive-thinking models |
|
|
26
26
|
| `inputs` | Optional typed input schema for one run — the same vocabulary as workflow inputs (`text`, `number`, `file`, `member`, `record_link`, `select`, …). The server validates every run payload against it |
|
|
@@ -181,9 +181,21 @@ On recovery, `output` adopts the row's output **only when it is an object** (a s
|
|
|
181
181
|
|
|
182
182
|
Sessions are scoped to the authenticated member who ran them: two members using the same `sessionId` string share nothing, and a member can never read or extend another member's thread.
|
|
183
183
|
|
|
184
|
-
### File inputs
|
|
184
|
+
### File inputs — what the agent can actually see
|
|
185
185
|
|
|
186
|
-
A
|
|
186
|
+
**A `file` input carries its own content. You do not declare a tool to read it.**
|
|
187
|
+
|
|
188
|
+
| Tier | Types | How the agent gets it |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| **Perceived natively** | `image/*`, `application/pdf` | A vision / document part — the agent literally sees it |
|
|
191
|
+
| **Materialized** | Word (`.docx`/`.doc`), Excel (`.xlsx`/`.xls`), CSV, and text (`.txt`, `.md`, `.json`, `.eml`, `.html`, `.xml`, `.yaml`) | Read server-side by the same engines `view_files` uses and inlined into the run's message, truncated at 40,000 characters with the agent told when that happened |
|
|
192
|
+
| **Unreadable** | Archives, audio, video | The run fails immediately, naming the file — a fact about the upload, not a gap in your configuration |
|
|
193
|
+
|
|
194
|
+
Every file in a `multi` input is materialized — nothing is collapsed to the first. Get the ids from [`useFileUpload` / `useAttachments`](./files.md).
|
|
195
|
+
|
|
196
|
+
**Why no tool:** the content ends up in the agent's context either way, so making it fetch what the server already holds costs a model round-trip and buys nothing. It also keeps the capability boundary tight — an agent that reads its own input never gains the ability to read *other* files in the workspace.
|
|
197
|
+
|
|
198
|
+
**When to add a tool anyway.** Only to reach *past* what was inlined. A spreadsheet is materialized at 100 rows × 50 columns per sheet and a Word file at 1,000 elements; overflow is reported, never silent. If the agent must read row 4,000 of a large sheet, add `excel_get_range` / `excel_find_cells` (or `view_files` for an ad-hoc second look at a different file).
|
|
187
199
|
|
|
188
200
|
### Auth, quota, and bounds
|
|
189
201
|
|
package/docs/runtime.md
CHANGED
|
@@ -129,20 +129,16 @@ fails. Dev apps are always bridged; standalone mode exists only on the deployed
|
|
|
129
129
|
On first data call the SDK resolves the app's identity from its own subdomain
|
|
130
130
|
(one shared `GET /v1/apps/by-subdomain/{slug}` fetch — concurrent first calls
|
|
131
131
|
coalesce; a transient failure isn't cached, the next call retries). If the app
|
|
132
|
-
is password-protected, the
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
`lotics_app_session
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
retries the original call once. There is no cancel button — the visitor enters
|
|
143
|
-
the password or leaves. When `localStorage` is unavailable (private browsing,
|
|
144
|
-
quota) the token lives in memory for the page's lifetime and the next reload
|
|
145
|
-
re-prompts. What the password does and doesn't protect: [security](./security.md).
|
|
132
|
+
is password-protected, the visitor never reaches the app at all until they
|
|
133
|
+
clear the gate: `<slug>.lotics.app` is served by the app-host Worker, which
|
|
134
|
+
withholds every byte of the bundle and serves a password page instead. On
|
|
135
|
+
success it sets `lotics_app_session` on the app's origin; the SDK reads that
|
|
136
|
+
cookie and forwards it as `X-Lotics-App-Session` on data calls. The SDK owns no
|
|
137
|
+
password UI — a gate is a property of *serving* an app, not of fetching data,
|
|
138
|
+
so it lives at the serving layer where one styled, localized page covers every
|
|
139
|
+
public app. A `401` with error code `PASSWORD_REQUIRED` (the owner rotated or
|
|
140
|
+
cleared the password) reloads the page, which puts the visitor back through the
|
|
141
|
+
gate. What the password does and doesn't protect: [security](./security.md).
|
|
146
142
|
|
|
147
143
|
### Transport error semantics
|
|
148
144
|
|
package/package.json
CHANGED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vanilla DOM password overlay for password-protected public apps.
|
|
3
|
-
*
|
|
4
|
-
* The SDK can't assume the host app's React tree has mounted yet (this fires
|
|
5
|
-
* before any data call resolves), so the overlay is built with plain DOM
|
|
6
|
-
* APIs and self-contained inline styles. It rejects the boot promise only
|
|
7
|
-
* on `cancel()` — which is currently never wired to a button; the visitor
|
|
8
|
-
* either enters the correct password or leaves the page.
|
|
9
|
-
*
|
|
10
|
-
* One overlay at a time. Concurrent calls reuse the in-flight promise so a
|
|
11
|
-
* burst of failing API calls produces a single modal, not several stacked.
|
|
12
|
-
*/
|
|
13
|
-
interface OverlayController {
|
|
14
|
-
showError(message: string): void;
|
|
15
|
-
setSubmitting(submitting: boolean): void;
|
|
16
|
-
}
|
|
17
|
-
export interface PasswordPromptArgs {
|
|
18
|
-
/**
|
|
19
|
-
* Validates the entered password. Resolve to keep the overlay open on a
|
|
20
|
-
* wrong password (the SDK calls `controller.showError(...)` afterwards),
|
|
21
|
-
* or `accept()` to dismiss the overlay and resolve the prompt with the
|
|
22
|
-
* password the visitor typed.
|
|
23
|
-
*/
|
|
24
|
-
authenticate: (password: string, controller: OverlayController) => Promise<{
|
|
25
|
-
ok: boolean;
|
|
26
|
-
}>;
|
|
27
|
-
}
|
|
28
|
-
export declare function promptForPassword(args: PasswordPromptArgs): Promise<string>;
|
|
29
|
-
export {};
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Vanilla DOM password overlay for password-protected public apps.
|
|
3
|
-
*
|
|
4
|
-
* The SDK can't assume the host app's React tree has mounted yet (this fires
|
|
5
|
-
* before any data call resolves), so the overlay is built with plain DOM
|
|
6
|
-
* APIs and self-contained inline styles. It rejects the boot promise only
|
|
7
|
-
* on `cancel()` — which is currently never wired to a button; the visitor
|
|
8
|
-
* either enters the correct password or leaves the page.
|
|
9
|
-
*
|
|
10
|
-
* One overlay at a time. Concurrent calls reuse the in-flight promise so a
|
|
11
|
-
* burst of failing API calls produces a single modal, not several stacked.
|
|
12
|
-
*/
|
|
13
|
-
let activePrompt = null;
|
|
14
|
-
export function promptForPassword(args) {
|
|
15
|
-
if (activePrompt)
|
|
16
|
-
return activePrompt;
|
|
17
|
-
activePrompt = new Promise((resolve) => {
|
|
18
|
-
const root = document.createElement("div");
|
|
19
|
-
root.dataset.loticsAppPasswordGate = "true";
|
|
20
|
-
root.style.cssText = [
|
|
21
|
-
"position:fixed",
|
|
22
|
-
"inset:0",
|
|
23
|
-
"z-index:2147483647",
|
|
24
|
-
"background:rgba(15,23,42,0.55)",
|
|
25
|
-
"backdrop-filter:blur(8px)",
|
|
26
|
-
"-webkit-backdrop-filter:blur(8px)",
|
|
27
|
-
"display:flex",
|
|
28
|
-
"align-items:center",
|
|
29
|
-
"justify-content:center",
|
|
30
|
-
"padding:24px",
|
|
31
|
-
"font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Inter,sans-serif",
|
|
32
|
-
"color:#0f172a",
|
|
33
|
-
].join(";");
|
|
34
|
-
const card = document.createElement("form");
|
|
35
|
-
card.setAttribute("role", "dialog");
|
|
36
|
-
card.setAttribute("aria-modal", "true");
|
|
37
|
-
card.setAttribute("aria-labelledby", "lotics-app-password-title");
|
|
38
|
-
card.style.cssText = [
|
|
39
|
-
"background:#fff",
|
|
40
|
-
"border-radius:14px",
|
|
41
|
-
"padding:28px",
|
|
42
|
-
"max-width:380px",
|
|
43
|
-
"width:100%",
|
|
44
|
-
"box-shadow:0 25px 50px -12px rgba(0,0,0,0.25)",
|
|
45
|
-
"display:flex",
|
|
46
|
-
"flex-direction:column",
|
|
47
|
-
"gap:16px",
|
|
48
|
-
].join(";");
|
|
49
|
-
const title = document.createElement("h2");
|
|
50
|
-
title.id = "lotics-app-password-title";
|
|
51
|
-
title.textContent = "Password required";
|
|
52
|
-
title.style.cssText = "margin:0;font-size:18px;font-weight:600;letter-spacing:-0.01em;";
|
|
53
|
-
const subtitle = document.createElement("p");
|
|
54
|
-
subtitle.textContent = "Enter the password to access this app.";
|
|
55
|
-
subtitle.style.cssText = "margin:0;color:#475569;font-size:13.5px;";
|
|
56
|
-
const input = document.createElement("input");
|
|
57
|
-
input.type = "password";
|
|
58
|
-
input.autocomplete = "current-password";
|
|
59
|
-
input.required = true;
|
|
60
|
-
input.placeholder = "Password";
|
|
61
|
-
input.setAttribute("aria-label", "Password");
|
|
62
|
-
input.style.cssText = [
|
|
63
|
-
"width:100%",
|
|
64
|
-
"box-sizing:border-box",
|
|
65
|
-
"padding:10px 12px",
|
|
66
|
-
"border:1px solid #cbd5e1",
|
|
67
|
-
"border-radius:8px",
|
|
68
|
-
"font-size:14px",
|
|
69
|
-
"outline:none",
|
|
70
|
-
"transition:border-color 0.15s,box-shadow 0.15s",
|
|
71
|
-
].join(";");
|
|
72
|
-
input.addEventListener("focus", () => {
|
|
73
|
-
input.style.borderColor = "#2563eb";
|
|
74
|
-
input.style.boxShadow = "0 0 0 3px rgba(37,99,235,0.18)";
|
|
75
|
-
});
|
|
76
|
-
input.addEventListener("blur", () => {
|
|
77
|
-
input.style.borderColor = "#cbd5e1";
|
|
78
|
-
input.style.boxShadow = "none";
|
|
79
|
-
});
|
|
80
|
-
const error = document.createElement("div");
|
|
81
|
-
error.setAttribute("role", "alert");
|
|
82
|
-
error.style.cssText = "color:#dc2626;font-size:13px;min-height:18px;";
|
|
83
|
-
const submit = document.createElement("button");
|
|
84
|
-
submit.type = "submit";
|
|
85
|
-
submit.textContent = "Continue";
|
|
86
|
-
submit.style.cssText = [
|
|
87
|
-
"padding:10px 12px",
|
|
88
|
-
"background:#0f172a",
|
|
89
|
-
"color:#fff",
|
|
90
|
-
"border:none",
|
|
91
|
-
"border-radius:8px",
|
|
92
|
-
"font-size:14px",
|
|
93
|
-
"font-weight:500",
|
|
94
|
-
"cursor:pointer",
|
|
95
|
-
"transition:background 0.15s,opacity 0.15s",
|
|
96
|
-
].join(";");
|
|
97
|
-
const setSubmitting = (s) => {
|
|
98
|
-
submit.disabled = s;
|
|
99
|
-
input.disabled = s;
|
|
100
|
-
submit.textContent = s ? "Checking…" : "Continue";
|
|
101
|
-
submit.style.opacity = s ? "0.7" : "1";
|
|
102
|
-
submit.style.cursor = s ? "wait" : "pointer";
|
|
103
|
-
};
|
|
104
|
-
const showError = (message) => {
|
|
105
|
-
error.textContent = message;
|
|
106
|
-
};
|
|
107
|
-
card.append(title, subtitle, input, error, submit);
|
|
108
|
-
root.append(card);
|
|
109
|
-
document.body.append(root);
|
|
110
|
-
// Defer focus until the element is in layout — some browsers ignore focus
|
|
111
|
-
// on freshly-attached inputs without a microtask break.
|
|
112
|
-
queueMicrotask(() => input.focus());
|
|
113
|
-
card.addEventListener("submit", (e) => {
|
|
114
|
-
e.preventDefault();
|
|
115
|
-
const value = input.value;
|
|
116
|
-
if (!value) {
|
|
117
|
-
showError("Enter a password.");
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
showError("");
|
|
121
|
-
setSubmitting(true);
|
|
122
|
-
args
|
|
123
|
-
.authenticate(value, { showError, setSubmitting })
|
|
124
|
-
.then((res) => {
|
|
125
|
-
if (res.ok) {
|
|
126
|
-
root.remove();
|
|
127
|
-
activePrompt = null;
|
|
128
|
-
resolve(value);
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
setSubmitting(false);
|
|
132
|
-
input.select();
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
.catch((err) => {
|
|
136
|
-
setSubmitting(false);
|
|
137
|
-
showError(err instanceof Error ? err.message : "Something went wrong.");
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
return activePrompt;
|
|
142
|
-
}
|