scenri 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +3 -3
- package/dist/{chunk-LMXHCXUC.js → chunk-HI7CG4TK.js} +7 -4
- package/dist/{cli-NXOTZTBT.js → cli-Y5X5XXFA.js} +3 -3
- package/dist/index.js +2 -2
- package/dist/serve.js +925 -100
- package/dist/{src-XRZP3ZB5.js → src-VQCTLFGX.js} +3 -3
- package/package.json +1 -1
- package/studio-dist/assets/{index-CJQMppMU.css → index-BTaMj1F0.css} +1 -1
- package/studio-dist/assets/index-te6uQiVY.js +106 -0
- package/studio-dist/index.html +2 -2
- package/studio-dist/assets/index-D1DGL2hD.js +0 -104
package/dist/serve.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { createUpdateChecker, classify, findNpm, isReleaseTriplet, stageVersion } from './chunk-YK37H6DM.js';
|
|
2
2
|
import { shouldAdoptRunning, anotherScenriLines, portBusyLines } from './chunk-CMLJGPYC.js';
|
|
3
|
-
import { SchemaTooNewError, createCore, BUDGET_EXHAUSTED, SpendCapError, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, nameMatches, budgetSize, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-
|
|
3
|
+
import { SchemaTooNewError, createCore, BUDGET_EXHAUSTED, SpendCapError, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, nameMatches, budgetSize, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-HI7CG4TK.js';
|
|
4
4
|
import { detectInstallKind } from './chunk-PAIAXRAC.js';
|
|
5
5
|
export { detectInstallKind } from './chunk-PAIAXRAC.js';
|
|
6
6
|
import { readMeta, repoSlug } from './chunk-Y3ZPBPLP.js';
|
|
7
7
|
import { compareSemver, newestStaged } from './chunk-4MAFHYAD.js';
|
|
8
|
+
import { powershellPath, PS_UTF8 } from './chunk-2IGINCPU.js';
|
|
8
9
|
import { dirname, join, normalize } from 'path';
|
|
9
10
|
import { fileURLToPath } from 'url';
|
|
10
11
|
import fs, { existsSync, readFileSync, mkdirSync, createReadStream, rmSync, readdirSync, statSync, renameSync } from 'fs';
|
|
11
|
-
import { homedir,
|
|
12
|
-
import { randomBytes, randomUUID, timingSafeEqual, createHash } from 'crypto';
|
|
12
|
+
import { homedir, tmpdir, networkInterfaces } from 'os';
|
|
13
13
|
import { copyFile, readFile, stat, readdir, mkdtemp, rm, access, rename, unlink, writeFile } from 'fs/promises';
|
|
14
|
-
import { spawn } from 'child_process';
|
|
14
|
+
import { spawn, execFile } from 'child_process';
|
|
15
15
|
import sharp21 from 'sharp';
|
|
16
16
|
import Fastify from 'fastify';
|
|
17
17
|
import fastifyStatic from '@fastify/static';
|
|
18
18
|
import fastifyMultipart from '@fastify/multipart';
|
|
19
|
+
import { randomUUID, randomInt, timingSafeEqual, createHash } from 'crypto';
|
|
19
20
|
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
20
21
|
import addFormats from 'ajv-formats';
|
|
21
22
|
import * as cheerio from 'cheerio';
|
|
@@ -23,6 +24,8 @@ import { lookup } from 'dns/promises';
|
|
|
23
24
|
import { isIP } from 'net';
|
|
24
25
|
import JSZip from 'jszip';
|
|
25
26
|
import { parse } from 'node-html-parser';
|
|
27
|
+
import { createServer } from 'http';
|
|
28
|
+
import { createSocket } from 'dgram';
|
|
26
29
|
import pixelmatch from 'pixelmatch';
|
|
27
30
|
import { PNG } from 'pngjs';
|
|
28
31
|
|
|
@@ -904,7 +907,7 @@ function createRunner(opts = {}) {
|
|
|
904
907
|
const env = buildChildEnv(parentEnv, ignoreEnvKeys());
|
|
905
908
|
return exe.direct ? spawnImpl(exe.command, args, { stdio, env, ...platform !== "win32" ? { detached: true } : {} }) : spawnImpl([exe.command, ...args.map(winArg)].join(" "), [], { stdio, env, shell: true });
|
|
906
909
|
};
|
|
907
|
-
async function
|
|
910
|
+
async function run4(args, signal, io) {
|
|
908
911
|
const exe = await resolution();
|
|
909
912
|
const timeoutMs = io?.timeoutMs ?? defaultTimeoutMs;
|
|
910
913
|
return new Promise((resolve, reject) => {
|
|
@@ -1195,7 +1198,7 @@ function createRunner(opts = {}) {
|
|
|
1195
1198
|
let result;
|
|
1196
1199
|
try {
|
|
1197
1200
|
await withWorkDir(
|
|
1198
|
-
(dir) =>
|
|
1201
|
+
(dir) => run4(execArgs(dir), void 0, {
|
|
1199
1202
|
stdin: CONNECT_PROMPT,
|
|
1200
1203
|
timeoutMs: connectTimeoutMs,
|
|
1201
1204
|
label: "connect"
|
|
@@ -1236,7 +1239,7 @@ function createRunner(opts = {}) {
|
|
|
1236
1239
|
connCache = null;
|
|
1237
1240
|
cached = null;
|
|
1238
1241
|
}
|
|
1239
|
-
return { run:
|
|
1242
|
+
return { run: run4, withWorkDir, probe, invalidateProbe, connect, invalidateConnection, noteConnection };
|
|
1240
1243
|
}
|
|
1241
1244
|
var PHOTO_VIEWS = [
|
|
1242
1245
|
"portrait",
|
|
@@ -1513,7 +1516,7 @@ function createCodexSetup(opts = {}) {
|
|
|
1513
1516
|
const runner = opts.runner ?? createRunner(opts);
|
|
1514
1517
|
const installTimeoutMs = opts.installTimeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
|
|
1515
1518
|
const ignoreEnvKeys = opts.ignoreEnvKeys ?? (() => []);
|
|
1516
|
-
function
|
|
1519
|
+
function run4(cmd, args, timeoutMs) {
|
|
1517
1520
|
return new Promise((resolve) => {
|
|
1518
1521
|
let settled = false;
|
|
1519
1522
|
let stderr = "";
|
|
@@ -1562,7 +1565,7 @@ function createCodexSetup(opts = {}) {
|
|
|
1562
1565
|
};
|
|
1563
1566
|
},
|
|
1564
1567
|
async install() {
|
|
1565
|
-
const res = await
|
|
1568
|
+
const res = await run4("npm", ["install", "-g", "@openai/codex"], installTimeoutMs);
|
|
1566
1569
|
if (res.code === 0) {
|
|
1567
1570
|
runner.invalidateProbe();
|
|
1568
1571
|
const avail = await runner.probe();
|
|
@@ -1596,7 +1599,7 @@ function createCodexSetup(opts = {}) {
|
|
|
1596
1599
|
return { ok: false, fallbackCommand: INSTALL_COMMAND, docsUrl: INSTALL_DOCS_URL, detail };
|
|
1597
1600
|
},
|
|
1598
1601
|
async login() {
|
|
1599
|
-
const res = await
|
|
1602
|
+
const res = await run4("codex", ["login"], installTimeoutMs);
|
|
1600
1603
|
runner.invalidateProbe();
|
|
1601
1604
|
if (res.code === 0) return { ok: true };
|
|
1602
1605
|
const detail = (res.spawnError ?? res.stderr).trim().slice(0, 400) || void 0;
|
|
@@ -1969,7 +1972,7 @@ function createDemoEngine(saveImage, opts = {}) {
|
|
|
1969
1972
|
);
|
|
1970
1973
|
return hexes.length ? hexes : ["#4B5563", "#9CA3AF"];
|
|
1971
1974
|
};
|
|
1972
|
-
const
|
|
1975
|
+
const esc2 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").slice(0, 90);
|
|
1973
1976
|
async function render(colors, label, w, h, seed) {
|
|
1974
1977
|
const c0 = colors[seed % colors.length];
|
|
1975
1978
|
const c1 = colors[(seed + 1) % colors.length] ?? "#111111";
|
|
@@ -1979,7 +1982,7 @@ function createDemoEngine(saveImage, opts = {}) {
|
|
|
1979
1982
|
</linearGradient></defs>
|
|
1980
1983
|
<rect width="100%" height="100%" fill="url(#g)"/>
|
|
1981
1984
|
<circle cx="${w * (0.25 + 0.5 * (seed * 37 % 100) / 100)}" cy="${h * 0.38}" r="${Math.min(w, h) * 0.18}" fill="#ffffff" opacity="0.25"/>
|
|
1982
|
-
<text x="24" y="${h - 48}" font-family="Helvetica, Arial" font-size="${Math.max(14, Math.round(w / 42))}" fill="#ffffff" opacity="0.92">${
|
|
1985
|
+
<text x="24" y="${h - 48}" font-family="Helvetica, Arial" font-size="${Math.max(14, Math.round(w / 42))}" fill="#ffffff" opacity="0.92">${esc2(label)}</text>
|
|
1983
1986
|
<text x="24" y="${h - 22}" font-family="Helvetica, Arial" font-size="12" fill="#ffffff" opacity="0.6">Scenri demo engine</text>
|
|
1984
1987
|
</svg>`;
|
|
1985
1988
|
return sharp21(Buffer.from(svg)).png().toBuffer();
|
|
@@ -7555,18 +7558,18 @@ async function runJob(deps, jobId, brandId, url, signal, only) {
|
|
|
7555
7558
|
patch3({ stage: "fetching_products", discovered, message: `Reading ${discovered.toLocaleString()} products` });
|
|
7556
7559
|
}
|
|
7557
7560
|
}
|
|
7558
|
-
const
|
|
7561
|
+
const run4 = beginWrite(deps, jobId, brandId, baseUrl, platform, tally2, discovered);
|
|
7559
7562
|
const pictures = drainPictures(deps, jobId, brandId, tally2, signal, hostOf(baseUrl));
|
|
7560
7563
|
let pictureErrors;
|
|
7561
7564
|
let pictureReasons = {};
|
|
7562
7565
|
const stats = { pages: 0, bytes: 0, refused: 0, reasons: {} };
|
|
7563
7566
|
try {
|
|
7564
|
-
if (bulk) for (const p of bulk)
|
|
7567
|
+
if (bulk) for (const p of bulk) run4.write(p);
|
|
7565
7568
|
if (urls.length)
|
|
7566
7569
|
await fetchProductPages(ctx, urls, {
|
|
7567
7570
|
concurrency: IMPORT_CONCURRENCY,
|
|
7568
7571
|
maxBytes: 15e5,
|
|
7569
|
-
onEach:
|
|
7572
|
+
onEach: run4.write,
|
|
7570
7573
|
stats,
|
|
7571
7574
|
// A run that cannot finish still has to end. Generous enough for a
|
|
7572
7575
|
// 2,200 page catalogue, which measures around sixteen minutes, and
|
|
@@ -7599,10 +7602,10 @@ async function runJob(deps, jobId, brandId, url, signal, only) {
|
|
|
7599
7602
|
message: stoppedMessage(tally2.upserted),
|
|
7600
7603
|
finished: true
|
|
7601
7604
|
});
|
|
7602
|
-
core.catalog.setSourceStatus(
|
|
7605
|
+
core.catalog.setSourceStatus(run4.sourceId, "partial", true);
|
|
7603
7606
|
return;
|
|
7604
7607
|
}
|
|
7605
|
-
|
|
7608
|
+
run4.finish({
|
|
7606
7609
|
sweep,
|
|
7607
7610
|
warnings,
|
|
7608
7611
|
errors: pictureErrors,
|
|
@@ -7960,7 +7963,7 @@ async function edgeBarGeometry(buf) {
|
|
|
7960
7963
|
mean[i] = m;
|
|
7961
7964
|
sd[i] = Math.sqrt(v / cross);
|
|
7962
7965
|
}
|
|
7963
|
-
const
|
|
7966
|
+
const run4 = (from, dir) => {
|
|
7964
7967
|
let n = 0;
|
|
7965
7968
|
for (let i = from; n < len * 0.2; i += dir, n++) {
|
|
7966
7969
|
if (sd[i] >= 0.6 || Math.abs(mean[i] - mean[from]) >= 1) break;
|
|
@@ -7976,7 +7979,7 @@ async function edgeBarGeometry(buf) {
|
|
|
7976
7979
|
const isd = probe.reduce((a, i) => a + sd[i], 0) / probe.length;
|
|
7977
7980
|
return Math.abs(mean[from] - im) > 2 && isd < 5 ? n : 0;
|
|
7978
7981
|
};
|
|
7979
|
-
return { lo:
|
|
7982
|
+
return { lo: run4(0, 1), hi: run4(len - 1, -1) };
|
|
7980
7983
|
};
|
|
7981
7984
|
const cols = scan(W, H, (x, y) => data[y * W + x]);
|
|
7982
7985
|
const rows = scan(H, W, (y, x) => data[y * W + x]);
|
|
@@ -8296,9 +8299,682 @@ function cancelSceneBuilds(brandId, sceneId) {
|
|
|
8296
8299
|
if (b.brandId === brandId && b.sceneId === sceneId && !b.finished) cancelAssetBuild(b.id);
|
|
8297
8300
|
}
|
|
8298
8301
|
}
|
|
8299
|
-
|
|
8302
|
+
|
|
8303
|
+
// src/network/codePage.ts
|
|
8304
|
+
var LOCKUP_PATHS = [
|
|
8305
|
+
"M64 18.71V38.8L51.38 51.39V12.62H57.9C61.27 12.62 64 15.35 64 18.72V18.71Z",
|
|
8306
|
+
"M12.62 12.62V51.39H6.1C2.73 51.39 0 48.66 0 45.29V25.2L12.62 12.61V12.62Z",
|
|
8307
|
+
"M51.38 51.38L38.79 64H18.7C15.33 64 12.6 61.27 12.6 57.9V51.38H51.37H51.38Z",
|
|
8308
|
+
"M12.62 12.62L25.2 0H45.29C48.66 0 51.39 2.73 51.39 6.1V12.62H12.62Z",
|
|
8309
|
+
"M100.99 28.93L91.76 27.8C88.5 27.41 87.19 26.58 87.19 25.1C87.19 23.45 89.98 22.49 94.29 22.49C99.38 22.49 102.43 23.36 104.17 26.58L111.61 24.27C108.65 17.87 102.08 16 94.98 16C85.1 16 78.7 19.31 78.7 25.97C78.7 31.02 82.49 33.81 90.28 34.76L98.68 35.8C102.29 36.24 103.3 37.19 103.3 38.5C103.3 40.24 100.64 41.46 95.59 41.46C89.41 41.46 87.01 40.2 85.31 37.02L77.52 39.33C80.31 45.77 86.92 47.95 95.07 47.95C105.17 47.95 111.88 44.55 111.88 37.72C111.88 32.84 108.53 29.84 101 28.93H100.99Z",
|
|
8310
|
+
"M132.73 22.66C137 22.66 140.87 24.01 142.7 28.54L150.49 26.67C148.79 20.66 142.3 16.05 132.6 16.05C121.41 16.05 113.44 22.23 113.44 31.98C113.44 41.73 121.41 48 132.6 48C142.31 48 148.8 43.3 150.49 37.29L142.7 35.42C140.87 39.95 137 41.3 132.73 41.3C126.94 41.3 122.24 38.38 122.24 32.42V31.55C122.24 25.59 126.94 22.67 132.73 22.67V22.66Z",
|
|
8311
|
+
"M161.51 34.89H179.58V28.71H161.51V22.97H181.54V16.52H152.8V47.43H181.84V40.99H161.51V34.89Z",
|
|
8312
|
+
"M208.71 33.85H208.67L193.86 16.52H184.94V47.43H193.04V27.49H193.08L210.15 47.43H216.81V16.52H208.71V33.85Z",
|
|
8313
|
+
"M252.25 26.8C252.25 20.14 248.07 16.52 239.84 16.52H220.77V47.43H229.48V37.5H236.62L242.63 47.43H252.56L245.55 36.37C249.9 34.8 252.25 31.62 252.25 26.79V26.8ZM243.54 27.32C243.54 29.98 242.06 31.28 239.06 31.28H229.48V22.96H239.06C242.06 22.96 243.54 24.27 243.54 26.88V27.32Z",
|
|
8314
|
+
"M263.79 16.52H255.08V47.43H263.79V16.52Z"
|
|
8315
|
+
];
|
|
8316
|
+
var CODE_PAGE_COPY = {
|
|
8317
|
+
title: "Enter your code",
|
|
8318
|
+
where: "Find it in Scenri on your computer, under Settings > Local access.",
|
|
8319
|
+
label: "6-digit code",
|
|
8320
|
+
open: "Open Scenri",
|
|
8321
|
+
opening: "Opening\u2026",
|
|
8322
|
+
foot: "Your code keeps Scenri private.",
|
|
8323
|
+
wrong: "That code didn't work. Try again.",
|
|
8324
|
+
locked: "Too many tries. Try again in a few minutes.",
|
|
8325
|
+
short: "Enter all 6 digits.",
|
|
8326
|
+
offline: "Can't reach Scenri. Check your Wi-Fi."
|
|
8327
|
+
};
|
|
8328
|
+
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
8329
|
+
var symbolIcon = () => {
|
|
8330
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><style>path{fill:#0a0a0a}@media(prefers-color-scheme:dark){path{fill:#f5f5f5}}</style>${LOCKUP_PATHS.slice(
|
|
8331
|
+
0,
|
|
8332
|
+
4
|
|
8333
|
+
).map((d) => `<path d="${d}"/>`).join("")}</svg>`;
|
|
8334
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
8335
|
+
};
|
|
8336
|
+
var STYLE = `
|
|
8337
|
+
:root {
|
|
8338
|
+
color-scheme: light dark;
|
|
8339
|
+
--page: #f6f6f6; --card: #ffffff; --ink: #0a0a0a; --on-ink: #ffffff; --soft: #666666; --faint: #767676;
|
|
8340
|
+
--line: #e8e8e8; --field: #f4f4f4; --field-line: #e4e4e4; --ring: rgba(10, 10, 10, 0.08);
|
|
8341
|
+
--off: #e6e6e6; --off-ink: #9a9a9a; --red: #d93025; --green: #188038;
|
|
8342
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
8343
|
+
}
|
|
8344
|
+
@media (prefers-color-scheme: dark) {
|
|
8345
|
+
:root {
|
|
8346
|
+
--page: #0d0d0d; --card: #141414; --ink: #f5f5f5; --on-ink: #0a0a0a; --soft: #9a9a9a; --faint: #858585;
|
|
8347
|
+
--line: #262626; --field: #1a1a1a; --field-line: #2e2e2e; --ring: rgba(245, 245, 245, 0.1);
|
|
8348
|
+
--off: #262626; --off-ink: #6e6e6e; --red: #ff6b62; --green: #4ade80;
|
|
8349
|
+
}
|
|
8350
|
+
}
|
|
8351
|
+
* { box-sizing: border-box; }
|
|
8352
|
+
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
|
|
8353
|
+
body {
|
|
8354
|
+
margin: 0; background: var(--card); color: var(--ink);
|
|
8355
|
+
font: 16px/1.45 var(--font); -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent;
|
|
8356
|
+
}
|
|
8357
|
+
.shell {
|
|
8358
|
+
min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; align-items: center;
|
|
8359
|
+
padding: max(44px, calc(env(safe-area-inset-top) + 28px)) max(24px, env(safe-area-inset-right))
|
|
8360
|
+
max(28px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left));
|
|
8361
|
+
}
|
|
8362
|
+
.lockup { display: block; width: auto; height: 22px; margin: 0 0 40px; color: var(--ink); }
|
|
8363
|
+
.card { width: 100%; max-width: 380px; text-align: center; }
|
|
8364
|
+
h1 { margin: 0; font-size: 26px; line-height: 1.15; font-weight: 700; letter-spacing: -0.025em; }
|
|
8365
|
+
.where { margin: 10px auto 30px; max-width: 29ch; color: var(--soft); font-size: 15px; text-wrap: balance; }
|
|
8366
|
+
.vh {
|
|
8367
|
+
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
|
|
8368
|
+
clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
8369
|
+
}
|
|
8370
|
+
.otp { position: relative; }
|
|
8371
|
+
.slots {
|
|
8372
|
+
display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)) 12px repeat(3, minmax(0, 1fr));
|
|
8373
|
+
gap: 8px; max-width: 344px; margin: 0 auto;
|
|
8374
|
+
}
|
|
8375
|
+
.slot {
|
|
8376
|
+
position: relative; display: flex; align-items: center; justify-content: center; aspect-ratio: 4 / 5;
|
|
8377
|
+
border: 1px solid var(--field-line); border-radius: 12px; background: var(--field);
|
|
8378
|
+
font-size: 28px; font-weight: 600; line-height: 1; font-variant-numeric: tabular-nums; color: var(--ink);
|
|
8379
|
+
transition: border-color 0.14s ease, box-shadow 0.14s ease, background-color 0.14s ease;
|
|
8380
|
+
}
|
|
8381
|
+
.slot[data-on] { border-color: var(--ink); box-shadow: 0 0 0 3px var(--ring); background: var(--card); }
|
|
8382
|
+
.slot[data-on]:empty::after {
|
|
8383
|
+
content: ""; width: 2px; height: 28px; border-radius: 1px; background: var(--ink);
|
|
8384
|
+
animation: blink 1.06s steps(1, end) infinite;
|
|
8385
|
+
}
|
|
8386
|
+
.sep { align-self: center; justify-self: center; width: 10px; height: 2px; border-radius: 1px; background: var(--field-line); }
|
|
8387
|
+
.otp[data-state="error"] .slot { border-color: var(--red); box-shadow: none; }
|
|
8388
|
+
.otp[data-state="error"] .slots { animation: shake 0.36s cubic-bezier(0.36, 0.07, 0.19, 0.97); }
|
|
8389
|
+
.otp[data-state="ok"] .slot { border-color: var(--green); }
|
|
8390
|
+
.otp[data-state="busy"] .slot { opacity: 0.55; }
|
|
8391
|
+
/* the real field, drawn by the boxes: transparent over them, so any box can be tapped */
|
|
8392
|
+
.js .otp input {
|
|
8393
|
+
position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; margin: 0; padding: 0;
|
|
8394
|
+
border: 0; outline: 0; background: transparent; color: transparent; -webkit-text-fill-color: transparent;
|
|
8395
|
+
caret-color: transparent; font-size: 16px; text-indent: -9999px; cursor: text;
|
|
8396
|
+
-webkit-appearance: none; appearance: none;
|
|
8397
|
+
}
|
|
8398
|
+
.js .otp input::selection { background: transparent; }
|
|
8399
|
+
.js .otp input:-webkit-autofill { transition: background-color 600000s 0s, color 600000s 0s; }
|
|
8400
|
+
/* without script: a plain field that still takes the code and submits */
|
|
8401
|
+
.nojs .slots { display: none; }
|
|
8402
|
+
.nojs .otp input {
|
|
8403
|
+
display: block; width: 100%; max-width: 344px; height: 60px; margin: 0 auto; padding: 0 16px;
|
|
8404
|
+
border: 1px solid var(--field-line); border-radius: 12px; background: var(--field); color: var(--ink);
|
|
8405
|
+
font: 600 28px/1 var(--font); letter-spacing: 0.3em; text-align: center; font-variant-numeric: tabular-nums;
|
|
8406
|
+
}
|
|
8407
|
+
.msg { min-height: 20px; margin: 12px 0 16px; color: var(--red); font-size: 14px; line-height: 20px; }
|
|
8408
|
+
.go {
|
|
8409
|
+
display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%; max-width: 344px;
|
|
8410
|
+
height: 52px; margin: 0 auto; border: 0; border-radius: 999px; background: var(--ink); color: var(--on-ink);
|
|
8411
|
+
font: 600 16px/1 var(--font); letter-spacing: -0.01em; cursor: pointer; touch-action: manipulation;
|
|
8412
|
+
transition: opacity 0.15s ease, transform 0.1s ease;
|
|
8413
|
+
}
|
|
8414
|
+
/* its own colours when it cannot be pressed yet, never a faded ink */
|
|
8415
|
+
.go:disabled { background: var(--off); color: var(--off-ink); cursor: default; }
|
|
8416
|
+
.otp[data-state="busy"] ~ .go:disabled, .otp[data-state="ok"] ~ .go:disabled { background: var(--ink); color: var(--on-ink); }
|
|
8417
|
+
.go:active:not(:disabled) { transform: scale(0.985); }
|
|
8418
|
+
.go:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
|
|
8419
|
+
.spin {
|
|
8420
|
+
width: 16px; height: 16px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%;
|
|
8421
|
+
animation: spin 0.7s linear infinite;
|
|
8422
|
+
}
|
|
8423
|
+
/* one short line, centred with the page, the lock beside it; if large text
|
|
8424
|
+
wraps it anyway, it wraps balanced and still centred */
|
|
8425
|
+
.foot {
|
|
8426
|
+
max-width: 34ch; margin: 36px auto 0; color: var(--faint); font-size: 13px; line-height: 18px;
|
|
8427
|
+
text-align: center; text-wrap: balance;
|
|
8428
|
+
}
|
|
8429
|
+
.foot svg { display: inline-block; width: 13px; height: 13px; margin-right: 6px; vertical-align: -2px; }
|
|
8430
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
8431
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
8432
|
+
@keyframes shake {
|
|
8433
|
+
10%, 90% { transform: translateX(-1px); } 20%, 80% { transform: translateX(3px); }
|
|
8434
|
+
30%, 50%, 70% { transform: translateX(-6px); } 40%, 60% { transform: translateX(6px); }
|
|
8435
|
+
}
|
|
8436
|
+
/* a phone with its keyboard up (Android resizes the page for it): everything moves up */
|
|
8437
|
+
@media (max-height: 560px) {
|
|
8438
|
+
.shell { padding-top: max(20px, calc(env(safe-area-inset-top) + 12px)); }
|
|
8439
|
+
.lockup { height: 18px; margin-bottom: 18px; }
|
|
8440
|
+
h1 { font-size: 22px; }
|
|
8441
|
+
.where { margin: 6px auto 18px; font-size: 14px; }
|
|
8442
|
+
.foot { display: none; }
|
|
8443
|
+
}
|
|
8444
|
+
/* a tablet or a computer: the same content, on a card in the middle of the page */
|
|
8445
|
+
@media (min-width: 560px) and (min-height: 600px) {
|
|
8446
|
+
body { background: var(--page); }
|
|
8447
|
+
.shell { justify-content: center; padding-bottom: max(10vh, 28px); }
|
|
8448
|
+
.lockup { margin-bottom: 32px; }
|
|
8449
|
+
.card {
|
|
8450
|
+
max-width: 420px; padding: 40px 36px 32px; border: 1px solid var(--line); border-radius: 20px;
|
|
8451
|
+
background: var(--card); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 16px 40px -16px rgba(0, 0, 0, 0.14);
|
|
8452
|
+
}
|
|
8453
|
+
.foot { margin-top: 24px; }
|
|
8454
|
+
}
|
|
8455
|
+
@media (prefers-reduced-motion: reduce) {
|
|
8456
|
+
.slot, .go { transition: none; }
|
|
8457
|
+
.slot[data-on]:empty::after, .otp[data-state="error"] .slots { animation: none; }
|
|
8458
|
+
.spin { animation-duration: 1.6s; }
|
|
8459
|
+
}
|
|
8460
|
+
@media (forced-colors: active) {
|
|
8461
|
+
.slot { border-color: CanvasText; }
|
|
8462
|
+
.slot[data-on] { outline: 2px solid Highlight; }
|
|
8463
|
+
}
|
|
8464
|
+
`;
|
|
8465
|
+
var SCRIPT = (copy) => `
|
|
8466
|
+
(function () {
|
|
8467
|
+
var LEN = 6, COPY = ${JSON.stringify({ open: copy.open, opening: copy.opening, wrong: copy.wrong, locked: copy.locked, short: copy.short, offline: copy.offline })};
|
|
8468
|
+
var form = document.getElementById('form'), input = document.getElementById('code');
|
|
8469
|
+
var otp = document.getElementById('otp'), msg = document.getElementById('msg'), go = document.getElementById('go');
|
|
8470
|
+
var slots = Array.prototype.slice.call(document.querySelectorAll('.slot'));
|
|
8471
|
+
var busy = false;
|
|
8472
|
+
input.maxLength = LEN;
|
|
8473
|
+
function digits(s) { return String(s || '').replace(/\\D/g, '').slice(0, LEN); }
|
|
8474
|
+
function fromPaste(s) { var m = /[?&]t=([^&#\\s]+)/.exec(s || ''); return digits(m ? m[1] : s); }
|
|
8475
|
+
function toEnd() { var n = input.value.length; try { input.setSelectionRange(n, n); } catch (e) {} }
|
|
8476
|
+
function paint() {
|
|
8477
|
+
var v = input.value, on = document.activeElement === input && !busy;
|
|
8478
|
+
for (var i = 0; i < LEN; i++) {
|
|
8479
|
+
slots[i].textContent = v.charAt(i);
|
|
8480
|
+
if (on && i === Math.min(v.length, LEN - 1)) slots[i].setAttribute('data-on', '');
|
|
8481
|
+
else slots[i].removeAttribute('data-on');
|
|
8482
|
+
}
|
|
8483
|
+
go.disabled = busy || v.length !== LEN;
|
|
8484
|
+
}
|
|
8485
|
+
function state(s, text) {
|
|
8486
|
+
otp.removeAttribute('data-state');
|
|
8487
|
+
void otp.offsetWidth;
|
|
8488
|
+
if (s) otp.setAttribute('data-state', s);
|
|
8489
|
+
msg.textContent = text || '';
|
|
8490
|
+
input.setAttribute('aria-invalid', s === 'error' ? 'true' : 'false');
|
|
8491
|
+
}
|
|
8492
|
+
function label(busyNow) {
|
|
8493
|
+
go.textContent = busyNow ? COPY.opening : COPY.open;
|
|
8494
|
+
if (!busyNow) return;
|
|
8495
|
+
var s = document.createElement('span');
|
|
8496
|
+
s.className = 'spin'; s.setAttribute('aria-hidden', 'true');
|
|
8497
|
+
go.insertBefore(s, go.firstChild);
|
|
8498
|
+
}
|
|
8499
|
+
function fail(kind, clear) {
|
|
8500
|
+
busy = false; label(false); state('error', COPY[kind]);
|
|
8501
|
+
if (navigator.vibrate) navigator.vibrate(40);
|
|
8502
|
+
if (clear) setTimeout(function () { input.value = ''; paint(); input.focus(); }, 420);
|
|
8503
|
+
paint();
|
|
8504
|
+
}
|
|
8505
|
+
function submit() {
|
|
8506
|
+
var code = digits(input.value);
|
|
8507
|
+
if (busy) return;
|
|
8508
|
+
if (code.length !== LEN) { state('error', COPY.short); input.focus(); return; }
|
|
8509
|
+
busy = true; state('busy', ''); label(true); paint();
|
|
8510
|
+
fetch('/api/phone?t=' + code, { headers: { accept: 'application/json' }, credentials: 'same-origin', cache: 'no-store' })
|
|
8511
|
+
.then(function (res) {
|
|
8512
|
+
if (res.ok) {
|
|
8513
|
+
state('ok', '');
|
|
8514
|
+
var u = new URL(location.href); u.searchParams.delete('t');
|
|
8515
|
+
location.replace(u.pathname + u.search + u.hash);
|
|
8516
|
+
return;
|
|
8517
|
+
}
|
|
8518
|
+
return res.json().catch(function () { return {}; }).then(function (b) {
|
|
8519
|
+
fail(b && b.error === 'too many tries' ? 'locked' : 'wrong', true);
|
|
8520
|
+
});
|
|
8521
|
+
})
|
|
8522
|
+
.catch(function () { fail('offline', false); });
|
|
8523
|
+
}
|
|
8524
|
+
input.addEventListener('input', function () {
|
|
8525
|
+
var clean = digits(input.value);
|
|
8526
|
+
if (clean !== input.value) input.value = clean;
|
|
8527
|
+
if (otp.getAttribute('data-state') === 'error') state('', '');
|
|
8528
|
+
toEnd(); paint();
|
|
8529
|
+
if (clean.length === LEN) submit();
|
|
8530
|
+
});
|
|
8531
|
+
input.addEventListener('paste', function (e) {
|
|
8532
|
+
var text = e.clipboardData ? e.clipboardData.getData('text') : '';
|
|
8533
|
+
e.preventDefault();
|
|
8534
|
+
input.value = fromPaste(text);
|
|
8535
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
8536
|
+
});
|
|
8537
|
+
['focus', 'blur', 'click', 'select', 'keyup'].forEach(function (t) {
|
|
8538
|
+
input.addEventListener(t, function () { if (t !== 'blur') toEnd(); paint(); });
|
|
8539
|
+
});
|
|
8540
|
+
// a form will not submit itself while its button is disabled, so Enter is heard here
|
|
8541
|
+
input.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); submit(); } });
|
|
8542
|
+
form.addEventListener('submit', function (e) { e.preventDefault(); submit(); });
|
|
8543
|
+
window.addEventListener('pageshow', function (e) {
|
|
8544
|
+
if (!e.persisted) return;
|
|
8545
|
+
busy = false; input.value = ''; label(false); state('', ''); paint();
|
|
8546
|
+
});
|
|
8547
|
+
if (msg.textContent) state('error', msg.textContent);
|
|
8548
|
+
paint();
|
|
8549
|
+
input.focus();
|
|
8550
|
+
})();
|
|
8551
|
+
`;
|
|
8552
|
+
var LOCK = `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="7" width="10" height="7" rx="1.75"/><path d="M5.5 7V5.25a2.5 2.5 0 0 1 5 0V7"/></svg>`;
|
|
8553
|
+
function codePage(problem) {
|
|
8554
|
+
const c = CODE_PAGE_COPY;
|
|
8555
|
+
const note = problem ? c[problem] : "";
|
|
8556
|
+
const slot = '<span class="slot"></span>';
|
|
8557
|
+
return `<!doctype html>
|
|
8558
|
+
<html lang="en" class="nojs">
|
|
8559
|
+
<head>
|
|
8560
|
+
<meta charset="utf-8">
|
|
8561
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
|
|
8562
|
+
<meta name="robots" content="noindex">
|
|
8563
|
+
<meta name="color-scheme" content="light dark">
|
|
8564
|
+
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
|
8565
|
+
<meta name="theme-color" content="#0d0d0d" media="(prefers-color-scheme: dark)">
|
|
8566
|
+
<meta name="format-detection" content="telephone=no">
|
|
8567
|
+
<title>Scenri</title>
|
|
8568
|
+
<link rel="icon" href="${symbolIcon()}">
|
|
8569
|
+
<script>document.documentElement.className = 'js';</script>
|
|
8570
|
+
<style>${STYLE}</style>
|
|
8571
|
+
</head>
|
|
8572
|
+
<body>
|
|
8573
|
+
<main class="shell">
|
|
8574
|
+
<svg class="lockup" viewBox="0 0 263.79 64" fill="currentColor" role="img" aria-label="Scenri">${LOCKUP_PATHS.map((d) => `<path d="${d}"/>`).join("")}</svg>
|
|
8575
|
+
<section class="card" aria-labelledby="title">
|
|
8576
|
+
<h1 id="title">${esc(c.title)}</h1>
|
|
8577
|
+
<p class="where" id="where">${esc(c.where)}</p>
|
|
8578
|
+
<form id="form" method="get" action="" novalidate>
|
|
8579
|
+
<div class="otp" id="otp">
|
|
8580
|
+
<label class="vh" for="code">${esc(c.label)}</label>
|
|
8581
|
+
<input id="code" name="t" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="7" autocomplete="one-time-code" enterkeyhint="go" autocapitalize="off" autocorrect="off" spellcheck="false" autofocus required aria-describedby="where msg" data-1p-ignore data-lpignore="true" data-bwignore data-form-type="other">
|
|
8582
|
+
<div class="slots" aria-hidden="true">${slot.repeat(3)}<span class="sep"></span>${slot.repeat(3)}</div>
|
|
8583
|
+
</div>
|
|
8584
|
+
<p class="msg" id="msg" role="alert">${esc(note)}</p>
|
|
8585
|
+
<button type="submit" class="go" id="go">${esc(c.open)}</button>
|
|
8586
|
+
</form>
|
|
8587
|
+
</section>
|
|
8588
|
+
<p class="foot">${LOCK}<span>${esc(c.foot)}</span></p>
|
|
8589
|
+
</main>
|
|
8590
|
+
<script>${SCRIPT(c)}</script>
|
|
8591
|
+
</body>
|
|
8592
|
+
</html>
|
|
8593
|
+
`;
|
|
8594
|
+
}
|
|
8595
|
+
|
|
8596
|
+
// src/network/hosts.ts
|
|
8597
|
+
var LOOPBACK_NAMES = ["localhost", "127.0.0.1", "::1"];
|
|
8598
|
+
var isLoopbackName = (host) => LOOPBACK_NAMES.includes(host);
|
|
8599
|
+
var isWildcardHost = (host) => host === "0.0.0.0" || host === "::";
|
|
8600
|
+
var isLoopbackAddress = (addr) => !!addr && (addr === "::1" || addr.startsWith("127.") || addr.startsWith("::ffff:127."));
|
|
8601
|
+
function isIPv4Literal(host) {
|
|
8602
|
+
const parts = host.split(".");
|
|
8603
|
+
return !isWildcardHost(host) && parts.length === 4 && parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255);
|
|
8604
|
+
}
|
|
8605
|
+
var VIRTUAL = /^(utun|tun|tap|ppp|ipsec|wg|zt|gif|stf|awdl|llw|anpi|bridge|docker|br-|veth|virbr|vmnet|vboxnet|lxc|lxd|cni|flannel|cali|tailscale)|virtual|vmware|hyper-v|vethernet|wsl|tailscale|zerotier|wireguard|openvpn|nordlynx|tap-windows|bluetooth|loopback/i;
|
|
8606
|
+
var PHYSICAL = /^(en\d|eth\d|wl|wlan|enp|eno|ens|wi-?fi|wlan|ethernet|wireless)/i;
|
|
8607
|
+
function octets(address) {
|
|
8608
|
+
const parts = address.split(".");
|
|
8609
|
+
if (parts.length !== 4) return null;
|
|
8610
|
+
const nums = parts.map((p) => Number(p));
|
|
8611
|
+
return nums.every((n, i) => /^\d{1,3}$/.test(parts[i]) && n >= 0 && n <= 255) ? nums : null;
|
|
8612
|
+
}
|
|
8613
|
+
function rangeRank(address) {
|
|
8614
|
+
const o = octets(address);
|
|
8615
|
+
if (!o) return null;
|
|
8616
|
+
if (o[0] === 192 && o[1] === 168) return 0;
|
|
8617
|
+
if (o[0] === 10) return 1;
|
|
8618
|
+
if (o[0] === 172 && o[1] >= 16 && o[1] <= 31) return 2;
|
|
8619
|
+
if (o[0] === 100 && o[1] >= 64 && o[1] <= 127) return 3;
|
|
8620
|
+
return null;
|
|
8621
|
+
}
|
|
8622
|
+
var isIPv4 = (a) => a.family === "IPv4" || a.family === 4;
|
|
8623
|
+
function lanCandidates(ifaces, routeAddress = null) {
|
|
8624
|
+
const found = [];
|
|
8625
|
+
for (const [name, addrs] of Object.entries(ifaces)) {
|
|
8626
|
+
if (VIRTUAL.test(name)) continue;
|
|
8627
|
+
for (const a of addrs ?? []) {
|
|
8628
|
+
if (!isIPv4(a) || a.internal) continue;
|
|
8629
|
+
if (a.mac === "00:00:00:00:00:00" && !PHYSICAL.test(name)) continue;
|
|
8630
|
+
const range = rangeRank(a.address);
|
|
8631
|
+
if (range === null) continue;
|
|
8632
|
+
found.push({ address: a.address, name, physical: PHYSICAL.test(name), range });
|
|
8633
|
+
}
|
|
8634
|
+
}
|
|
8635
|
+
found.sort(
|
|
8636
|
+
(a, b) => Number(b.address === routeAddress) - Number(a.address === routeAddress) || Number(b.physical) - Number(a.physical) || a.range - b.range || a.name.localeCompare(b.name) || a.address.localeCompare(b.address, void 0, { numeric: true })
|
|
8637
|
+
);
|
|
8638
|
+
return [...new Set(found.map((f) => f.address))];
|
|
8639
|
+
}
|
|
8640
|
+
function defaultRouteAddress() {
|
|
8641
|
+
return new Promise((resolve) => {
|
|
8642
|
+
let socket = null;
|
|
8643
|
+
const done = (value) => {
|
|
8644
|
+
try {
|
|
8645
|
+
socket?.close();
|
|
8646
|
+
} catch {
|
|
8647
|
+
}
|
|
8648
|
+
socket = null;
|
|
8649
|
+
resolve(value);
|
|
8650
|
+
};
|
|
8651
|
+
try {
|
|
8652
|
+
socket = createSocket("udp4");
|
|
8653
|
+
socket.once("error", () => done(null));
|
|
8654
|
+
socket.connect(9, "192.0.2.1", () => {
|
|
8655
|
+
try {
|
|
8656
|
+
done(socket?.address().address ?? null);
|
|
8657
|
+
} catch {
|
|
8658
|
+
done(null);
|
|
8659
|
+
}
|
|
8660
|
+
});
|
|
8661
|
+
setTimeout(() => done(null), 500).unref();
|
|
8662
|
+
} catch {
|
|
8663
|
+
done(null);
|
|
8664
|
+
}
|
|
8665
|
+
});
|
|
8666
|
+
}
|
|
8667
|
+
async function phoneAddresses() {
|
|
8668
|
+
let ifaces = {};
|
|
8669
|
+
try {
|
|
8670
|
+
ifaces = networkInterfaces();
|
|
8671
|
+
} catch {
|
|
8672
|
+
return [];
|
|
8673
|
+
}
|
|
8674
|
+
return lanCandidates(ifaces, await defaultRouteAddress());
|
|
8675
|
+
}
|
|
8676
|
+
function ownAddresses() {
|
|
8677
|
+
const out = /* @__PURE__ */ new Set();
|
|
8678
|
+
try {
|
|
8679
|
+
for (const addrs of Object.values(networkInterfaces())) for (const a of addrs ?? []) out.add(a.address);
|
|
8680
|
+
} catch {
|
|
8681
|
+
}
|
|
8682
|
+
return out;
|
|
8683
|
+
}
|
|
8684
|
+
var run = (cmd, args, opts) => new Promise((resolve, reject) => {
|
|
8685
|
+
execFile(
|
|
8686
|
+
cmd,
|
|
8687
|
+
args,
|
|
8688
|
+
{ windowsHide: true, encoding: "utf8", timeout: opts?.timeout ?? 8e3 },
|
|
8689
|
+
(err, stdout, stderr) => err ? reject(Object.assign(err, { stderr })) : resolve(stdout)
|
|
8690
|
+
);
|
|
8691
|
+
});
|
|
8692
|
+
var SOCKETFILTERFW = "/usr/libexec/ApplicationFirewall/socketfilterfw";
|
|
8693
|
+
var PROMPT_MS = 18e4;
|
|
8694
|
+
async function macVerdict(nodePath, exec = run) {
|
|
8695
|
+
const state = await exec(SOCKETFILTERFW, ["--getglobalstate"]);
|
|
8696
|
+
if (/disabled|State = 0/i.test(state)) return "ok";
|
|
8697
|
+
if (!/enabled|State = [12]/i.test(state)) return "unknown";
|
|
8698
|
+
const all = await exec(SOCKETFILTERFW, ["--getblockall"]);
|
|
8699
|
+
if (/block all state set to enabled|State = 2/i.test(all)) return "blocks-all";
|
|
8700
|
+
const app = await exec(SOCKETFILTERFW, ["--getappblocked", nodePath]);
|
|
8701
|
+
if (/is blocked/i.test(app)) return "blocked";
|
|
8702
|
+
if (/is permitted/i.test(app)) return "ok";
|
|
8703
|
+
return "unknown";
|
|
8704
|
+
}
|
|
8705
|
+
var ruleName = (port) => `Scenri-${port}`;
|
|
8706
|
+
var psQuote = (s) => `'${s.replace(/'/g, "''")}'`;
|
|
8707
|
+
function windowsScript(nodePath, port) {
|
|
8708
|
+
return [
|
|
8709
|
+
PS_UTF8,
|
|
8710
|
+
"$ErrorActionPreference='SilentlyContinue';",
|
|
8711
|
+
"$c=(Get-NetConnectionProfile | Where-Object { $_.IPv4Connectivity -ne 'Disconnected' } | Select-Object -First 1).NetworkCategory;",
|
|
8712
|
+
`$n=if ($c -eq 'DomainAuthenticated') { 'Domain' } else { "$c" };`,
|
|
8713
|
+
"$on=(Get-NetFirewallProfile -Name $n).Enabled;",
|
|
8714
|
+
`$r=Get-NetFirewallApplicationFilter -Program ${psQuote(nodePath)} | Get-NetFirewallRule | Where-Object { $_.Enabled -eq 'True' -and $_.Direction -eq 'Inbound' };`,
|
|
8715
|
+
"$b=@($r | Where-Object { $_.Action -eq 'Block' } | ForEach-Object { $_.Profile.ToString() }) -join ';';",
|
|
8716
|
+
"$a=@($r | Where-Object { $_.Action -eq 'Allow' } | ForEach-Object { $_.Profile.ToString() }) -join ';';",
|
|
8717
|
+
`$s=[bool](Get-NetFirewallRule -Name ${psQuote(ruleName(port))} | Where-Object { $_.Enabled -eq 'True' });`,
|
|
8718
|
+
'Write-Output "$n|$on|$b|$a|$s"'
|
|
8719
|
+
].join(" ");
|
|
8720
|
+
}
|
|
8721
|
+
var covers = (profiles, profile) => profiles.split(";").filter(Boolean).some((p) => /\bAny\b/i.test(p) || p.split(",").some((x) => x.trim().toLowerCase() === profile.toLowerCase()));
|
|
8722
|
+
function parseWindows(out) {
|
|
8723
|
+
const line = out.trim().split(/\r?\n/).pop() ?? "";
|
|
8724
|
+
const [profile, on, block = "", allow = "", ours = ""] = line.split("|");
|
|
8725
|
+
if (!profile || !["Public", "Private", "Domain"].includes(profile)) return "unknown";
|
|
8726
|
+
if (/^false$/i.test(on ?? "")) return "ok";
|
|
8727
|
+
if (covers(block, profile)) return "blocked";
|
|
8728
|
+
if (/^true$/i.test(ours) || covers(allow, profile)) return "ok";
|
|
8729
|
+
return "blocked";
|
|
8730
|
+
}
|
|
8731
|
+
function windowsAllowScript(nodePath, port) {
|
|
8732
|
+
return [
|
|
8733
|
+
"$ErrorActionPreference='Stop';",
|
|
8734
|
+
"try {",
|
|
8735
|
+
`Get-NetFirewallApplicationFilter -Program ${psQuote(nodePath)} -ErrorAction SilentlyContinue | Get-NetFirewallRule | Where-Object { $_.Direction -eq 'Inbound' -and $_.Action -eq 'Block' } | Remove-NetFirewallRule;`,
|
|
8736
|
+
`Remove-NetFirewallRule -Name ${psQuote(ruleName(port))} -ErrorAction SilentlyContinue;`,
|
|
8737
|
+
`New-NetFirewallRule -Name ${psQuote(ruleName(port))} -DisplayName 'Scenri (phones on this network)' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${port} -RemoteAddress LocalSubnet -Profile Any | Out-Null;`,
|
|
8738
|
+
"exit 0",
|
|
8739
|
+
"} catch { exit 1 }"
|
|
8740
|
+
].join(" ");
|
|
8741
|
+
}
|
|
8742
|
+
var encoded = (script) => Buffer.from(script, "utf16le").toString("base64");
|
|
8743
|
+
function windowsElevate(nodePath, port, env) {
|
|
8744
|
+
const ps = powershellPath(env);
|
|
8745
|
+
const inner = `'-NoProfile','-NonInteractive','-WindowStyle','Hidden','-EncodedCommand','${encoded(windowsAllowScript(nodePath, port))}'`;
|
|
8746
|
+
return [
|
|
8747
|
+
"-NoProfile",
|
|
8748
|
+
"-NonInteractive",
|
|
8749
|
+
"-Command",
|
|
8750
|
+
`try { $p = Start-Process -FilePath ${psQuote(ps)} -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList ${inner}; exit $p.ExitCode } catch { exit 1223 }`
|
|
8751
|
+
];
|
|
8752
|
+
}
|
|
8753
|
+
var macAllowArgs = (nodePath) => [
|
|
8754
|
+
"-e",
|
|
8755
|
+
"on run argv",
|
|
8756
|
+
"-e",
|
|
8757
|
+
"set p to quoted form of item 1 of argv",
|
|
8758
|
+
"-e",
|
|
8759
|
+
`do shell script "${SOCKETFILTERFW} --add " & p & " && ${SOCKETFILTERFW} --unblockapp " & p with administrator privileges`,
|
|
8760
|
+
"-e",
|
|
8761
|
+
"end run",
|
|
8762
|
+
nodePath
|
|
8763
|
+
];
|
|
8764
|
+
async function allowScenri(opts) {
|
|
8765
|
+
const platform = opts.platform ?? process.platform;
|
|
8766
|
+
const nodePath = opts.nodePath ?? process.execPath;
|
|
8767
|
+
const exec = opts.exec ?? run;
|
|
8768
|
+
const env = opts.env ?? process.env;
|
|
8769
|
+
try {
|
|
8770
|
+
if (platform === "darwin") {
|
|
8771
|
+
await exec("/usr/bin/osascript", macAllowArgs(nodePath), { timeout: PROMPT_MS });
|
|
8772
|
+
return "done";
|
|
8773
|
+
}
|
|
8774
|
+
if (platform === "win32") {
|
|
8775
|
+
await exec(powershellPath(env), windowsElevate(nodePath, opts.port, env), { timeout: PROMPT_MS });
|
|
8776
|
+
return "done";
|
|
8777
|
+
}
|
|
8778
|
+
return "unsupported";
|
|
8779
|
+
} catch (err) {
|
|
8780
|
+
const e = err;
|
|
8781
|
+
const said = `${e.message ?? ""} ${e.stderr ?? ""}`;
|
|
8782
|
+
if (e.code === 1223 || /\(-128\)|User cancel/i.test(said)) return "cancelled";
|
|
8783
|
+
return "failed";
|
|
8784
|
+
}
|
|
8785
|
+
}
|
|
8786
|
+
async function firewallVerdict(opts) {
|
|
8787
|
+
const platform = opts.platform ?? process.platform;
|
|
8788
|
+
const nodePath = opts.nodePath ?? process.execPath;
|
|
8789
|
+
const exec = opts.exec ?? run;
|
|
8790
|
+
try {
|
|
8791
|
+
if (platform === "darwin") return await macVerdict(nodePath, exec);
|
|
8792
|
+
if (platform === "win32") {
|
|
8793
|
+
const out = await exec(powershellPath(opts.env ?? process.env), [
|
|
8794
|
+
"-NoProfile",
|
|
8795
|
+
"-NonInteractive",
|
|
8796
|
+
"-Command",
|
|
8797
|
+
windowsScript(nodePath, opts.port)
|
|
8798
|
+
]);
|
|
8799
|
+
return parseWindows(out);
|
|
8800
|
+
}
|
|
8801
|
+
} catch {
|
|
8802
|
+
}
|
|
8803
|
+
return "unknown";
|
|
8804
|
+
}
|
|
8805
|
+
|
|
8806
|
+
// src/network/phoneAccess.ts
|
|
8807
|
+
var CODE_ALPHABET = "0123456789";
|
|
8808
|
+
var CODE_LENGTH = 6;
|
|
8809
|
+
var CODE_SETTING = "access.code";
|
|
8810
|
+
var SYNC_MS = 3e4;
|
|
8811
|
+
function newCode() {
|
|
8812
|
+
let out = "";
|
|
8813
|
+
for (let i = 0; i < CODE_LENGTH; i++) out += CODE_ALPHABET[randomInt(CODE_ALPHABET.length)];
|
|
8814
|
+
return out;
|
|
8815
|
+
}
|
|
8816
|
+
var normalizeCode = (s) => s.replace(/[\s-]/g, "").toUpperCase();
|
|
8817
|
+
var groupCode = (code) => `${code.slice(0, 3)} ${code.slice(3)}`;
|
|
8818
|
+
function deviceOf(ua) {
|
|
8819
|
+
const s = ua ?? "";
|
|
8820
|
+
if (/iPhone/i.test(s)) return "iPhone";
|
|
8821
|
+
if (/iPad/i.test(s)) return "iPad";
|
|
8822
|
+
if (/Android/i.test(s)) return /Mobile/i.test(s) ? "Android phone" : "Android tablet";
|
|
8823
|
+
if (/Macintosh|Mac OS X/i.test(s)) return "Mac or iPad";
|
|
8824
|
+
if (/Windows/i.test(s)) return "Windows computer";
|
|
8825
|
+
if (/CrOS/i.test(s)) return "Chromebook";
|
|
8826
|
+
if (/Linux/i.test(s)) return "computer";
|
|
8827
|
+
return "device";
|
|
8828
|
+
}
|
|
8829
|
+
var FRESH_MS = 5e3;
|
|
8830
|
+
function listenWith(handler) {
|
|
8831
|
+
return (address, port) => new Promise((resolve, reject) => {
|
|
8832
|
+
const server = createServer(handler());
|
|
8833
|
+
server.keepAliveTimeout = 72e3;
|
|
8834
|
+
server.once("error", reject);
|
|
8835
|
+
server.listen({ port, host: address, exclusive: true }, () => {
|
|
8836
|
+
server.off("error", reject);
|
|
8837
|
+
server.on("error", () => void 0);
|
|
8838
|
+
resolve({
|
|
8839
|
+
close: () => new Promise((done) => {
|
|
8840
|
+
server.close(() => done());
|
|
8841
|
+
server.closeAllConnections();
|
|
8842
|
+
})
|
|
8843
|
+
});
|
|
8844
|
+
});
|
|
8845
|
+
});
|
|
8846
|
+
}
|
|
8847
|
+
function createPhoneAccess(deps) {
|
|
8848
|
+
const { store } = deps;
|
|
8849
|
+
const bind = (deps.bind ?? "").trim();
|
|
8850
|
+
const mode = !bind ? "listeners" : isLoopbackName(bind) ? "none" : isWildcardHost(bind) ? "wildcard" : "fixed";
|
|
8851
|
+
const addresses = deps.addresses ?? phoneAddresses;
|
|
8852
|
+
const own = deps.own ?? ownAddresses;
|
|
8853
|
+
const now2 = deps.now ?? Date.now;
|
|
8854
|
+
const listen = deps.listen ?? listenWith(deps.handler ?? (() => () => void 0));
|
|
8855
|
+
const firewall = deps.firewall ?? ((p) => firewallVerdict({ port: p }));
|
|
8856
|
+
const allowThrough = deps.allow ?? ((p) => allowScenri({ port: p }));
|
|
8857
|
+
let code = store.getSetting(CODE_SETTING);
|
|
8858
|
+
if (!code || !new RegExp(`^\\d{${CODE_LENGTH}}$`).test(normalizeCode(code))) {
|
|
8859
|
+
code = newCode();
|
|
8860
|
+
store.setSetting(CODE_SETTING, code);
|
|
8861
|
+
}
|
|
8862
|
+
let theCode = normalizeCode(code);
|
|
8863
|
+
let port = null;
|
|
8864
|
+
const open = /* @__PURE__ */ new Map();
|
|
8865
|
+
let candidates = [];
|
|
8866
|
+
let syncedAt = -Infinity;
|
|
8867
|
+
let chain = Promise.resolve();
|
|
8868
|
+
let timer = null;
|
|
8869
|
+
let closed = false;
|
|
8870
|
+
let lastVisit = null;
|
|
8871
|
+
let verdict = null;
|
|
8872
|
+
const syncOnce = async () => {
|
|
8873
|
+
if (closed) return;
|
|
8874
|
+
candidates = mode === "none" ? [] : mode === "fixed" ? [bind] : await addresses().catch(() => []);
|
|
8875
|
+
syncedAt = now2();
|
|
8876
|
+
if (mode !== "listeners" || port === null) return;
|
|
8877
|
+
const want = new Set(candidates);
|
|
8878
|
+
for (const [address, closer] of open) {
|
|
8879
|
+
if (want.has(address)) continue;
|
|
8880
|
+
open.delete(address);
|
|
8881
|
+
await closer.close().catch(() => void 0);
|
|
8882
|
+
}
|
|
8883
|
+
for (const address of candidates) {
|
|
8884
|
+
if (open.has(address) || closed) continue;
|
|
8885
|
+
try {
|
|
8886
|
+
open.set(address, await listen(address, port));
|
|
8887
|
+
} catch {
|
|
8888
|
+
}
|
|
8889
|
+
}
|
|
8890
|
+
};
|
|
8891
|
+
const sync = () => {
|
|
8892
|
+
chain = chain.then(syncOnce, syncOnce);
|
|
8893
|
+
return chain;
|
|
8894
|
+
};
|
|
8895
|
+
const readFirewall = async () => {
|
|
8896
|
+
if (port === null) return "unknown";
|
|
8897
|
+
if (verdict && now2() - verdict.at < 2e4) return verdict.value;
|
|
8898
|
+
const value = await firewall(port).catch(() => "unknown");
|
|
8899
|
+
verdict = { at: now2(), value };
|
|
8900
|
+
return value;
|
|
8901
|
+
};
|
|
8902
|
+
const reachable = () => mode === "listeners" ? candidates.filter((a) => open.has(a)) : candidates;
|
|
8903
|
+
return {
|
|
8904
|
+
get code() {
|
|
8905
|
+
return theCode;
|
|
8906
|
+
},
|
|
8907
|
+
renew() {
|
|
8908
|
+
theCode = newCode();
|
|
8909
|
+
store.setSetting(CODE_SETTING, theCode);
|
|
8910
|
+
return theCode;
|
|
8911
|
+
},
|
|
8912
|
+
async start(p) {
|
|
8913
|
+
port = p;
|
|
8914
|
+
await sync();
|
|
8915
|
+
if (mode === "listeners" && !closed) {
|
|
8916
|
+
timer = setInterval(() => void sync(), deps.syncMs ?? SYNC_MS);
|
|
8917
|
+
timer.unref();
|
|
8918
|
+
}
|
|
8919
|
+
},
|
|
8920
|
+
sync,
|
|
8921
|
+
async close() {
|
|
8922
|
+
closed = true;
|
|
8923
|
+
if (timer) clearInterval(timer);
|
|
8924
|
+
await chain.catch(() => void 0);
|
|
8925
|
+
const all = [...open.values()];
|
|
8926
|
+
open.clear();
|
|
8927
|
+
await Promise.all(all.map((c) => c.close().catch(() => void 0)));
|
|
8928
|
+
},
|
|
8929
|
+
async status(thisComputer, fresh = false) {
|
|
8930
|
+
if (fresh || now2() - syncedAt >= FRESH_MS) await sync();
|
|
8931
|
+
const origins = port === null ? [] : reachable().map((a) => `http://${a.includes(":") ? `[${a}]` : a}:${port}`);
|
|
8932
|
+
const address = origins[0] ?? null;
|
|
8933
|
+
return {
|
|
8934
|
+
reach: mode === "none" ? "this-computer" : "network",
|
|
8935
|
+
thisComputer,
|
|
8936
|
+
platform: deps.platform ?? process.platform,
|
|
8937
|
+
address,
|
|
8938
|
+
url: address ? `${address}/?t=${theCode}` : null,
|
|
8939
|
+
code: theCode,
|
|
8940
|
+
others: origins.slice(1),
|
|
8941
|
+
problem: mode === "none" || port === null ? null : candidates.length === 0 ? "no-network" : origins.length === 0 ? "blocked" : null,
|
|
8942
|
+
lastVisit
|
|
8943
|
+
};
|
|
8944
|
+
},
|
|
8945
|
+
visit(remote, ua) {
|
|
8946
|
+
const at = now2();
|
|
8947
|
+
if (lastVisit && at - lastVisit.at < 1e3) return;
|
|
8948
|
+
if (remote && own().has(remote.replace(/^::ffff:/, ""))) return;
|
|
8949
|
+
lastVisit = { at, device: deviceOf(ua) };
|
|
8950
|
+
},
|
|
8951
|
+
firewall: readFirewall,
|
|
8952
|
+
async allow() {
|
|
8953
|
+
if (port === null) return { result: "unsupported", firewall: "unknown" };
|
|
8954
|
+
const result = await allowThrough(port).catch(() => "failed");
|
|
8955
|
+
verdict = null;
|
|
8956
|
+
if (result === "done" && mode === "listeners") {
|
|
8957
|
+
chain = chain.then(async () => {
|
|
8958
|
+
const all = [...open.values()];
|
|
8959
|
+
open.clear();
|
|
8960
|
+
await Promise.all(all.map((c) => c.close().catch(() => void 0)));
|
|
8961
|
+
});
|
|
8962
|
+
await sync();
|
|
8963
|
+
}
|
|
8964
|
+
return { result, firewall: await readFirewall() };
|
|
8965
|
+
}
|
|
8966
|
+
};
|
|
8967
|
+
}
|
|
8968
|
+
|
|
8969
|
+
// src/access.ts
|
|
8300
8970
|
var ACCESS_COOKIE = "sc_access";
|
|
8971
|
+
var cookieName = (port) => port ? `${ACCESS_COOKIE}_${port}` : ACCESS_COOKIE;
|
|
8301
8972
|
var LEGACY_ACCESS_COOKIE = "bt_access";
|
|
8973
|
+
var COOKIE_MAX_AGE_S = 400 * 24 * 60 * 60;
|
|
8974
|
+
var WRONG_LIMIT = 10;
|
|
8975
|
+
var WRONG_LIMIT_ALL = 100;
|
|
8976
|
+
var WRONG_WINDOW_MS = 10 * 6e4;
|
|
8977
|
+
var ADDRESSES_KEPT = 5e3;
|
|
8302
8978
|
function hostnameOf(hostHeader) {
|
|
8303
8979
|
if (!hostHeader) return null;
|
|
8304
8980
|
const h = hostHeader.trim().toLowerCase();
|
|
@@ -8311,44 +8987,105 @@ function hostnameOf(hostHeader) {
|
|
|
8311
8987
|
const colon = h.lastIndexOf(":");
|
|
8312
8988
|
return colon === -1 ? h : h.slice(0, colon);
|
|
8313
8989
|
}
|
|
8314
|
-
function
|
|
8315
|
-
|
|
8990
|
+
function fromThisComputer(req) {
|
|
8991
|
+
const host = hostnameOf(req.headers.host);
|
|
8992
|
+
return !!host && isLoopbackName(host) && isLoopbackAddress(req.socket?.remoteAddress);
|
|
8993
|
+
}
|
|
8994
|
+
function codeMatches(expected, supplied) {
|
|
8316
8995
|
const a = Buffer.from(expected);
|
|
8317
|
-
const b = Buffer.from(supplied);
|
|
8318
|
-
|
|
8319
|
-
return timingSafeEqual(a, b);
|
|
8996
|
+
const b = Buffer.from(normalizeCode(supplied));
|
|
8997
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
8320
8998
|
}
|
|
8321
8999
|
function cookieValue(header, name) {
|
|
8322
9000
|
for (const part of (header ?? "").split(";")) {
|
|
8323
9001
|
const eq = part.indexOf("=");
|
|
8324
9002
|
if (eq === -1) continue;
|
|
8325
|
-
if (part.slice(0, eq).trim()
|
|
9003
|
+
if (part.slice(0, eq).trim() !== name) continue;
|
|
9004
|
+
try {
|
|
9005
|
+
return decodeURIComponent(part.slice(eq + 1).trim());
|
|
9006
|
+
} catch {
|
|
9007
|
+
return void 0;
|
|
9008
|
+
}
|
|
8326
9009
|
}
|
|
8327
9010
|
return void 0;
|
|
8328
9011
|
}
|
|
9012
|
+
function createLimiter(now2) {
|
|
9013
|
+
const seen = /* @__PURE__ */ new Map();
|
|
9014
|
+
let everyone = { since: now2(), wrong: /* @__PURE__ */ new Set() };
|
|
9015
|
+
const live = (t) => now2() - t.since <= WRONG_WINDOW_MS;
|
|
9016
|
+
const all = () => {
|
|
9017
|
+
if (!live(everyone)) everyone = { since: now2(), wrong: /* @__PURE__ */ new Set() };
|
|
9018
|
+
return everyone;
|
|
9019
|
+
};
|
|
9020
|
+
const entry = (ip) => {
|
|
9021
|
+
const e = seen.get(ip);
|
|
9022
|
+
if (e && live(e)) return e;
|
|
9023
|
+
seen.delete(ip);
|
|
9024
|
+
return null;
|
|
9025
|
+
};
|
|
9026
|
+
return {
|
|
9027
|
+
locked: (ip) => all().wrong.size >= WRONG_LIMIT_ALL || (entry(ip)?.wrong.size ?? 0) >= WRONG_LIMIT,
|
|
9028
|
+
wrong(ip, value) {
|
|
9029
|
+
const v = normalizeCode(value);
|
|
9030
|
+
all().wrong.add(v);
|
|
9031
|
+
let e = entry(ip);
|
|
9032
|
+
if (!e) {
|
|
9033
|
+
e = { since: now2(), wrong: /* @__PURE__ */ new Set() };
|
|
9034
|
+
seen.set(ip, e);
|
|
9035
|
+
}
|
|
9036
|
+
e.wrong.add(v);
|
|
9037
|
+
if (seen.size > ADDRESSES_KEPT) {
|
|
9038
|
+
for (const [k, t] of seen) if (!live(t)) seen.delete(k);
|
|
9039
|
+
while (seen.size > ADDRESSES_KEPT) seen.delete(seen.keys().next().value);
|
|
9040
|
+
}
|
|
9041
|
+
}
|
|
9042
|
+
};
|
|
9043
|
+
}
|
|
9044
|
+
function refuse(req, reply, problem) {
|
|
9045
|
+
const accept = String(req.headers.accept ?? "");
|
|
9046
|
+
const isPage = (req.method === "GET" || req.method === "HEAD") && (req.headers["sec-fetch-mode"] === "navigate" || accept.includes("text/html"));
|
|
9047
|
+
if (isPage) {
|
|
9048
|
+
return reply.status(403).header("content-type", "text/html; charset=utf-8").header("cache-control", "no-store").send(codePage(problem));
|
|
9049
|
+
}
|
|
9050
|
+
const error = problem === "locked" ? "too many tries" : problem === "wrong" ? "wrong code" : "access code required";
|
|
9051
|
+
return reply.status(403).send({ error });
|
|
9052
|
+
}
|
|
8329
9053
|
function registerAccessGuard(app, opts = {}) {
|
|
8330
|
-
const
|
|
8331
|
-
const
|
|
9054
|
+
const named = /* @__PURE__ */ new Set([...LOOPBACK_NAMES, ...(opts.allowedHosts ?? []).map((h) => h.trim().toLowerCase())]);
|
|
9055
|
+
const limiter = createLimiter(opts.now ?? Date.now);
|
|
8332
9056
|
app.addHook("onRequest", async (req, reply) => {
|
|
8333
9057
|
const host = hostnameOf(req.headers.host);
|
|
8334
|
-
if (!host || !
|
|
9058
|
+
if (!host || !(named.has(host) || isIPv4Literal(host))) {
|
|
8335
9059
|
return reply.status(403).send({ error: "forbidden host" });
|
|
8336
9060
|
}
|
|
8337
9061
|
if (req.headers["sec-fetch-site"] === "cross-site") {
|
|
8338
9062
|
const isNavigation = req.headers["sec-fetch-mode"] === "navigate" && (req.method === "GET" || req.method === "HEAD");
|
|
8339
9063
|
if (!isNavigation) return reply.status(403).send({ error: "cross-site request blocked" });
|
|
8340
9064
|
}
|
|
8341
|
-
|
|
9065
|
+
const code = opts.code?.();
|
|
9066
|
+
if (!code || fromThisComputer(req)) return;
|
|
9067
|
+
const ip = req.socket?.remoteAddress ?? "";
|
|
8342
9068
|
const q = req.query?.t;
|
|
8343
|
-
const fromQuery = typeof q === "string" ? q : void 0;
|
|
9069
|
+
const fromQuery = typeof q === "string" && q ? q : void 0;
|
|
8344
9070
|
const header = req.headers["x-access-token"];
|
|
8345
|
-
const
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
9071
|
+
const fromHeader = typeof header === "string" && header ? header : void 0;
|
|
9072
|
+
const port = opts.port?.() ?? null;
|
|
9073
|
+
const fromCookie = cookieValue(req.headers.cookie, cookieName(port)) ?? cookieValue(req.headers.cookie, ACCESS_COOKIE) ?? cookieValue(req.headers.cookie, LEGACY_ACCESS_COOKIE);
|
|
9074
|
+
if (limiter.locked(ip)) return refuse(req, reply, "locked");
|
|
9075
|
+
const supplied = [fromCookie, fromQuery, fromHeader].filter((v) => !!v);
|
|
9076
|
+
if (supplied.some((v) => codeMatches(code, v))) {
|
|
9077
|
+
if (fromQuery && codeMatches(code, fromQuery) && !(fromCookie && codeMatches(code, fromCookie))) {
|
|
9078
|
+
reply.header(
|
|
9079
|
+
"set-cookie",
|
|
9080
|
+
`${cookieName(port)}=${encodeURIComponent(code)}; Path=/; Max-Age=${COOKIE_MAX_AGE_S}; HttpOnly; SameSite=Strict`
|
|
9081
|
+
);
|
|
9082
|
+
}
|
|
9083
|
+
opts.onVisit?.(ip, req.headers["user-agent"]);
|
|
9084
|
+
return;
|
|
8351
9085
|
}
|
|
9086
|
+
for (const v of supplied) limiter.wrong(ip, v);
|
|
9087
|
+
if (supplied.length && limiter.locked(ip)) return refuse(req, reply, "locked");
|
|
9088
|
+
return refuse(req, reply, fromQuery ? "wrong" : null);
|
|
8352
9089
|
});
|
|
8353
9090
|
}
|
|
8354
9091
|
|
|
@@ -9335,14 +10072,14 @@ async function seamResidual(image, plan, source) {
|
|
|
9335
10072
|
const between = (a, b) => {
|
|
9336
10073
|
const limit = horizontal ? W : H;
|
|
9337
10074
|
if (a < 0 || b < 0 || a >= limit || b >= limit) return null;
|
|
9338
|
-
const
|
|
10075
|
+
const run4 = horizontal ? H : W;
|
|
9339
10076
|
let sum = 0;
|
|
9340
|
-
for (let i = 0; i <
|
|
10077
|
+
for (let i = 0; i < run4; i++) {
|
|
9341
10078
|
const ia = (horizontal ? i * W + a : a * W + i) * ch;
|
|
9342
10079
|
const ib = (horizontal ? i * W + b : b * W + i) * ch;
|
|
9343
10080
|
sum += Math.abs(data[ia] - data[ib]) + Math.abs(data[ia + 1] - data[ib + 1]) + Math.abs(data[ia + 2] - data[ib + 2]);
|
|
9344
10081
|
}
|
|
9345
|
-
return sum / (
|
|
10082
|
+
return sum / (run4 * 3);
|
|
9346
10083
|
};
|
|
9347
10084
|
const near = horizontal ? plan.left : plan.top;
|
|
9348
10085
|
const far = near + (horizontal ? source.width : source.height);
|
|
@@ -10344,13 +11081,13 @@ function beginRun(deps, id, view, chained) {
|
|
|
10344
11081
|
});
|
|
10345
11082
|
}
|
|
10346
11083
|
function endRun(deps, id, view, aborted) {
|
|
10347
|
-
const
|
|
10348
|
-
if (!
|
|
11084
|
+
const run4 = runs.get(id);
|
|
11085
|
+
if (!run4 || run4.status !== "running") return;
|
|
10349
11086
|
const rec = getPresenterDraft(deps.core, id);
|
|
10350
11087
|
const error = view ? rec?.views[view].error : rec?.readError;
|
|
10351
11088
|
const failed = !aborted && !!error && error !== "cancelled";
|
|
10352
11089
|
runs.set(id, {
|
|
10353
|
-
...
|
|
11090
|
+
...run4,
|
|
10354
11091
|
view,
|
|
10355
11092
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10356
11093
|
status: aborted ? "cancelled" : failed ? "failed" : "done",
|
|
@@ -11976,7 +12713,7 @@ function startSceneStudioJob(deps, input) {
|
|
|
11976
12713
|
prune2(input.brandId);
|
|
11977
12714
|
const ctrl = new AbortController();
|
|
11978
12715
|
running4.set(job.id, ctrl);
|
|
11979
|
-
const work =
|
|
12716
|
+
const work = run2(deps, job, { ...input, instruction, imageHashes: hashes, reading, ask }, ctrl.signal).finally(() => {
|
|
11980
12717
|
running4.delete(job.id);
|
|
11981
12718
|
tasks.delete(job.id);
|
|
11982
12719
|
});
|
|
@@ -12017,7 +12754,7 @@ async function read(deps, job, input, signal) {
|
|
|
12017
12754
|
if (!checked.ok) throw fail2(checked.error);
|
|
12018
12755
|
return checked.reading;
|
|
12019
12756
|
}
|
|
12020
|
-
async function
|
|
12757
|
+
async function run2(deps, job, input, signal) {
|
|
12021
12758
|
try {
|
|
12022
12759
|
let reading = input.reading ?? null;
|
|
12023
12760
|
if (input.kind !== "again") {
|
|
@@ -12280,7 +13017,7 @@ function createSceneExamples(deps) {
|
|
|
12280
13017
|
if (gone && gone !== hashOf2(example.file)) deps.release([gone]);
|
|
12281
13018
|
return wrote;
|
|
12282
13019
|
};
|
|
12283
|
-
async function
|
|
13020
|
+
async function run4(job, signal) {
|
|
12284
13021
|
const engine = await deps.engine();
|
|
12285
13022
|
if (!engine) throw new Error("Nothing can draw these right now. Connect Codex and try again.");
|
|
12286
13023
|
const engineId = engine.capabilities().id;
|
|
@@ -12478,7 +13215,7 @@ function createSceneExamples(deps) {
|
|
|
12478
13215
|
jobs2.set(k, job);
|
|
12479
13216
|
const ctrl = new AbortController();
|
|
12480
13217
|
controllers2.set(k, ctrl);
|
|
12481
|
-
const task =
|
|
13218
|
+
const task = run4(job, ctrl.signal).then(() => {
|
|
12482
13219
|
job.status = ctrl.signal.aborted ? "cancelled" : job.failed.length && !job.done.length ? "failed" : "done";
|
|
12483
13220
|
if (job.status === "failed") job.error = job.failed[0].error;
|
|
12484
13221
|
}).catch((err) => {
|
|
@@ -12745,21 +13482,21 @@ function sceneWork(j) {
|
|
|
12745
13482
|
}
|
|
12746
13483
|
function presenterWork(core, brandId) {
|
|
12747
13484
|
const out = [];
|
|
12748
|
-
for (const
|
|
12749
|
-
const rec = getPresenterDraft(core,
|
|
13485
|
+
for (const run4 of presenterDraftRuns(brandId)) {
|
|
13486
|
+
const rec = getPresenterDraft(core, run4.draftId);
|
|
12750
13487
|
if (!rec) continue;
|
|
12751
13488
|
const wanted = rec.extras ? PRESENTER_VIEWS : CORE_VIEWS;
|
|
12752
|
-
const view =
|
|
13489
|
+
const view = run4.view;
|
|
12753
13490
|
out.push({
|
|
12754
|
-
id: `presenter:${
|
|
13491
|
+
id: `presenter:${run4.draftId}:${run4.id}`,
|
|
12755
13492
|
kind: "presenter",
|
|
12756
|
-
status:
|
|
12757
|
-
step:
|
|
13493
|
+
status: run4.status,
|
|
13494
|
+
step: run4.status === "running" ? view ?? "reading" : view,
|
|
12758
13495
|
name: rec.name,
|
|
12759
13496
|
thumb: rec.views.portrait.hash ?? null,
|
|
12760
|
-
startedAt:
|
|
12761
|
-
finishedAt:
|
|
12762
|
-
error:
|
|
13497
|
+
startedAt: run4.startedAt,
|
|
13498
|
+
finishedAt: run4.finishedAt,
|
|
13499
|
+
error: run4.error,
|
|
12763
13500
|
draftId: rec.id,
|
|
12764
13501
|
presenterId: rec.presenterId ?? null,
|
|
12765
13502
|
done: wanted.filter((v) => rec.views[v].status === "approved").length,
|
|
@@ -12926,10 +13663,19 @@ function registerProjectRoutes(app, deps) {
|
|
|
12926
13663
|
core.store.addToSet(set.id, nodeIds);
|
|
12927
13664
|
return { ok: true, added: nodeIds.length, nodeIds: core.store.membersOf(set.id) };
|
|
12928
13665
|
});
|
|
13666
|
+
app.post("/api/sets/:id/nodes/remove", async (req, reply) => {
|
|
13667
|
+
const set = core.store.getSet(req.params.id);
|
|
13668
|
+
if (!set) return reply.status(404).send({ error: "set not found" });
|
|
13669
|
+
const raw = req.body?.nodeIds;
|
|
13670
|
+
const nodeIds = (Array.isArray(raw) ? raw : []).map(String).filter(Boolean);
|
|
13671
|
+
if (nodeIds.length === 0) return reply.status(400).send({ error: "nodeIds must name at least one shot" });
|
|
13672
|
+
core.store.removeFromSet(set.id, nodeIds);
|
|
13673
|
+
return { ok: true, nodeIds: core.store.membersOf(set.id) };
|
|
13674
|
+
});
|
|
12929
13675
|
app.delete("/api/sets/:id/nodes/:nodeId", async (req, reply) => {
|
|
12930
13676
|
const { id, nodeId } = req.params;
|
|
12931
13677
|
if (!core.store.getSet(id)) return reply.status(404).send({ error: "set not found" });
|
|
12932
|
-
core.store.removeFromSet(id, nodeId);
|
|
13678
|
+
core.store.removeFromSet(id, [nodeId]);
|
|
12933
13679
|
return { ok: true, nodeIds: core.store.membersOf(id) };
|
|
12934
13680
|
});
|
|
12935
13681
|
}
|
|
@@ -13196,6 +13942,43 @@ function registerImageRoutes(app, deps) {
|
|
|
13196
13942
|
|
|
13197
13943
|
// src/release/notes.data.ts
|
|
13198
13944
|
var RELEASES = [
|
|
13945
|
+
{
|
|
13946
|
+
version: "0.17.0",
|
|
13947
|
+
date: "2026-09-24",
|
|
13948
|
+
sections: [
|
|
13949
|
+
{
|
|
13950
|
+
heading: "Library",
|
|
13951
|
+
body: "Products, presenters and scenes of your own can be selected and then kept or deleted together from a bar at the bottom of the page, and renamed from their menu. Bookmarks are now Keepers, on every page and in every menu."
|
|
13952
|
+
},
|
|
13953
|
+
{
|
|
13954
|
+
heading: "Create",
|
|
13955
|
+
body: "Home, Create and the libraries share one tab row, with search and size at its end. On a phone, Create keeps its tabs readable and puts the set and the sort in one button beside search. Choosing a tab while scrolled down starts the list from the top."
|
|
13956
|
+
},
|
|
13957
|
+
{
|
|
13958
|
+
heading: "Fixes",
|
|
13959
|
+
body: "Tab counts on Create no longer jump while the page loads. Closing and opening the assets panel keeps the shot you were looking at in place, and alerts no longer cover the composer's buttons."
|
|
13960
|
+
}
|
|
13961
|
+
]
|
|
13962
|
+
},
|
|
13963
|
+
{
|
|
13964
|
+
version: "0.16.0",
|
|
13965
|
+
date: "2026-09-24",
|
|
13966
|
+
title: "Open Scenri on your phone or tablet, and Settings with one page for each thing.",
|
|
13967
|
+
sections: [
|
|
13968
|
+
{
|
|
13969
|
+
heading: "Local access",
|
|
13970
|
+
body: "Scenri now opens on a phone, tablet or another computer on the same Wi-Fi. Scan the QR code in Settings > Local access, or type the address and enter the six-digit code. The first start may ask whether Node can accept incoming connections; allow it, and if the firewall still stands in the way, Local access says so and, where it can, Allow Scenri fixes it through your computer's own prompt."
|
|
13971
|
+
},
|
|
13972
|
+
{
|
|
13973
|
+
heading: "Security",
|
|
13974
|
+
body: "Only devices with the code get in, and wrong guesses are slowed to a crawl. New code, on the computer running Scenri, signs every device out."
|
|
13975
|
+
},
|
|
13976
|
+
{
|
|
13977
|
+
heading: "Settings",
|
|
13978
|
+
body: "Providers, Appearance, Library, Local access, Updates and About each have their own page, and every delete sits under Danger zone. On a phone, a link to a Settings page opens that page."
|
|
13979
|
+
}
|
|
13980
|
+
]
|
|
13981
|
+
},
|
|
13199
13982
|
{
|
|
13200
13983
|
version: "0.15.1",
|
|
13201
13984
|
date: "2026-09-23",
|
|
@@ -14158,13 +14941,16 @@ function isNewsworthy(entry) {
|
|
|
14158
14941
|
// src/routes/updates.ts
|
|
14159
14942
|
function registerUpdateRoutes(app, deps) {
|
|
14160
14943
|
const { core, meta, updates, runtime } = deps;
|
|
14161
|
-
app.get("/api/version", async () => ({
|
|
14944
|
+
app.get("/api/version", async (req) => ({
|
|
14162
14945
|
name: meta.name,
|
|
14163
14946
|
version: meta.version,
|
|
14164
14947
|
schema: SCHEMA_VERSION,
|
|
14165
14948
|
installKind: runtime.installKind,
|
|
14166
14949
|
supervised: runtime.supervised,
|
|
14167
|
-
home: core.home
|
|
14950
|
+
home: core.home,
|
|
14951
|
+
// Settings hides what acts on this computer (its file manager, its
|
|
14952
|
+
// desktop) from a phone that opened Scenri over the Wi-Fi
|
|
14953
|
+
thisComputer: fromThisComputer(req)
|
|
14168
14954
|
}));
|
|
14169
14955
|
const ghSlug = repoSlug(meta.repository);
|
|
14170
14956
|
const UNRELEASED = "0.0.0";
|
|
@@ -14356,7 +15142,8 @@ function registerSystemRoutes(app, deps) {
|
|
|
14356
15142
|
const dbBytes = (await stat(dbPath).catch(() => null))?.size ?? 0;
|
|
14357
15143
|
return { dir: core.home, dbPath, images: files, bytes: bytes + dbBytes };
|
|
14358
15144
|
});
|
|
14359
|
-
app.post("/api/system/reveal", async (
|
|
15145
|
+
app.post("/api/system/reveal", async (req, reply) => {
|
|
15146
|
+
if (!fromThisComputer(req)) return reply.status(403).send({ error: "Only on the computer running Scenri." });
|
|
14360
15147
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
14361
15148
|
try {
|
|
14362
15149
|
spawn(cmd, [core.home], { detached: true, stdio: "ignore" }).unref();
|
|
@@ -14660,7 +15447,7 @@ function registerDesktopRoutes(app, deps) {
|
|
|
14660
15447
|
record: null
|
|
14661
15448
|
};
|
|
14662
15449
|
}
|
|
14663
|
-
const { installDeps } = await import('./cli-
|
|
15450
|
+
const { installDeps } = await import('./cli-Y5X5XXFA.js');
|
|
14664
15451
|
const { desktopStatus } = await import('./install-VXJPLYWM.js');
|
|
14665
15452
|
return desktopStatus(installDeps(runtime.entry));
|
|
14666
15453
|
});
|
|
@@ -14668,7 +15455,7 @@ function registerDesktopRoutes(app, deps) {
|
|
|
14668
15455
|
if (!runtime.entry) {
|
|
14669
15456
|
return { ok: false, reason: "unsupported", message: "Desktop shortcuts are not available on this system yet." };
|
|
14670
15457
|
}
|
|
14671
|
-
const { addToDesktop } = await import('./cli-
|
|
15458
|
+
const { addToDesktop } = await import('./cli-Y5X5XXFA.js');
|
|
14672
15459
|
return addToDesktop(runtime.entry);
|
|
14673
15460
|
});
|
|
14674
15461
|
app.get("/api/desktop", async () => {
|
|
@@ -14682,7 +15469,8 @@ function registerDesktopRoutes(app, deps) {
|
|
|
14682
15469
|
installKind: runtime.installKind
|
|
14683
15470
|
};
|
|
14684
15471
|
});
|
|
14685
|
-
app.post("/api/desktop/install", async (
|
|
15472
|
+
app.post("/api/desktop/install", async (req, reply) => {
|
|
15473
|
+
if (!fromThisComputer(req)) return reply.status(403).send({ error: "Only on the computer running Scenri." });
|
|
14686
15474
|
if (runtime.installKind === "dev") {
|
|
14687
15475
|
return reply.status(409).send({
|
|
14688
15476
|
error: "Running from a source checkout; there is no installed build to put on a desktop.",
|
|
@@ -14716,6 +15504,32 @@ function registerDesktopRoutes(app, deps) {
|
|
|
14716
15504
|
});
|
|
14717
15505
|
}
|
|
14718
15506
|
|
|
15507
|
+
// src/routes/phone.ts
|
|
15508
|
+
function registerPhoneRoutes(app, deps) {
|
|
15509
|
+
const { phone } = deps;
|
|
15510
|
+
app.get(
|
|
15511
|
+
"/api/phone",
|
|
15512
|
+
async (req) => phone.status(fromThisComputer(req), req.query?.fresh === "1")
|
|
15513
|
+
);
|
|
15514
|
+
app.post("/api/phone/code", async (req, reply) => {
|
|
15515
|
+
if (!fromThisComputer(req)) {
|
|
15516
|
+
return reply.status(403).send({ error: "Change this on the computer running Scenri." });
|
|
15517
|
+
}
|
|
15518
|
+
phone.renew();
|
|
15519
|
+
return reply.send(await phone.status(true));
|
|
15520
|
+
});
|
|
15521
|
+
app.get("/api/phone/help", async (req, reply) => {
|
|
15522
|
+
if (!fromThisComputer(req)) return reply.send({ firewall: "unknown" });
|
|
15523
|
+
return reply.send({ firewall: await phone.firewall() });
|
|
15524
|
+
});
|
|
15525
|
+
app.post("/api/phone/allow", async (req, reply) => {
|
|
15526
|
+
if (!fromThisComputer(req)) {
|
|
15527
|
+
return reply.status(403).send({ error: "Change this on the computer running Scenri." });
|
|
15528
|
+
}
|
|
15529
|
+
return reply.send(await phone.allow());
|
|
15530
|
+
});
|
|
15531
|
+
}
|
|
15532
|
+
|
|
14719
15533
|
// src/server.ts
|
|
14720
15534
|
function seedFor(sourceHash, width, height) {
|
|
14721
15535
|
let h = 2166136261;
|
|
@@ -14730,7 +15544,22 @@ function buildServer(opts) {
|
|
|
14730
15544
|
const { core, engines } = opts;
|
|
14731
15545
|
const meta = readMeta();
|
|
14732
15546
|
const app = Fastify({ logger: false });
|
|
14733
|
-
|
|
15547
|
+
const phone = createPhoneAccess({ store: core.store, bind: opts.bind, handler: () => app.routing, ...opts.phone });
|
|
15548
|
+
app.decorate("phone", phone);
|
|
15549
|
+
app.addHook("onClose", async () => phone.close());
|
|
15550
|
+
let port = null;
|
|
15551
|
+
registerAccessGuard(app, {
|
|
15552
|
+
...opts.access,
|
|
15553
|
+
code: () => phone.code,
|
|
15554
|
+
port: () => {
|
|
15555
|
+
if (port === null) {
|
|
15556
|
+
const addr = app.server.address();
|
|
15557
|
+
port = addr && typeof addr === "object" ? addr.port : null;
|
|
15558
|
+
}
|
|
15559
|
+
return port;
|
|
15560
|
+
},
|
|
15561
|
+
onVisit: (remote, ua) => phone.visit(remote, ua)
|
|
15562
|
+
});
|
|
14734
15563
|
const reserved = /* @__PURE__ */ new Map();
|
|
14735
15564
|
const runningGenerations = /* @__PURE__ */ new Map();
|
|
14736
15565
|
core.catalog.reconcileInterruptedJobs();
|
|
@@ -16302,6 +17131,7 @@ function buildServer(opts) {
|
|
|
16302
17131
|
return drained;
|
|
16303
17132
|
});
|
|
16304
17133
|
registerSystemRoutes(app, { core, thumbs });
|
|
17134
|
+
registerPhoneRoutes(app, { phone });
|
|
16305
17135
|
registerGuideRoutes(app, { core, version: meta.version });
|
|
16306
17136
|
registerDesktopRoutes(app, {
|
|
16307
17137
|
core,
|
|
@@ -16330,24 +17160,26 @@ function withStoredAssets(sent, stored) {
|
|
|
16330
17160
|
return out;
|
|
16331
17161
|
}
|
|
16332
17162
|
|
|
17163
|
+
// src/banner.ts
|
|
17164
|
+
function localUrl(host, port, code) {
|
|
17165
|
+
if (host === "::1") return `http://[::1]:${port}`;
|
|
17166
|
+
if (isLoopbackName(host) || isWildcardHost(host)) return `http://127.0.0.1:${port}`;
|
|
17167
|
+
return `http://${host.includes(":") ? `[${host}]` : host}:${port}/?t=${code}`;
|
|
17168
|
+
}
|
|
17169
|
+
function startLines(info) {
|
|
17170
|
+
const lines = [`Scenri Studio \u2192 ${localUrl(info.host, info.port, info.phone?.code ?? "")}`];
|
|
17171
|
+
if (info.phone?.address) lines.push(`on your phone \u2192 ${info.phone.address} code ${groupCode(info.phone.code)}`);
|
|
17172
|
+
lines.push(`data dir \u2192 ${info.home}`, "Keep this window open while Scenri is running.");
|
|
17173
|
+
if (!info.studioBuilt) lines.push("", "(studio UI not built, API only. Run: pnpm build)");
|
|
17174
|
+
return lines;
|
|
17175
|
+
}
|
|
17176
|
+
|
|
16333
17177
|
// src/serve.ts
|
|
16334
17178
|
var PORT = Number(process.env.SCENRI_PORT || 4747);
|
|
16335
17179
|
var HOST = process.env.SCENRI_HOST || "127.0.0.1";
|
|
16336
|
-
var LOOPBACK2 = ["127.0.0.1", "localhost", "::1"];
|
|
16337
|
-
function lanAddresses() {
|
|
16338
|
-
const out = [];
|
|
16339
|
-
for (const addrs of Object.values(networkInterfaces())) {
|
|
16340
|
-
for (const a of addrs ?? []) {
|
|
16341
|
-
if (a.family === "IPv4" || a.family === 4) {
|
|
16342
|
-
if (!a.internal) out.push(a.address);
|
|
16343
|
-
}
|
|
16344
|
-
}
|
|
16345
|
-
}
|
|
16346
|
-
return out;
|
|
16347
|
-
}
|
|
16348
17180
|
async function serve() {
|
|
16349
17181
|
try {
|
|
16350
|
-
await
|
|
17182
|
+
await run3();
|
|
16351
17183
|
} catch (err) {
|
|
16352
17184
|
if (err instanceof SchemaTooNewError) {
|
|
16353
17185
|
console.error(`
|
|
@@ -16358,7 +17190,7 @@ async function serve() {
|
|
|
16358
17190
|
throw err;
|
|
16359
17191
|
}
|
|
16360
17192
|
}
|
|
16361
|
-
async function
|
|
17193
|
+
async function run3() {
|
|
16362
17194
|
const core = createCore();
|
|
16363
17195
|
const stubs = process.env.SCENRI_DEMO_ENGINE === "1" ? [createDemoEngine((b) => core.images.save(b), demoOptionsFromEnv(process.env))] : [];
|
|
16364
17196
|
const engines = createEngineRegistry(core, stubs);
|
|
@@ -16373,16 +17205,15 @@ async function run2() {
|
|
|
16373
17205
|
const studioDist = candidates.find((p) => existsSync(p));
|
|
16374
17206
|
const supervised = process.env.SCENRI_SUPERVISED === "1";
|
|
16375
17207
|
const installKind = detectInstallKind(fileURLToPath(import.meta.url), core.home);
|
|
16376
|
-
const
|
|
16377
|
-
const token = onlyThisMachine ? void 0 : randomBytes(24).toString("base64url");
|
|
16378
|
-
const reachableAt = onlyThisMachine ? [] : HOST === "0.0.0.0" || HOST === "::" ? lanAddresses() : [HOST];
|
|
17208
|
+
const named = isLoopbackName(HOST) || isWildcardHost(HOST) || isIPv4Literal(HOST) ? [] : [HOST];
|
|
16379
17209
|
const app = buildServer({
|
|
16380
17210
|
core,
|
|
16381
17211
|
engines,
|
|
16382
17212
|
analyzer,
|
|
16383
17213
|
sizeReader,
|
|
16384
17214
|
studioDist,
|
|
16385
|
-
access: { allowedHosts:
|
|
17215
|
+
access: { allowedHosts: named },
|
|
17216
|
+
bind: process.env.SCENRI_HOST,
|
|
16386
17217
|
runtime: {
|
|
16387
17218
|
installKind,
|
|
16388
17219
|
supervised,
|
|
@@ -16451,32 +17282,26 @@ async function run2() {
|
|
|
16451
17282
|
void repairPresenterCrops(core, (line) => console.log(line)).catch(() => void 0);
|
|
16452
17283
|
app.updates.schedule();
|
|
16453
17284
|
app.content.schedule();
|
|
16454
|
-
const
|
|
16455
|
-
|
|
16456
|
-
|
|
16457
|
-
|
|
16458
|
-
|
|
16459
|
-
|
|
16460
|
-
|
|
16461
|
-
|
|
16462
|
-
|
|
16463
|
-
console.log(" Warning: the studio is reachable from your network.");
|
|
16464
|
-
console.log(" Anyone who opens a link above can generate on your API keys and");
|
|
16465
|
-
console.log(" delete your library. The token in the URL is the only thing gating");
|
|
16466
|
-
console.log(" that, and it is new on every run.");
|
|
16467
|
-
console.log(" For this machine only, unset SCENRI_HOST.\n");
|
|
16468
|
-
}
|
|
17285
|
+
const port = app.server.address()?.port ?? PORT;
|
|
17286
|
+
await app.phone.start(port).catch(() => void 0);
|
|
17287
|
+
const phone = await app.phone.status(true).catch(() => null);
|
|
17288
|
+
const localUrl2 = localUrl(HOST, port, app.phone.code);
|
|
17289
|
+
console.log("");
|
|
17290
|
+
for (const line of startLines({ host: HOST, port, home: core.home, studioBuilt: !!studioDist, phone })) {
|
|
17291
|
+
console.log(line ? ` ${line}` : "");
|
|
17292
|
+
}
|
|
17293
|
+
console.log("");
|
|
16469
17294
|
if (process.env.SCENRI_NO_OPEN !== "1") {
|
|
16470
17295
|
let told = false;
|
|
16471
17296
|
const tellUrl = () => {
|
|
16472
17297
|
if (told) return;
|
|
16473
17298
|
told = true;
|
|
16474
|
-
console.log(` Open ${
|
|
17299
|
+
console.log(` Open ${localUrl2} in your browser.
|
|
16475
17300
|
`);
|
|
16476
17301
|
};
|
|
16477
17302
|
try {
|
|
16478
17303
|
const { default: open } = await import('open');
|
|
16479
|
-
const child = await open(
|
|
17304
|
+
const child = await open(localUrl2);
|
|
16480
17305
|
child.once("error", tellUrl);
|
|
16481
17306
|
child.once("exit", (openExit) => {
|
|
16482
17307
|
if (openExit !== null && openExit !== 0) tellUrl();
|
|
@@ -16486,7 +17311,7 @@ async function run2() {
|
|
|
16486
17311
|
}
|
|
16487
17312
|
}
|
|
16488
17313
|
const ownEntry = fileURLToPath(import.meta.url);
|
|
16489
|
-
const { addToDesktop, installDeps } = await import('./cli-
|
|
17314
|
+
const { addToDesktop, installDeps } = await import('./cli-Y5X5XXFA.js');
|
|
16490
17315
|
const { refreshLauncher } = await import('./refresh-5HBDLLLB.js');
|
|
16491
17316
|
const { askOnTerminal, offerDesktop, shouldOfferDesktop } = await import('./offer-W6NJXMWT.js');
|
|
16492
17317
|
const { launcherInstalled } = await import('./paths-6ZXRZUHY.js');
|