@or3/cloud 0.1.106 → 0.1.112

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/assets/Caddyfile CHANGED
@@ -3,15 +3,40 @@
3
3
  # site handler runs, so Strict-Transport-Security is only ever emitted on
4
4
  # HTTPS responses (it is meaningless on plain HTTP).
5
5
  header {
6
+ # Apply after upstream headers to avoid duplicate security-policy values.
7
+ defer
6
8
  Strict-Transport-Security "max-age=31536000"
7
9
  X-Content-Type-Options "nosniff"
8
- X-Frame-Options "DENY"
9
10
  Referrer-Policy "strict-origin-when-cross-origin"
10
11
  Permissions-Policy "camera=(), microphone=(), geolocation=()"
12
+ }
13
+
14
+ # Only the two host-owned bridge documents can be embedded. Ordinary app
15
+ # pages still deny every ancestor, including same-origin frames.
16
+ @hostDocument {
17
+ not path /or3/portable-frame /streamsaver/mitm.html
18
+ }
19
+ header @hostDocument {
20
+ defer
21
+ X-Frame-Options "DENY"
11
22
  # Nuxt needs same-origin scripts plus inline hydration styles/scripts. The
12
23
  # browser exchanges OpenRouter PKCE authorization codes directly with the
13
24
  # OpenRouter API, so that origin must remain available to connect-src.
14
- Content-Security-Policy "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https://openrouter.ai; media-src 'self' blob:; worker-src 'self' blob:; frame-src 'none'; upgrade-insecure-requests"
25
+ Content-Security-Policy "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https://openrouter.ai; media-src 'self' blob:; worker-src 'self' blob:; frame-src 'self'; upgrade-insecure-requests"
26
+ }
27
+
28
+ # Preserve the portable route's hash-authorized, opaque-origin sandbox CSP.
29
+ # Applying the app policy here would block its worker or weaken containment.
30
+ @portableFrame path /or3/portable-frame
31
+ header @portableFrame >X-Frame-Options "SAMEORIGIN"
32
+
33
+ # The fallback streaming exporter uses this bundled same-origin bridge to
34
+ # register its download service worker; it cannot use an opaque origin.
35
+ @downloadBridge path /streamsaver/mitm.html
36
+ header @downloadBridge {
37
+ defer
38
+ X-Frame-Options "SAMEORIGIN"
39
+ Content-Security-Policy "default-src 'none'; script-src 'unsafe-inline'; connect-src 'self'; worker-src 'self'; frame-ancestors 'self'; base-uri 'none'; form-action 'none'"
15
40
  }
16
41
 
17
42
  encode zstd gzip
@@ -71,9 +71,9 @@ services:
71
71
  - CMD-SHELL
72
72
  - >-
73
73
  if [ -x /nodejs/bin/node ]; then
74
- exec /nodejs/bin/node -e "const fs=require('node:fs');try{fs.accessSync('/data',fs.constants.R_OK|fs.constants.W_OK);for(const p of [process.env.OR3_BASIC_AUTH_DB_PATH,process.env.OR3_SQLITE_DB_PATH].filter(Boolean)){const fd=fs.openSync(p,'r+');fs.closeSync(fd)}}catch{process.exit(1)}fetch('http://127.0.0.1:3000/api/health?deep=true').then(async r=>{const b=await r.json().catch(()=>({}));if(!r.ok||b.status!=='ok')process.exit(1)}).catch(()=>process.exit(1))";
74
+ exec /nodejs/bin/node -e "const fs=require('node:fs');try{const a=process.env.AUTH_PROVIDER||process.env.OR3_AUTH_PROVIDER||'basic-auth',s=process.env.NUXT_PUBLIC_STORAGE_PROVIDER||'fs',q=process.env.OR3_SYNC_PROVIDER||'sqlite';if(q!=='sqlite'||!(a==='basic-auth'&&s==='fs'||a==='clerk'&&s==='s3')||process.env.AUTH_PROVIDER&&process.env.OR3_AUTH_PROVIDER&&process.env.AUTH_PROVIDER!==process.env.OR3_AUTH_PROVIDER)throw Error('profile');fs.accessSync('/data',fs.constants.R_OK|fs.constants.W_OK);const paths=a==='basic-auth'?[process.env.OR3_BASIC_AUTH_DB_PATH,process.env.OR3_SQLITE_DB_PATH]:[process.env.OR3_SQLITE_DB_PATH];if(paths.some(p=>!p))throw Error('database path');for(const p of paths){const fd=fs.openSync(p,'r+');fs.closeSync(fd)}}catch{process.exit(1)}fetch('http://127.0.0.1:3000/api/health?deep=true').then(async r=>{const b=await r.json().catch(()=>({}));if(!r.ok||b.status!=='ok'||b.providers?.auth?.provider!==(process.env.AUTH_PROVIDER||process.env.OR3_AUTH_PROVIDER||'basic-auth')||b.providers?.sync?.provider!=='sqlite'||b.providers?.storage?.provider!==(process.env.NUXT_PUBLIC_STORAGE_PROVIDER||'fs'))process.exit(1)}).catch(()=>process.exit(1))";
75
75
  elif [ -x /usr/local/bin/node ]; then
76
- exec /usr/local/bin/node -e "const fs=require('node:fs');try{fs.accessSync('/data',fs.constants.R_OK|fs.constants.W_OK);for(const p of [process.env.OR3_BASIC_AUTH_DB_PATH,process.env.OR3_SQLITE_DB_PATH].filter(Boolean)){const fd=fs.openSync(p,'r+');fs.closeSync(fd)}}catch{process.exit(1)}fetch('http://127.0.0.1:3000/api/health?deep=true').then(async r=>{const b=await r.json().catch(()=>({}));if(!r.ok||b.status!=='ok')process.exit(1)}).catch(()=>process.exit(1))";
76
+ exec /usr/local/bin/node -e "const fs=require('node:fs');try{const a=process.env.AUTH_PROVIDER||process.env.OR3_AUTH_PROVIDER||'basic-auth',s=process.env.NUXT_PUBLIC_STORAGE_PROVIDER||'fs',q=process.env.OR3_SYNC_PROVIDER||'sqlite';if(q!=='sqlite'||!(a==='basic-auth'&&s==='fs'||a==='clerk'&&s==='s3')||process.env.AUTH_PROVIDER&&process.env.OR3_AUTH_PROVIDER&&process.env.AUTH_PROVIDER!==process.env.OR3_AUTH_PROVIDER)throw Error('profile');fs.accessSync('/data',fs.constants.R_OK|fs.constants.W_OK);const paths=a==='basic-auth'?[process.env.OR3_BASIC_AUTH_DB_PATH,process.env.OR3_SQLITE_DB_PATH]:[process.env.OR3_SQLITE_DB_PATH];if(paths.some(p=>!p))throw Error('database path');for(const p of paths){const fd=fs.openSync(p,'r+');fs.closeSync(fd)}}catch{process.exit(1)}fetch('http://127.0.0.1:3000/api/health?deep=true').then(async r=>{const b=await r.json().catch(()=>({}));if(!r.ok||b.status!=='ok'||b.providers?.auth?.provider!==(process.env.AUTH_PROVIDER||process.env.OR3_AUTH_PROVIDER||'basic-auth')||b.providers?.sync?.provider!=='sqlite'||b.providers?.storage?.provider!==(process.env.NUXT_PUBLIC_STORAGE_PROVIDER||'fs'))process.exit(1)}).catch(()=>process.exit(1))";
77
77
  else exit 127; fi
78
78
  interval: 10s
79
79
  timeout: 5s
@@ -20354,6 +20354,8 @@ async function writeEnvFileDetailed(updates, options = {}) {
20354
20354
 
20355
20355
  // ../../shared/admin/configuration.ts
20356
20356
  var siteSettingPaths = {
20357
+ OR3_STORAGE_CAPACITY_WARNING_PERCENT: ["storageCapacityWarningPercent"],
20358
+ OR3_STORAGE_CAPACITY_CRITICAL_PERCENT: ["storageCapacityCriticalPercent"],
20357
20359
  OR3_CHAT_TOOL_ITERATIONS: ["chatToolIterations"],
20358
20360
  OR3_CHAT_GENERATED_IMAGES: ["chatGeneratedImages"],
20359
20361
  OR3_SITE_NAME: ["or3", "site", "name"],
@@ -20558,9 +20560,23 @@ function quote(value) {
20558
20560
  return `'${value.replaceAll("'", "'\\''")}'`;
20559
20561
  }
20560
20562
  function redact(text, secrets = []) {
20561
- let output2 = text;
20562
- for (const secret of secrets.filter(Boolean))
20563
- output2 = output2.replaceAll(secret, "[REDACTED]");
20563
+ const mask = new Uint8Array(text.length);
20564
+ for (const secret of secrets.filter(Boolean)) {
20565
+ for (let index = text.indexOf(secret);index !== -1; index = text.indexOf(secret, index + 1))
20566
+ mask.fill(1, index, index + secret.length);
20567
+ }
20568
+ let output2 = "", cursor = 0;
20569
+ for (let index = 0;index < mask.length; ) {
20570
+ if (!mask[index]) {
20571
+ index += 1;
20572
+ continue;
20573
+ }
20574
+ output2 += `${text.slice(cursor, index)}[REDACTED]`;
20575
+ while (index < mask.length && mask[index])
20576
+ index += 1;
20577
+ cursor = index;
20578
+ }
20579
+ output2 += text.slice(cursor);
20564
20580
  output2 = output2.replace(/((?:PASSWORD|SECRET|TOKEN|JWT)\s*[=:]\s*)(?!\[REDACTED\])([^\r\n]+)/gi, "$1[REDACTED]");
20565
20581
  return output2;
20566
20582
  }
@@ -20817,10 +20833,128 @@ var SECRET_KEYS = [
20817
20833
  "OR3_BASIC_AUTH_BOOTSTRAP_PASSWORD",
20818
20834
  "OR3_AUTH_INVITE_TOKEN_SECRET",
20819
20835
  "OR3_STORAGE_FS_TOKEN_SECRET",
20836
+ "OR3_STORAGE_S3_ACCESS_KEY_ID",
20837
+ "OR3_STORAGE_S3_SECRET_ACCESS_KEY",
20838
+ "OR3_STORAGE_S3_SESSION_TOKEN",
20839
+ "NUXT_CLERK_SECRET_KEY",
20840
+ "NUXT_AUTH_INVITE_TOKEN_SECRET",
20841
+ "NUXT_ADMIN_AUTH_PASSWORD",
20842
+ "NUXT_ADMIN_AUTH_JWT_SECRET",
20843
+ "OR3_LIBRARY_LINK_SECRET",
20844
+ "NUXT_ADMIN_LIBRARY_LINK_SECRET",
20845
+ "OR3_PLUGIN_CONNECTION_SECRET",
20846
+ "NUXT_ADMIN_PLUGIN_CONNECTION_SECRET",
20847
+ "OR3_BACKGROUND_ENCRYPTION_KEY",
20848
+ "NUXT_BACKGROUND_JOBS_ENCRYPTION_KEY",
20849
+ "OR3_WEBHOOKS_ENCRYPTION_KEY",
20850
+ "NUXT_WEBHOOKS_ENCRYPTION_KEY",
20851
+ "OR3_CONNECT_ENCRYPTION_KEY",
20852
+ "NUXT_CONNECT_ENCRYPTION_KEY",
20853
+ "OR3_CONNECT_CLOUDFLARE_API_TOKEN",
20854
+ "NUXT_CONNECT_CLOUDFLARE_API_TOKEN",
20855
+ "OPENROUTER_API_KEY",
20856
+ "NUXT_OPENROUTER_API_KEY",
20857
+ "OR3_OPENAI_API_KEY",
20858
+ "NUXT_OPENAI_API_KEY",
20859
+ "OR3_BILLING_STRIPE_SECRET_KEY",
20860
+ "OR3_BILLING_STRIPE_WEBHOOK_SECRET",
20861
+ "CONVEX_SELF_HOSTED_ADMIN_KEY",
20862
+ "CONVEX_DEPLOY_KEY",
20820
20863
  "OR3_ADMIN_JWT_SECRET",
20821
20864
  "OR3_ADMIN_PASSWORD"
20822
20865
  ];
20866
+ function managedProviderProfile(env) {
20867
+ const auth = env.AUTH_PROVIDER ?? env.OR3_AUTH_PROVIDER ?? "basic-auth";
20868
+ const sync = env.OR3_SYNC_PROVIDER ?? "sqlite";
20869
+ const storage = env.NUXT_PUBLIC_STORAGE_PROVIDER ?? "fs";
20870
+ if (env.AUTH_PROVIDER && env.OR3_AUTH_PROVIDER && env.AUTH_PROVIDER !== env.OR3_AUTH_PROVIDER || sync !== "sqlite") {
20871
+ throw new Error("backup-profile-mismatch: unsupported or inconsistent managed provider profile.");
20872
+ }
20873
+ if (auth === "basic-auth" && storage === "fs")
20874
+ return "basic-sqlite-fs";
20875
+ if (auth === "clerk" && storage === "s3")
20876
+ return "clerk-sqlite-s3";
20877
+ throw new Error("backup-profile-mismatch: unsupported managed provider profile.");
20878
+ }
20879
+ function assertLegacyManagedProfile(env) {
20880
+ const overrides = {
20881
+ SSR_AUTH_ENABLED: "true",
20882
+ OR3_SYNC_ENABLED: "true",
20883
+ OR3_CLOUD_SYNC_ENABLED: "true",
20884
+ OR3_STORAGE_ENABLED: "true",
20885
+ OR3_CLOUD_STORAGE_ENABLED: "true",
20886
+ OR3_SQLITE_DRIVER: "better-sqlite3",
20887
+ OR3_STORAGE_PROVIDER: "fs",
20888
+ NUXT_AUTH_ENABLED: "true",
20889
+ NUXT_AUTH_PROVIDER: "basic-auth",
20890
+ NUXT_PUBLIC_AUTH_PROVIDER: "basic-auth",
20891
+ NUXT_PUBLIC_SSR_AUTH_ENABLED: "true",
20892
+ NUXT_SYNC_ENABLED: "true",
20893
+ NUXT_PUBLIC_SYNC_ENABLED: "true",
20894
+ NUXT_SYNC_PROVIDER: "sqlite",
20895
+ NUXT_PUBLIC_SYNC_PROVIDER: "sqlite",
20896
+ NUXT_STORAGE_ENABLED: "true",
20897
+ NUXT_PUBLIC_STORAGE_ENABLED: "true",
20898
+ NUXT_STORAGE_PROVIDER: "fs"
20899
+ };
20900
+ const ambiguous = /^(?:RUNTIME_CONFIG$|NUXT_(?:AUTH|SYNC|STORAGE|PUBLIC)$|NITRO_ENV_(?:PREFIX|EXPANSION)$|NITRO_(?:AUTH|SYNC|STORAGE|PUBLIC)(?:_|$))/;
20901
+ if (managedProviderProfile(env) !== "basic-sqlite-fs" || Object.entries(overrides).some(([key, expected]) => env[key] !== undefined && env[key] !== expected) || Object.keys(env).some((key) => ambiguous.test(key))) {
20902
+ throw new Error("Legacy managed state supports only the Basic/SQLite/filesystem profile; provider conversion requires a separate qualified deployment.");
20903
+ }
20904
+ }
20905
+ var ADOPTION_SECRET_KEYS = [
20906
+ "OR3_BASIC_AUTH_JWT_SECRET",
20907
+ "OR3_BASIC_AUTH_REFRESH_SECRET",
20908
+ "OR3_BASIC_AUTH_BOOTSTRAP_PASSWORD",
20909
+ "OR3_AUTH_INVITE_TOKEN_SECRET",
20910
+ "OR3_STORAGE_FS_TOKEN_SECRET",
20911
+ "OR3_ADMIN_PASSWORD",
20912
+ "OR3_ADMIN_JWT_SECRET",
20913
+ "NUXT_AUTH_INVITE_TOKEN_SECRET",
20914
+ "NUXT_ADMIN_AUTH_JWT_SECRET",
20915
+ "OR3_LIBRARY_LINK_SECRET",
20916
+ "NUXT_ADMIN_LIBRARY_LINK_SECRET",
20917
+ "OR3_PLUGIN_CONNECTION_SECRET",
20918
+ "NUXT_ADMIN_PLUGIN_CONNECTION_SECRET",
20919
+ "OR3_BACKGROUND_ENCRYPTION_KEY",
20920
+ "NUXT_BACKGROUND_JOBS_ENCRYPTION_KEY",
20921
+ "OR3_WEBHOOKS_ENCRYPTION_KEY",
20922
+ "NUXT_WEBHOOKS_ENCRYPTION_KEY",
20923
+ "OR3_CONNECT_ENCRYPTION_KEY",
20924
+ "NUXT_CONNECT_ENCRYPTION_KEY",
20925
+ "OR3_CONNECT_CLOUDFLARE_API_TOKEN",
20926
+ "NUXT_CONNECT_CLOUDFLARE_API_TOKEN",
20927
+ "OPENROUTER_API_KEY",
20928
+ "NUXT_OPENROUTER_API_KEY",
20929
+ "OR3_OPENAI_API_KEY",
20930
+ "NUXT_OPENAI_API_KEY"
20931
+ ];
20932
+ var ADOPTION_RUNTIME_KEYS = [
20933
+ "OR3_MARKETPLACE_REGISTRY_ORIGIN",
20934
+ "OR3_MARKETPLACE_INSTALL_ENABLED",
20935
+ "OR3_MARKETPLACE_RELEASE_KEYS",
20936
+ "OR3_INSTANCE_ID",
20937
+ "OR3_MARKETPLACE_MAX_ARTIFACT_BYTES",
20938
+ "OR3_MARKETPLACE_RESERVE_BYTES",
20939
+ "OR3_BACKGROUND_STREAMING_ENABLED",
20940
+ "OR3_BACKGROUND_STREAMING_PROVIDER",
20941
+ "OR3_BACKGROUND_MAX_JOBS",
20942
+ "OR3_BACKGROUND_MAX_JOBS_PER_USER",
20943
+ "OR3_BACKGROUND_JOB_TIMEOUT",
20944
+ "NUXT_BACKGROUND_JOBS_ENABLED",
20945
+ "NUXT_PUBLIC_BACKGROUND_STREAMING_ENABLED",
20946
+ "NUXT_BACKGROUND_JOBS_STORAGE_PROVIDER",
20947
+ "NUXT_BACKGROUND_JOBS_MAX_CONCURRENT_JOBS",
20948
+ "NUXT_BACKGROUND_JOBS_MAX_CONCURRENT_JOBS_PER_USER",
20949
+ "NUXT_BACKGROUND_JOBS_JOB_TIMEOUT_MS",
20950
+ "OR3_PLUGIN_ALLOWED_MODELS",
20951
+ "OR3_PLUGIN_MODEL_PRICES",
20952
+ "NUXT_ADMIN_PLUGIN_ALLOWED_MODELS",
20953
+ "NUXT_ADMIN_PLUGIN_MODEL_PRICES"
20954
+ ];
20823
20955
  var ALLOWED_ENV_KEYS = new Set([
20956
+ ...ADOPTION_SECRET_KEYS,
20957
+ ...ADOPTION_RUNTIME_KEYS,
20824
20958
  "SSR_AUTH_ENABLED",
20825
20959
  "NUXT_SECRETS_PROVIDER",
20826
20960
  "NUXT_SECRETS_NAMESPACE",
@@ -20837,6 +20971,7 @@ var ALLOWED_ENV_KEYS = new Set([
20837
20971
  "OR3_BASIC_AUTH_REFRESH_SECRET",
20838
20972
  "OR3_BASIC_AUTH_ACCESS_TTL_SECONDS",
20839
20973
  "OR3_BASIC_AUTH_REFRESH_TTL_SECONDS",
20974
+ "OR3_SQLITE_DRIVER",
20840
20975
  "OR3_BASIC_AUTH_DB_PATH",
20841
20976
  "OR3_BASIC_AUTH_BOOTSTRAP_EMAIL",
20842
20977
  "OR3_BASIC_AUTH_BOOTSTRAP_PASSWORD",
@@ -20853,6 +20988,8 @@ var ALLOWED_ENV_KEYS = new Set([
20853
20988
  "OR3_CLOUD_STORAGE_ENABLED",
20854
20989
  "NUXT_PUBLIC_STORAGE_PROVIDER",
20855
20990
  "OR3_STORAGE_FS_ROOT",
20991
+ "OR3_STORAGE_FS_CLEANUP_NAMESPACE_ID",
20992
+ "OR3_STORAGE_RETENTION_WORKSPACES",
20856
20993
  "OR3_STORAGE_FS_TOKEN_SECRET",
20857
20994
  "OR3_STORAGE_FS_URL_TTL_SECONDS",
20858
20995
  "OR3_ADMIN_USERNAME",
@@ -20893,7 +21030,9 @@ function dashboardUpdatesEnabled(directory) {
20893
21030
  }
20894
21031
  }
20895
21032
  function secretValues(env) {
20896
- return SECRET_KEYS.map((key) => env[key]).filter((value) => Boolean(value));
21033
+ const known = new Set(SECRET_KEYS);
21034
+ const secretName = /(?:^|_)(?:PASSWORD|SECRET|TOKEN|PRIVATE_KEY|API_KEY|ACCESS_KEY|AUTH_KEY)(?:_|$)/i;
21035
+ return Object.entries(env).filter(([key, value]) => Boolean(value) && (known.has(key) || secretName.test(key))).map(([, value]) => value);
20897
21036
  }
20898
21037
 
20899
21038
  // src/deployment/state-store.ts
@@ -20955,6 +21094,7 @@ import { dirname as dirname6, join as join8 } from "node:path";
20955
21094
  var execFile = promisify(execFileCallback);
20956
21095
  var COMMAND_TIMEOUT_MS = 180000;
20957
21096
  var STREAM_COMMAND_TIMEOUT_MS = 15 * 60000;
21097
+ var MAX_STREAM_COMMAND_TIMEOUT_MS = 4 * 60 * 60000;
20958
21098
  var PROCESS_ENV_PASSTHROUGH = [
20959
21099
  "PATH",
20960
21100
  "HOME",
@@ -21161,6 +21301,41 @@ async function resolvedComposeConfig(directory, mode, deadline = performance.now
21161
21301
  ${result.stderr}`);
21162
21302
  return result.stdout;
21163
21303
  }
21304
+ async function assertLegacyComposeProfile(directory, mode, env, deadline) {
21305
+ assertLegacyManagedProfile(env);
21306
+ let resolved2;
21307
+ try {
21308
+ resolved2 = await resolvedComposeConfig(directory, mode, deadline);
21309
+ } catch (error62) {
21310
+ throw new Error(redact(error62 instanceof Error ? error62.message : String(error62), secretValues(env)));
21311
+ }
21312
+ let service;
21313
+ try {
21314
+ service = parseComposeConfig(resolved2).services?.or3;
21315
+ } catch {
21316
+ throw new Error("Resolved Compose provider configuration is unreadable.");
21317
+ }
21318
+ if (!service || !service.environment || typeof service.environment !== "object")
21319
+ throw new Error("Resolved Compose provider environment is missing.");
21320
+ const entries = Array.isArray(service.environment) ? service.environment.map((entry) => {
21321
+ if (typeof entry !== "string" || !/^[A-Za-z_][A-Za-z0-9_]*=/.test(entry))
21322
+ throw new Error("Resolved Compose provider environment is ambiguous.");
21323
+ const index = entry.indexOf("=");
21324
+ return [entry.slice(0, index), entry.slice(index + 1)];
21325
+ }) : Object.entries(service.environment);
21326
+ const effective = {};
21327
+ const seen = new Set;
21328
+ for (const [key, value] of entries) {
21329
+ if (typeof key !== "string" || typeof value !== "string" || seen.has(key))
21330
+ throw new Error("Resolved Compose provider environment is ambiguous.");
21331
+ seen.add(key);
21332
+ effective[key] = value;
21333
+ }
21334
+ assertLegacyManagedProfile(effective);
21335
+ if ((effective.OR3_AUTH_PROVIDER || effective.AUTH_PROVIDER) !== "basic-auth" || effective.SSR_AUTH_ENABLED !== "true" || (effective.OR3_CLOUD_SYNC_ENABLED || effective.OR3_SYNC_ENABLED) !== "true" || effective.OR3_SYNC_PROVIDER !== "sqlite" || (effective.OR3_CLOUD_STORAGE_ENABLED || effective.OR3_STORAGE_ENABLED) !== "true" || (effective.OR3_STORAGE_PROVIDER || effective.NUXT_PUBLIC_STORAGE_PROVIDER) !== "fs") {
21336
+ throw new Error("Resolved Compose provider profile is missing required Basic/SQLite/filesystem settings.");
21337
+ }
21338
+ }
21164
21339
  async function assertSafeComposeBinding(directory, mode, env, deadline) {
21165
21340
  const config2 = await resolvedComposeConfig(directory, mode, deadline);
21166
21341
  const parsed = parseComposeConfig(config2);
@@ -21234,7 +21409,7 @@ async function assertRunningAppImage(directory, mode, expectedImage, deadline =
21234
21409
 
21235
21410
  // src/runtime/health.ts
21236
21411
  var DEEP_HEALTH_TIMEOUT_MS = 180000;
21237
- var HEALTH_SCRIPT = "const fs=require('node:fs');try{fs.accessSync('/data',fs.constants.R_OK|fs.constants.W_OK);for(const path of [process.env.OR3_BASIC_AUTH_DB_PATH,process.env.OR3_SQLITE_DB_PATH].filter(Boolean)){const fd=fs.openSync(path,'r+');fs.closeSync(fd)}}catch{process.exit(1)}fetch('http://127.0.0.1:3000/api/health?deep=true').then(async response=>{const body=await response.json().catch(()=>({}));if(!response.ok||body.status!=='ok')process.exit(1)}).catch(()=>process.exit(1))";
21412
+ var HEALTH_SCRIPT = "const fs=require('node:fs');try{const auth=process.env.AUTH_PROVIDER||process.env.OR3_AUTH_PROVIDER||'basic-auth',storage=process.env.NUXT_PUBLIC_STORAGE_PROVIDER||'fs',sync=process.env.OR3_SYNC_PROVIDER||'sqlite';if(sync!=='sqlite'||!(auth==='basic-auth'&&storage==='fs'||auth==='clerk'&&storage==='s3')||process.env.AUTH_PROVIDER&&process.env.OR3_AUTH_PROVIDER&&process.env.AUTH_PROVIDER!==process.env.OR3_AUTH_PROVIDER)throw Error('profile');fs.accessSync('/data',fs.constants.R_OK|fs.constants.W_OK);const paths=auth==='basic-auth'?[process.env.OR3_BASIC_AUTH_DB_PATH,process.env.OR3_SQLITE_DB_PATH]:[process.env.OR3_SQLITE_DB_PATH];if(paths.some(p=>!p))throw Error('database path');for(const path of paths){const fd=fs.openSync(path,'r+');fs.closeSync(fd)}}catch{process.exit(1)}fetch('http://127.0.0.1:3000/api/health?deep=true').then(async response=>{const body=await response.json().catch(()=>({}));if(!response.ok||body.status!=='ok'||body.providers?.auth?.provider!==(process.env.AUTH_PROVIDER||process.env.OR3_AUTH_PROVIDER||'basic-auth')||body.providers?.sync?.provider!=='sqlite'||body.providers?.storage?.provider!==(process.env.NUXT_PUBLIC_STORAGE_PROVIDER||'fs'))process.exit(1)}).catch(()=>process.exit(1))";
21238
21413
  var CONTAINER_NODE = "/nodejs/bin/node";
21239
21414
  var LEGACY_CONTAINER_NODE = "/usr/local/bin/node";
21240
21415
  var CONTAINER_NODE_SHELL = `if [ -x ${CONTAINER_NODE} ]; then exec ${CONTAINER_NODE} -e "$1"; elif [ -x ${LEGACY_CONTAINER_NODE} ]; then exec ${LEGACY_CONTAINER_NODE} -e "$1"; else exit 127; fi`;
@@ -21290,6 +21465,8 @@ async function restartCommand(directory, lifecycle) {
21290
21465
  const loaded = await loadManaged(directory);
21291
21466
  assertDeploymentDirectoryIdentity(directory, loaded.state);
21292
21467
  assertPhaseAllowsCommand("restart", loaded.state);
21468
+ assertLegacyManagedProfile(loaded.env);
21469
+ await assertLegacyComposeProfile(loaded.directory, loaded.state.mode, loaded.env, lifecycle?.deadline);
21293
21470
  if (lifecycle) {
21294
21471
  await assertSafeComposeBinding(directory, loaded.state.mode, loaded.env, lifecycle.deadline);
21295
21472
  await assertRunningAppImage(directory, loaded.state.mode, loaded.state.image, lifecycle.deadline);
@@ -317,6 +317,22 @@ async function readLastReceipt() {
317
317
  }
318
318
  }
319
319
 
320
+ /** Read-only allowlist: a recorded snapshot is not current checksum evidence. */
321
+ export function projectRecoveryState(state) {
322
+ const safeId = value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,100}$/.test(value) ? value : null;
323
+ const safeTime = value => typeof value === 'string' && value.length <= 64 && Number.isFinite(Date.parse(value)) ? value : null;
324
+ const snapshotId = safeId(state?.rollback?.backupId);
325
+ const pending = state?.incompleteOperation;
326
+ return {
327
+ observedAt: new Date().toISOString(), source: 'managed_operator',
328
+ snapshot: snapshotId ? { id: snapshotId, capturedAt: safeTime(state.rollback.createdAt), verification: 'operator_reported' } : null,
329
+ coverage: ['canonical', 'auth', 'files', 'configuration', 'billing_trials_support', 'credential_escrow', 'independent_copy'].map(component => ({ component, status: 'unknown', verifiedAt: null })),
330
+ lastRecoveryTestAt: null,
331
+ incompleteOperation: pending && safeId(pending.id) && ['init', 'update', 'restore', 'adopt', 'credentials-reset', 'backup', 'rollback'].includes(pending.operation)
332
+ ? { id: pending.id, operation: pending.operation, startedAt: safeTime(pending.startedAt) } : null,
333
+ };
334
+ }
335
+
320
336
  async function status(includeReceipt = false) {
321
337
  const version = await currentVersion();
322
338
  const [job, checked] = await Promise.all([readJob(), readReleaseCheck()]);
@@ -992,6 +1008,7 @@ async function main() {
992
1008
  if (await service.handle(service.controller, request, response)) return;
993
1009
  } catch { return send(response, 503, { code: 'controller_unavailable' }); }
994
1010
  }
1011
+ if (request.method === 'GET' && request.url === '/v2/recovery') return send(response, 200, { protocolVersion: 2, recovery: projectRecoveryState(await managedState()) });
995
1012
  if (request.method === 'GET' && route === '/status') return reply(200, await status(isV2));
996
1013
  if (request.method === 'POST' && route === '/check') {
997
1014
  if (!consumeRateLimit(checkAttempts, Date.now(), 30, 60_000)) {