thinkncollab-shell 0.0.19 → 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/LICENSE +82 -0
  2. package/Readme.md +246 -502
  3. package/dist/bin/thinknsh.js +8 -0
  4. package/dist/src/commands/auth/login.js +7 -0
  5. package/dist/src/commands/auth/logout.js +7 -0
  6. package/dist/src/commands/auth/register.js +7 -0
  7. package/dist/src/commands/auth/whoami.js +7 -0
  8. package/dist/src/commands/dock/bench.js +7 -0
  9. package/dist/src/commands/dock/ci.js +7 -0
  10. package/dist/src/commands/dock/cluster.js +7 -0
  11. package/dist/src/commands/dock/containers.js +7 -0
  12. package/dist/src/commands/dock/dock.js +7 -0
  13. package/dist/src/commands/github/commits.js +7 -0
  14. package/dist/src/commands/github/issue-create.js +7 -0
  15. package/dist/src/commands/github/issues.js +7 -0
  16. package/dist/src/commands/github/pulls.js +7 -0
  17. package/dist/src/commands/github/statusgh.js +7 -0
  18. package/dist/src/commands/index.js +7 -0
  19. package/dist/src/commands/message/say.js +7 -0
  20. package/dist/src/commands/room/join.js +7 -0
  21. package/dist/src/commands/room/leave.js +7 -0
  22. package/dist/src/commands/room/rooms.js +7 -0
  23. package/dist/src/commands/room/sync.js +7 -0
  24. package/dist/src/commands/sandbox/console.js +7 -0
  25. package/dist/src/commands/sandbox/dep.js +7 -0
  26. package/dist/src/commands/sandbox/deploy.js +7 -0
  27. package/dist/src/commands/sandbox/dev.js +7 -0
  28. package/dist/src/commands/sandbox/exec.js +7 -0
  29. package/dist/src/commands/sandbox/freeze.js +7 -0
  30. package/dist/src/commands/sandbox/push.js +7 -0
  31. package/dist/src/commands/sandbox/resume.js +7 -0
  32. package/dist/src/commands/sandbox/sandbox.js +7 -0
  33. package/dist/src/commands/sandbox/share.js +7 -0
  34. package/dist/src/commands/sandbox/snapshot.js +7 -0
  35. package/dist/src/commands/sandbox/sync-deps.js +7 -0
  36. package/dist/src/commands/shell/doctor.js +7 -0
  37. package/dist/src/commands/shell/notifications.js +7 -0
  38. package/dist/src/commands/shell/notify.js +7 -0
  39. package/dist/src/commands/shell/status.js +7 -0
  40. package/dist/src/commands/shell/version.js +7 -0
  41. package/dist/src/commands/tasks/build.js +7 -0
  42. package/dist/src/commands/tasks/complete.js +7 -0
  43. package/dist/src/commands/tasks/create-task.js +7 -0
  44. package/dist/src/commands/tasks/loadtest.js +7 -0
  45. package/dist/src/commands/tasks/task-status.js +7 -0
  46. package/dist/src/commands/tasks/task.js +7 -0
  47. package/dist/src/commands/tasks/tasks.js +7 -0
  48. package/dist/src/commands/team/invite.js +7 -0
  49. package/dist/src/commands/team/myteam.js +7 -0
  50. package/dist/src/commands/team/teammates.js +7 -0
  51. package/dist/src/commands/terminal/share.js +7 -0
  52. package/dist/src/core/api-client.js +7 -0
  53. package/dist/src/core/crypto/e2ee-vault.js +7 -0
  54. package/dist/src/core/dep-store.js +7 -0
  55. package/dist/src/core/dependency-tracker.js +7 -0
  56. package/dist/src/core/geekload.js +7 -0
  57. package/dist/src/core/local-runner.js +7 -0
  58. package/dist/src/core/multi-lang-scanner.js +7 -0
  59. package/dist/src/core/notification-window.js +8 -0
  60. package/dist/src/core/notify-auth.js +7 -0
  61. package/dist/src/core/project-config.js +7 -0
  62. package/dist/src/core/sandbox/arch-shim.js +7 -0
  63. package/dist/src/core/sandbox/capsule-builder.js +7 -0
  64. package/dist/src/core/sandbox/cgroups.js +7 -0
  65. package/dist/src/core/sandbox/container-monitor.js +7 -0
  66. package/dist/src/core/sandbox/crun-manager.js +7 -0
  67. package/dist/src/core/sandbox/engine.js +7 -0
  68. package/dist/src/core/sandbox/image-manager.js +7 -0
  69. package/dist/src/core/sandbox/image-store.js +7 -0
  70. package/dist/src/core/sandbox/index.js +7 -0
  71. package/dist/src/core/sandbox/macos-sandbox.js +7 -0
  72. package/dist/src/core/sandbox/microvm-manager.js +7 -0
  73. package/dist/src/core/sandbox/native-service-manager.js +7 -0
  74. package/dist/src/core/sandbox/network.js +7 -0
  75. package/dist/src/core/sandbox/oci-exporter.js +7 -0
  76. package/dist/src/core/sandbox/oci-puller.js +7 -0
  77. package/dist/src/core/sandbox/oci-runtime-spec.js +7 -0
  78. package/dist/src/core/sandbox/policy-engine.js +7 -0
  79. package/dist/src/core/sandbox/port-proxy-daemon.js +7 -0
  80. package/dist/src/core/sandbox/room-sync-manager.js +7 -0
  81. package/dist/src/core/sandbox/runtime-manager.js +7 -0
  82. package/dist/src/core/sandbox/snapshot-manager.js +7 -0
  83. package/dist/src/core/sandbox/storage.js +7 -0
  84. package/dist/src/core/sandbox/supervisor.js +7 -0
  85. package/dist/src/core/sandbox/tunnel-daemon.js +7 -0
  86. package/dist/src/core/sandbox/tunnel.js +7 -0
  87. package/dist/src/core/sandbox/window-launcher.js +7 -0
  88. package/dist/src/core/sandbox/windows-wsl2.js +7 -0
  89. package/dist/src/core/sandbox-window.js +8 -0
  90. package/dist/src/core/shell.js +7 -0
  91. package/dist/src/core/websocket.js +7 -0
  92. package/dist/src/dock/api/routes.js +7 -0
  93. package/dist/src/dock/api/server.js +7 -0
  94. package/dist/src/dock/api/wire.js +7 -0
  95. package/dist/src/dock/bench/memory.js +7 -0
  96. package/dist/src/dock/bench/runner.js +7 -0
  97. package/dist/src/dock/build/builder.js +7 -0
  98. package/dist/src/dock/build/buildfile.js +7 -0
  99. package/dist/src/dock/build/buildkit.js +7 -0
  100. package/dist/src/dock/build/changes.js +7 -0
  101. package/dist/src/dock/ci/actions.js +7 -0
  102. package/dist/src/dock/ci/runner.js +7 -0
  103. package/dist/src/dock/ci/workflow.js +7 -0
  104. package/dist/src/dock/cli/api.js +7 -0
  105. package/dist/src/dock/cli/args.js +7 -0
  106. package/dist/src/dock/cli/commands.js +7 -0
  107. package/dist/src/dock/cli/compose.js +7 -0
  108. package/dist/src/dock/cli/context.js +7 -0
  109. package/dist/src/dock/cli/files.js +7 -0
  110. package/dist/src/dock/cli/format.js +7 -0
  111. package/dist/src/dock/cli/manage.js +7 -0
  112. package/dist/src/dock/cli/moved.js +7 -0
  113. package/dist/src/dock/cli/services.js +7 -0
  114. package/dist/src/dock/cli/session.js +7 -0
  115. package/dist/src/dock/cli/workspace.js +7 -0
  116. package/dist/src/dock/client.js +7 -0
  117. package/dist/src/dock/compat/cases.js +7 -0
  118. package/dist/src/dock/compat/runner.js +7 -0
  119. package/dist/src/dock/config.js +7 -0
  120. package/dist/src/dock/daemon-main.js +7 -0
  121. package/dist/src/dock/daemon.js +7 -0
  122. package/dist/src/dock/deploy/compose.js +7 -0
  123. package/dist/src/dock/engine.js +7 -0
  124. package/dist/src/dock/image/archive.js +7 -0
  125. package/dist/src/dock/image/auth.js +7 -0
  126. package/dist/src/dock/image/reference.js +7 -0
  127. package/dist/src/dock/image/registry.js +7 -0
  128. package/dist/src/dock/image/tar.js +7 -0
  129. package/dist/src/dock/index.js +7 -0
  130. package/dist/src/dock/linux/host.js +7 -0
  131. package/dist/src/dock/paths.js +7 -0
  132. package/dist/src/dock/runtime/emulation.js +7 -0
  133. package/dist/src/dock/runtime/fsevents.js +7 -0
  134. package/dist/src/dock/runtime/fsops.js +7 -0
  135. package/dist/src/dock/runtime/hostfwd.js +7 -0
  136. package/dist/src/dock/runtime/logs.js +7 -0
  137. package/dist/src/dock/runtime/network.js +7 -0
  138. package/dist/src/dock/runtime/ports.js +7 -0
  139. package/dist/src/dock/runtime/rootfs.js +7 -0
  140. package/dist/src/dock/runtime/seccomp-profile.LICENSE +202 -0
  141. package/dist/src/dock/runtime/seccomp-profile.json +1227 -0
  142. package/dist/src/dock/runtime/seccomp.js +7 -0
  143. package/dist/src/dock/runtime/spec.js +7 -0
  144. package/dist/src/dock/runtime/stats.js +7 -0
  145. package/dist/src/dock/runtime/volumes.js +7 -0
  146. package/dist/src/dock/selftest.js +7 -0
  147. package/dist/src/dock/setup/app.js +7 -0
  148. package/dist/src/dock/setup/autostart.js +7 -0
  149. package/dist/src/dock/setup/index.js +7 -0
  150. package/dist/src/dock/setup/integrate.js +7 -0
  151. package/dist/src/dock/setup/migrate.js +7 -0
  152. package/dist/src/dock/state/store.js +7 -0
  153. package/dist/src/dock/ui/page.js +385 -0
  154. package/dist/src/dock/ui/server.js +7 -0
  155. package/dist/src/dock/version.js +7 -0
  156. package/dist/src/dock/workspace/apps.js +7 -0
  157. package/dist/src/dock/workspace/workspace.js +7 -0
  158. package/dist/src/events/notifications.js +7 -0
  159. package/dist/src/utils/logger.js +7 -0
  160. package/dist/src/utils/terminalCrypto.js +7 -0
  161. package/package.json +41 -16
  162. package/bin/thinknsh.js +0 -71
  163. package/src/commands/auth/login.js +0 -84
  164. package/src/commands/auth/logout.js +0 -32
  165. package/src/commands/auth/register.js +0 -52
  166. package/src/commands/auth/whoami.js +0 -52
  167. package/src/commands/github/commits.js +0 -58
  168. package/src/commands/github/issue-create.js +0 -59
  169. package/src/commands/github/issues.js +0 -57
  170. package/src/commands/github/pulls.js +0 -58
  171. package/src/commands/github/statusgh.js +0 -50
  172. package/src/commands/index.js +0 -136
  173. package/src/commands/message/say.js +0 -49
  174. package/src/commands/room/join.js +0 -142
  175. package/src/commands/room/leave.js +0 -24
  176. package/src/commands/room/rooms.js +0 -66
  177. package/src/commands/shell/notify.js +0 -88
  178. package/src/commands/shell/status.js +0 -73
  179. package/src/commands/shell/version.js +0 -18
  180. package/src/commands/tasks/build.js +0 -190
  181. package/src/commands/tasks/complete.js +0 -940
  182. package/src/commands/tasks/create-task.js +0 -101
  183. package/src/commands/tasks/loadtest.js +0 -101
  184. package/src/commands/tasks/suggestTask.js +0 -0
  185. package/src/commands/tasks/task-status.js +0 -72
  186. package/src/commands/tasks/task.js +0 -182
  187. package/src/commands/tasks/tasks.js +0 -184
  188. package/src/commands/team/invite.js +0 -46
  189. package/src/commands/team/myteam.js +0 -78
  190. package/src/commands/team/teammates.js +0 -52
  191. package/src/commands/terminal/share.js +0 -261
  192. package/src/core/api-client.js +0 -246
  193. package/src/core/geekload.js +0 -524
  194. package/src/core/local-runner.js +0 -861
  195. package/src/core/notification-window.js +0 -165
  196. package/src/core/project-config.js +0 -100
  197. package/src/core/shell.js +0 -734
  198. package/src/core/websocket.js +0 -482
  199. package/src/events/notifications.js +0 -184
  200. package/src/utils/logger.js +0 -159
  201. package/src/utils/terminalCrypto.js +0 -47
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const{DockDaemon}=require("./daemon"),log=o=>console.log(`${(new Date).toISOString()} ${o}`),daemon=new DockDaemon({log});let stopping=!1;async function shutdown(o){stopping||(stopping=!0,log(`received ${o}, shutting down`),await daemon.stop(),process.exit(0))}process.on("SIGINT",()=>shutdown("SIGINT")),process.on("SIGTERM",()=>shutdown("SIGTERM")),daemon.start().catch(o=>{log(`failed to start: ${o.message}`),process.exit(1)});
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),Store=require("./state/store"),ApiServer=require("./api/server"),{Engine}=require("./engine"),{ping}=require("./client"),{dockPaths}=require("./paths");function pidAlive(e){try{return process.kill(e,0),!0}catch(e){return"EPERM"===e.code}}function readPid(e){try{const s=parseInt(fs.readFileSync(e,"utf8").trim(),10);return Number.isInteger(s)&&s>0?s:null}catch(e){return null}}class DockDaemon{constructor({home:e,log:s}={}){this.paths=dockPaths(e),this.log=s||(()=>{}),this.store=null,this.engine=null,this.api=null}async start(){const{home:e,socket:s,db:t,pidFile:i}=this.paths;if(fs.mkdirSync(e,{recursive:!0,mode:448}),await ping(s)){const e=readPid(i);throw new Error(`dock daemon is already running${e?` (pid ${e})`:""} on ${s}`)}"win32"!==process.platform&&fs.existsSync(s)&&fs.rmSync(s,{force:!0});const r=readPid(i);r&&!pidAlive(r)&&fs.rmSync(i,{force:!0}),this.store=new Store(t),this.engine=new Engine({store:this.store,paths:this.paths,log:this.log}),this.api=new ApiServer({store:this.store,engine:this.engine,socketPath:s,home:e,log:this.log});try{await this.api.start()}catch(e){throw this.store.close(),e}"win32"!==process.platform&&fs.chmodSync(s,384),this._shareSocketWithGroup(s),fs.writeFileSync(i,String(process.pid)),this.store.recordEvent("daemon","start",String(process.pid)),this.log(`dock daemon listening on ${s} (pid ${process.pid}, schema v${this.store.schemaVersion()})`),this.engine.recover().catch(e=>this.log(`recover: ${e.message}`)),this.engine.startResourceManager()}_shareSocketWithGroup(e){if("linux"!==process.platform)return;const s=process.env.TNC_DOCK_SOCKET_GROUP||"tnc-dock";try{const t=fs.readFileSync("/etc/group","utf8").split("\n").find(e=>e.split(":")[0]===s);if(!t)return;fs.chownSync(e,0,Number(t.split(":")[2])),fs.chmodSync(e,432),this.log(`socket shared with group ${s}`)}catch(e){this.log(`could not share socket with group ${s}: ${e.message}`)}}async stop(){const{socket:e,pidFile:s}=this.paths;this.engine&&this.engine.shutdown(),this.api&&await this.api.stop(),this.store&&(this.store.recordEvent("daemon","stop",String(process.pid)),this.store.close()),readPid(s)===process.pid&&fs.rmSync(s,{force:!0}),"win32"!==process.platform&&fs.rmSync(e,{force:!0}),this.api=this.engine=this.store=null}}module.exports={DockDaemon,readPid,pidAlive};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),path=require("path"),yaml=require("js-yaml"),{expand,splitWords}=require("../build/buildfile");function composeError(e){const r=new Error(e);return r.statusCode=400,r}function readDotEnv(e){const r={};if(!fs.existsSync(e))return r;for(const t of fs.readFileSync(e,"utf8").split(/\r?\n/)){const e=/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/.exec(t);e&&!t.trim().startsWith("#")&&(r[e[1]]=e[2].replace(/^(['"])(.*)\1$/,"$2"))}return r}function interpolate(e,r){return"string"==typeof e?expand(e.replace(/\$\$/g,"\\$"),r):Array.isArray(e)?e.map(e=>interpolate(e,r)):e&&"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,t])=>[e,interpolate(t,r)])):e}function duration(e){if(null==e)return;if("number"==typeof e)return e;const r=/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(String(e).trim());if(!r)throw composeError(`invalid duration "${e}"`);return parseFloat(r[1])*{ms:.001,s:1,m:60,h:3600}[r[2]||"s"]}function memoryMB(e){if(void 0===e)return;const r=/^(\d+(?:\.\d+)?)\s*([kmg]?)b?$/i.exec(String(e).trim());if(!r)throw composeError(`invalid memory "${e}"`);return Math.round(parseFloat(r[1])*{"":1/1048576,k:1/1024,m:1,g:1024}[r[2].toLowerCase()])}function toList(e){return null==e?null:Array.isArray(e)?e.map(String):splitWords(String(e))}function parsePorts(e,r){const t=(e.expose||[]).map(e=>({containerPort:Number(String(e).split("/")[0]),hostPort:null}));return[...(e.ports||[]).map(e=>{if("object"==typeof e){if(e.protocol&&"tcp"!==e.protocol)throw composeError(`service ${r}: only tcp ports are supported`);return{containerPort:Number(e.target),hostPort:e.published?Number(e.published):null}}const[t,o="tcp"]=String(e).split("/");if("tcp"!==o)throw composeError(`service ${r}: only tcp ports are supported (${e})`);const s=t.split(":"),n=Number(s.pop()),i=s.length?Number(s.pop()):null;if(!n||0===s.length&&0===i)throw composeError(`service ${r}: invalid port "${e}"`);return{containerPort:n,hostPort:i}}),...t].filter((e,r,t)=>t.findIndex(r=>r.containerPort===e.containerPort)===r)}function parseVolumes(e,r){return(e.volumes||[]).map(e=>{const t="object"==typeof e?e:null,[o,s,n]=t?[t.source,t.target,t.read_only?"ro":""]:String(e).split(":");if(!s)throw composeError(`service ${r}: anonymous volumes are not supported on clusters; name the volume`);if(!o||o.startsWith(".")||o.startsWith("/")||o.startsWith("~")||"bind"===t?.type)throw composeError(`service ${r}: bind mounts (${o}) cannot be deployed to servers; use a named volume`);return{name:o,containerPath:s,readOnly:"ro"===n}})}function parseHealthcheck(e){return!e||e.disable?null:{test:Array.isArray(e.test)?e.test:["CMD-SHELL",String(e.test)],intervalSec:duration(e.interval),timeoutSec:duration(e.timeout),retries:e.retries,startPeriodSec:duration(e.start_period)}}function composeToStack(e,{stackName:r,env:t=process.env}={}){const o=path.dirname(path.resolve(e)),s=yaml.load(fs.readFileSync(e,"utf8"))||{},n={...readDotEnv(path.join(o,".env")),...t},i=interpolate(s,n),a=(r||i.name||path.basename(o)).toLowerCase().replace(/[^a-z0-9-]/g,"-");if(!i.services||"object"!=typeof i.services)throw composeError("compose file has no services");const c=[],l={};for(const[e,r]of Object.entries(i.services)){if(!r.image)throw composeError(`service ${e}: servers cannot build images; build and push it, then set "image:"${r.build?" (build: is only used locally)":""}`);r.build&&c.push(`service ${e}: "build" is ignored on the cluster; "${r.image}" will be pulled`),r.depends_on&&c.push(`service ${e}: depends_on is not enforced on the cluster; pods restart until their dependencies answer`);const t={};for(const e of[].concat(r.env_file||[]))Object.assign(t,readDotEnv(path.resolve(o,"object"==typeof e?e.path:e)));if(Array.isArray(r.environment))for(const e of r.environment){const r=String(e).indexOf("=");-1===r?void 0!==n[e]&&(t[e]=n[e]):t[e.slice(0,r)]=e.slice(r+1)}else for(const[e,o]of Object.entries(r.environment||{}))t[e]=null===o?n[e]??"":String(o);const s=r.deploy?.resources?.limits||{},i=r["x-tnc"]||{};l[e]={image:r.image,command:toList(r.entrypoint),args:toList(r.command),env:t,secretEnv:i.secrets||[],ports:parsePorts(r,e),replicas:r.deploy?.replicas??1,resources:{...s.cpus?{cpu:Number(s.cpus)}:{},...s.memory?{memoryMB:memoryMB(s.memory)}:{}},healthcheck:parseHealthcheck(r.healthcheck),volumes:parseVolumes(r,e),nodeSelector:i.nodeSelector||{},ingress:i.ingress||null}}return{stack:a,services:l,warnings:c}}module.exports={composeToStack,memoryMB,duration,readDotEnv,interpolate};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),path=require("path"),crypto=require("crypto"),{EventEmitter}=require("events"),{parseReference}=require("./image/reference"),{pullImage,pushImage,verifyLogin}=require("./image/registry"),{AuthStore,normalizeRegistry}=require("./image/auth"),{createLinuxHost}=require("./linux/host"),{Rootfs}=require("./runtime/rootfs"),{Network}=require("./runtime/network"),{PortProxies}=require("./runtime/ports"),{Volumes}=require("./runtime/volumes"),{buildSpec,mergeEnv,processArgs,resolveUser,additionalGids}=require("./runtime/spec"),{ContainerFs}=require("./runtime/fsops"),{Stats}=require("./runtime/stats"),{Emulation,parsePlatform}=require("./runtime/emulation"),{HostForward,HOST_IP,ENGINE_SOCK}=require("./runtime/hostfwd"),{FsEventRelay}=require("./runtime/fsevents"),archive=require("./image/archive"),{BUILD_FILE,HOST_ALIAS}=require("./api/wire"),NAME_RE=/^\/?[a-zA-Z0-9][a-zA-Z0-9_.-]+$/,CRUN_CREATE_ERROR=[/^crun: /,/^executable file `.*` not found/,/^open executable: /,/^exec(ve)? `.*`: /,/^chdir to `.*`: /],GO_ARCH={x64:"amd64",arm64:"arm64"},ADJECTIVES=["brave","calm","eager","fancy","gentle","happy","jolly","keen","lucid","merry","nifty","proud","quick","sharp","sunny","witty"],NOUNS=["aryabhata","bose","raman","kalam","sarabhai","ramanujan","bhabha","saha","chandra","khorana","visvesvaraya","dhawan","swaminathan","mahalanobis"];function httpError(t,e){const i=new Error(e);return i.statusCode=t,i}const shortId=t=>t.replace(/^sha256:/,"").slice(0,12),iso=t=>t?new Date(t).toISOString():"0001-01-01T00:00:00Z",sleep=t=>new Promise(e=>setTimeout(e,t));function redactEnv(t){return Array.isArray(t)?t.map(t=>String(t).replace(/=.*$/s,"=***")):t}function redactExecBody(t){return t&&t.Env?{...t,Env:redactEnv(t.Env)}:t}class Engine extends EventEmitter{constructor({store:t,paths:e,log:i=()=>{},hostFactory:s=createLinuxHost}){super(),this.store=t,this.paths=e,this.log=i,this.hostFactory=s,this._host=null,this.execs=new Map,this.waiters=new Map,this.exiting=new Set,this.timer=null,this.lastLiveness=0,this.healthTimers=new Map,this.starting=new Map,this.auth=new AuthStore(e.auth||path.join(e.home,"auth.json")),this.fs=new ContainerFs(this),this.stats=new Stats(this),this.emulation=new Emulation(this),this.hostfwd=new HostForward({engine:this,socketPath:e.socket,log:i}),this.fsevents=new FsEventRelay({engine:this,log:i});for(const t of[e.blobs,e.containers,e.volumes])fs.mkdirSync(t,{recursive:!0})}get host(){if(!this._host){try{this._host=this.hostFactory({log:this.log})}catch(t){throw httpError(501,t.message)}this.rootfs=new Rootfs({host:this._host,blobsDir:this.paths.blobs}),this.network=new Network({host:this._host,store:this.store}),this.ports=new PortProxies({host:this._host,log:this.log})}return this._host}get buildkit(){if(!this._buildkit){const{BuildKit:t}=require("./build/buildkit");this._buildkit=new t(this)}return this._buildkit}get volumes(){return this._volumes||(this._volumes=new Volumes({store:this.store,host:this.host,dir:this.paths.volumes,emit:(t,e,i,s)=>this.emitEvent(t,e,i,s),engineSocket:this.paths.socket,guestEngineSocket:ENGINE_SOCK})),this._volumes}async ready(){try{await this.host.ready()}catch(t){throw t.statusCode?t:httpError(500,t.message)}if(!this.kernelVersion){const t=await this.host.run(["uname","-r"]);0===t.code&&(this.kernelVersion=t.stdout.trim())}if("vm"===this.host.kind)await this.hostfwd.listen(),await this.hostfwd.ensureGuest();else if("wsl"===this.host.kind&&!this._wslGateway){const t=await this.host.run(["/bin/sh","-c","ip route show default | awk '{print $3; exit}'"]);this._wslGateway=t.stdout.trim()||null}}_hostGateway(t={}){return"vm"===this._host?.kind?HOST_IP:"wsl"===this._host?.kind?this._wslGateway:Object.values(t).find(t=>t.gateway)?.gateway||null}emitEvent(t,e,i,s={}){const n=this.store.recordEvent(t,e,i,s);this.emit("event",n)}_attrs(t,e={}){return{...this.store.getImage(t.image)?.config?.config?.Labels||{},...t.config.Config.Labels||{},image:t.config.imageRef,name:t.name,...e}}containerDir(t){return path.join(this.paths.containers,t)}logFile(t){return path.join(this.containerDir(t),"json.log")}exitFile(t){return path.join(this.containerDir(t),"exit")}linuxArch(){return GO_ARCH[process.arch]||process.arch}findImage(t){let e=null;try{e=parseReference(t).familiar}catch(t){}return this.store.findImage(t,e)}requireImage(t){const e=this.findImage(t);if(!e)throw httpError(404,`No such image: ${t}`);return e}_credentials(t,e){return e&&e.username&&"string"==typeof e.password?{username:e.username,password:e.password}:this.auth.get(t)}async pull(t,e=()=>{},{credentials:i,platform:s}={}){const n=parseReference(t),r="string"==typeof s?parsePlatform(s):s,{image:o,fresh:a}=await pullImage(n,{blobsDir:this.paths.blobs,arch:r?.arch||this.linuxArch(),variant:r?.variant,progress:e,credentials:this._credentials(n.domain,i)}),h=this.store.saveImage(o);return e({status:a?`Status: Downloaded newer image for ${n.familiar}`:`Status: Image is up to date for ${n.familiar}`}),this.emitEvent("image","pull",n.familiar,{name:n.familiar}),h}async push(t,e,i=()=>{},{credentials:s}={}){const n=parseReference(t);if(n.digest)throw httpError(400,`cannot push a digest reference: ${t}`);let r;if(e||t.lastIndexOf(":")>t.lastIndexOf("/"))r=[parseReference(e?`${n.familiarName}:${e}`:t)];else{const t=`${n.familiarName}:`;if(r=this.store.listImages().flatMap(t=>t.repoTags).filter(e=>e.startsWith(t)).map(t=>parseReference(t)),!r.length)throw httpError(404,`An image does not exist locally with the tag: ${n.familiarName}`)}const o=this._credentials(n.domain,s),a=[];for(const t of r){const e=this.findImage(t.familiar);if(!e)throw httpError(404,`An image does not exist locally with the tag: ${t.familiar}`);const s=(e.repoDigests||[]).map(t=>{try{return parseReference(t)}catch(t){return null}}).filter(e=>e&&e.domain===t.domain).map(t=>t.path),n=await pushImage(t,e,{blobsDir:this.paths.blobs,credentials:o,sources:s,progress:i}),r=`${t.familiarName}@${n.digest}`,h=(e.repoDigests||[]).filter(e=>!e.startsWith(`${t.familiarName}@`));this.store.saveImage({...e,repoTags:[],repoDigests:[...h,r]}),this.emitEvent("image","push",t.familiar,{name:t.familiar}),a.push({ref:t.familiar,...n})}return a}async login({username:t,password:e,serveraddress:i}={}){const s=normalizeRegistry(i);if(!t||"string"!=typeof e||!e)throw httpError(400,"username and password are required");return await verifyLogin(s,{username:t,password:e}),this.auth.set(s,{username:t,password:e}),this.emitEvent("registry","login",s,{name:s}),s}logout(t){const e=normalizeRegistry(t),i=this.auth.remove(e);return i&&this.emitEvent("registry","logout",e,{name:e}),{host:e,removed:i}}tagImage(t,e,i="latest"){const s=this.requireImage(t),n=parseReference(i?`${e}:${i}`:e);this.store.saveImage({...s,repoTags:[n.familiar]}),this.emitEvent("image","tag",s.id,{name:n.familiar})}async removeImage(t,{force:e=!1}={}){const i=this.requireImage(t);let s=null;try{s=parseReference(t).familiar}catch(t){}const n=this.store.listContainers().filter(t=>t.image===i.id);if(s&&i.repoTags.includes(s)&&i.repoTags.length>1)return this.store.setImageTags(i.id,i.repoTags.filter(t=>t!==s)),this.emitEvent("image","untag",i.id,{name:s}),[{Untagged:s}];if(n.length&&!e)throw httpError(409,`conflict: unable to remove repository reference "${t}" (must force) - container ${shortId(n[0].id)} is using its referenced image ${shortId(i.id)}`);if(n.length&&e)return this.store.setImageTags(i.id,[]),i.repoTags.map(t=>({Untagged:t}));this.store.deleteImage(i.id);const r=new Set;for(const t of this.store.listImages()){r.add(t.id);for(const e of t.layers||[])r.add(e.digest)}const o=[i.id,...(i.layers||[]).map(t=>t.digest)].filter(t=>!r.has(t));for(const t of o)fs.rmSync(path.join(this.paths.blobs,t.replace(/^sha256:/,"")),{force:!0});if(this._host&&await this.host.isUp().catch(()=>!1)){const t=(i.layers||[]).map(t=>t.digest).filter(t=>!r.has(t)).map(t=>t.replace(/^sha256:/,""));t.length&&await this.host.sh('cd "$1/layers" && shift && for h; do rm -rf "$h" "$h.done"; done',[this.rootfs.root,...t]).catch(()=>{})}return this.emitEvent("image","delete",i.id,{}),[...i.repoTags.map(t=>({Untagged:t})),...(i.repoDigests||[]).map(t=>({Untagged:t})),{Deleted:i.id}]}imageSummary(t){return{Id:t.id,ParentId:"",RepoTags:t.repoTags,RepoDigests:t.repoDigests||[],Created:Math.floor(t.createdAt/1e3),Size:t.size,SharedSize:-1,VirtualSize:t.size,Labels:t.config?.config?.Labels||{},Containers:this.store.listContainers().filter(e=>e.image===t.id).length}}imageInspect(t){const e=t.config||{};return{Id:t.id,RepoTags:t.repoTags,RepoDigests:t.repoDigests||[],Parent:"",Comment:e.comment||"",Created:e.created||iso(t.createdAt),Author:e.author||"",Config:e.config||{},Architecture:t.architecture||e.architecture,Variant:e.variant,Os:t.os||e.os,Size:t.size,VirtualSize:t.size,GraphDriver:{Name:"overlayfs",Data:null},RootFS:{Type:"layers",Layers:e.rootfs?.diff_ids||[]},Metadata:{LastTagTime:"0001-01-01T00:00:00Z"}}}async build(t,e,i){const{Builder:s,normalizeTag:n}=require("./build/builder");await this.ready();const r=path.join(this.paths.home,"build",crypto.randomBytes(8).toString("hex")),o=path.join(r,"context");fs.mkdirSync(o,{recursive:!0});try{const{spawnSync:a}=require("child_process");completeTar(t);const h=a("tar",["-xf",t,"-C",o],{encoding:"utf8"});if(0!==h.status)throw httpError(400,`invalid build context: ${(h.stderr||"").trim()}`);const c=e.buildFile||BUILD_FILE,l=path.resolve(o,c);if(!l.startsWith(o+path.sep)||!fs.existsSync(l))throw httpError(400,`Cannot locate the build file: ${c}`);const u=(e.tags||[]).map(n);let d=null;const m=path.join(o,".tnc-build-secrets");fs.existsSync(m)&&(d=path.join(r,"secrets"),fs.renameSync(m,d));const f=new s(this,{...e,contextDir:o,buildDir:r,out:i,secretsDir:d,platform:parsePlatform(e.platform)}),g=await f.run(l),p=this.store.saveImage({...g,repoTags:u});if(i({aux:{ID:p.id}}),e.quiet)i({stream:`${p.id}\n`});else{i({stream:`Successfully built ${shortId(p.id)}\n`});for(const t of u)i({stream:`Successfully tagged ${t}\n`})}for(const t of u)this.emitEvent("image","tag",p.id,{name:t});return p}finally{fs.rmSync(r,{recursive:!0,force:!0})}}findContainer(t){const e=this.store.findContainer(t);if(!e)throw httpError(404,`No such container: ${t}`);return e}_generateName(){for(let t=0;t<50;t++){const t=`${ADJECTIVES[crypto.randomInt(ADJECTIVES.length)]}_${NOUNS[crypto.randomInt(NOUNS.length)]}`;if(!this.store.findContainer(t))return t}return`dock_${crypto.randomBytes(4).toString("hex")}`}create(t,e={},{platform:i}={}){if(t&&!NAME_RE.test(t))throw httpError(400,`Invalid container name (${t}), only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed`);const s=t?t.replace(/^\//,""):this._generateName(),n=this.store.findContainer(s);if(n&&n.name===s)throw httpError(409,`Conflict. The container name "/${s}" is already in use by container "${n.id}". You have to remove (or rename) that container to be able to reuse that name.`);if(!e.Image)throw httpError(400,"Config cannot be empty in order to create a container");const r=this.requireImage(e.Image),o=parsePlatform(i);if(o&&r.architecture&&r.architecture!==o.arch)throw httpError(404,`No such image: ${e.Image} for linux/${o.arch} (the local one is linux/${r.architecture})`);for(const t of(e.HostConfig||{}).DeviceRequests||[])if("nvidia"===t.Driver||(t.Capabilities||[]).flat().includes("gpu"))throw httpError(400,"GPUs cannot be passed to containers by dock yet");const{HostConfig:a={},NetworkingConfig:h,...c}=e,l=joinedContainer(a.NetworkMode);if(l&&(this.findContainer(l),Object.keys(a.PortBindings||{}).length))throw httpError(400,"conflicting options: port publishing and the container network mode");const u=l?[]:this._wantedNetworks(a.NetworkMode,h?.EndpointsConfig);if(0===processArgs(c,r.config?.config||{}).length)throw httpError(400,"No command specified");const d=crypto.randomBytes(32).toString("hex"),m=this.volumes.resolve(a,r.config?.config||{});this._volumesFrom(a.VolumesFrom,m);const f=this.containerDir(d);fs.mkdirSync(f,{recursive:!0});const g=this.store.insertContainer({id:d,name:s,image:r.id,state:"created",config:{Config:c,HostConfig:a,imageRef:e.Image,networks:u},runtime:{restartCount:0,volumes:m.volumes,anonymousVolumes:m.anonymous,ociMounts:m.ociMounts,apiMounts:m.apiMounts}});return this.emitEvent("container","create",d,{...r.config?.config?.Labels||{},...c.Labels||{},image:e.Image,name:s}),{Id:g.id,Warnings:[]}}_volumesFrom(t=[],e){const i=new Set(e.ociMounts.map(t=>t.destination));for(const s of t||[]){const[t,n]=String(s).split(":"),r=this.findContainer(t),o="ro"===n;for(const t of r.runtime.ociMounts||[]){if("bind"!==t.type||i.has(t.destination))continue;const s=t.options.filter(t=>"ro"!==t&&"rw"!==t);e.ociMounts.push({...t,options:[...s,o||t.options.includes("ro")&&"rw"!==n?"ro":"rw"]});const a=(r.runtime.apiMounts||[]).find(e=>e.Destination===t.destination);a&&e.apiMounts.push({...a,RW:!o&&a.RW,Mode:o?"ro":a.Mode}),i.add(t.destination)}for(const t of r.runtime.volumes||[])e.volumes.includes(t)||e.volumes.push(t)}}_wantedNetworks(t,e={}){this.host;const i=this.network.resolve(t),s=[{name:i.name,aliases:(n=i,e?.[n.name]?.Aliases||e?.[t]?.Aliases||[])}];var n;for(const t of Object.keys(e||{})){const n=this.network.resolve(t);if(!s.some(t=>t.name===n.name)){if(["host","null"].includes(i.driver)||["host","null"].includes(n.driver))throw httpError(400,"container cannot be connected to multiple networks with one of the networks in host or none mode");s.push({name:n.name,aliases:e[t].Aliases||[]})}}return s}_hostname(t){return t.config.Config.Hostname||t.id.slice(0,12)}_hostsContent(t){const e=["127.0.0.1\tlocalhost","::1\tlocalhost ip6-localhost ip6-loopback"],i=t.runtime.networks||{},s=Object.values(i).find(t=>t.ip);s&&e.push(`${s.ip}\t${this._hostname(t)}`);const n=new Set;for(const[s,r]of Object.entries(i)){let i;try{i=this.network.resolve(s)}catch(t){continue}if(i.dns&&r.ip)for(const i of this.store.listContainers()){if(i.id===t.id||"running"!==i.state)continue;const r=i.runtime.networks?.[s];if(!r?.ip||n.has(`${s}:${i.id}`))continue;n.add(`${s}:${i.id}`);const o=[...new Set([i.name,...r.aliases||[],this._hostname(i),i.id.slice(0,12)])];e.push(`${r.ip}\t${o.join(" ")}`)}}const r=this._hostGateway(i),o=(t.config.HostConfig.ExtraHosts||[]).map(t=>{const e=t.search(/[:=]/);return[t.slice(0,e),t.slice(e+1)]});r&&!o.some(([t])=>t===HOST_ALIAS)&&e.push(`${r}\t${HOST_ALIAS} host.tnc.internal`);for(const[t,i]of o){const s="host-gateway"===i?r:i;s&&e.push(`${s}\t${t}`)}return e.join("\n")+"\n"}_refreshHosts(t){const e=new Set(t);for(const t of this.store.listContainers())if("running"===t.state&&Object.keys(t.runtime.networks||{}).some(t=>e.has(t)))try{fs.writeFileSync(path.join(this.containerDir(t.id),"hosts"),this._hostsContent(t))}catch(t){}}async _resolvConf(){if(this._resolv)return this._resolv;let t=await this.host.run(["cat","/run/systemd/resolve/resolv.conf"]);0!==t.code&&(t=await this.host.run(["cat","/etc/resolv.conf"]));const e=(t.stdout.match(/^nameserver\s+(\S+)/gm)||[]).map(t=>t.split(/\s+/)[1]).filter(t=>!t.startsWith("127."));return this._resolv=(e.length?e:["1.1.1.1","8.8.8.8"]).map(t=>`nameserver ${t}`).join("\n")+"\n",this._resolv}async _containerResolv(t,e){const i=e?(await this.host.run(["cat","/etc/resolv.conf"])).stdout:await this._resolvConf();return[...t.Dns?.length?t.Dns.map(t=>`nameserver ${t}`):i.split("\n").filter(t=>/^nameserver\s/.test(t)),...t.DnsSearch?.length?[`search ${t.DnsSearch.join(" ")}`]:i.split("\n").filter(t=>/^search\s/.test(t)),...t.DnsOptions?.length?[`options ${t.DnsOptions.join(" ")}`]:i.split("\n").filter(t=>/^options\s/.test(t))].join("\n")+"\n"}async _devices(t=[]){const e=[];for(const i of t||[]){const t=await this.host.run(["stat","-L","-c","%t %T %F %u %g %a",i.PathOnHost]);if(0!==t.code)throw httpError(400,`error gathering device information while adding custom device "${i.PathOnHost}": no such file or directory`);const[s,n,...r]=t.stdout.trim().split(" "),o=r.slice(0,-3).join(" "),[a,h,c]=r.slice(-3);if(!/character|block/.test(o))throw httpError(400,`${i.PathOnHost} is not a device`);e.push({path:i.PathInContainer||i.PathOnHost,type:/block/.test(o)?"b":"c",major:parseInt(s,16),minor:parseInt(n,16),fileMode:parseInt(c,8)|(/block/.test(o)?24576:8192),uid:Number(a),gid:Number(h),CgroupPermissions:i.CgroupPermissions||"rwm"})}return e}async start(t){const e=this.findContainer(t).id,i=this.starting.get(e);if(i)return await i.catch(()=>{}),"running"===this.store.getContainer(e)?.state?"notModified":this.start(e);const s=this._start(e);this.starting.set(e,s);try{return await s}finally{this.starting.delete(e)}}async _start(t){let e=this.findContainer(t);if("running"===e.state)return"notModified";if("removing"===e.state)throw httpError(409,`container ${e.id} is marked for removal and cannot be started`);await this.ready();for(let t=0;t<600&&!this.fs.idle(e.id);t++)await sleep(50);const i=this.store.getImage(e.image);if(!i)throw httpError(404,`No such image: ${e.config.imageRef}`);i.architecture&&i.architecture!==this.linuxArch()&&await this.emulation.ensure(i.architecture);const s=i.config?.config||{},{Config:n,HostConfig:r}=e.config,o=e.id,a=this.containerDir(o),h=t=>this.host.guestPath(t),c=n.WorkingDir||s.WorkingDir||"/",l=n.Hostname||o.slice(0,12);let u=null,d={};try{this.volumes.linuxSide&&e.runtime.volumes?.length&&await this.host.sh('for d; do mkdir -p "$d"; done',e.runtime.volumes.map(t=>this.volumes.linuxDir(t)));const t=await this.rootfs.prepare(o,i,c),m=n.User||s.User||"",f=m&&m.split(":").some(t=>!/^\d+$/.test(t))?resolveUser(m,await this.rootfs.readFile(o,"/etc/passwd"),await this.rootfs.readFile(o,"/etc/group")):resolveUser(m);if(r.GroupAdd?.length){const t=r.GroupAdd.some(t=>!/^\d+$/.test(String(t)));f.additionalGids=additionalGids(r.GroupAdd,t?await this.rootfs.readFile(o,"/etc/group"):"")}const g=await this._devices(r.Devices),p=joinedContainer(r.NetworkMode);let w=null;if(p){if(w=this.findContainer(p),"running"!==w.state||!w.runtime.network)throw httpError(409,`cannot join the network of ${w.name}: it is not running`);u={nsPath:w.runtime.network.nsPath,endpoints:{}}}else u=await this.network.attachAll(o,e.config.networks||this._wantedNetworks(r.NetworkMode));const y=Object.values(u.endpoints)[0]||{};if(fs.writeFileSync(path.join(a,"hostname"),l+"\n"),w)for(const t of["hosts","resolv.conf"])fs.copyFileSync(path.join(this.containerDir(w.id),t),path.join(a,t));else fs.writeFileSync(path.join(a,"hosts"),this._hostsContent({...e,runtime:{...e.runtime,networks:u.endpoints}})),fs.writeFileSync(path.join(a,"resolv.conf"),await this._containerResolv(r,null===u.nsPath));const v=mergeEnv(s.Env,[`HOSTNAME=${l}`,...n.Env||[]]),{shim:C}=this.host.binaries(),S=processArgs(n,s),E=buildSpec({id:o,rootfs:t,cwd:c,user:f,hostname:l,nsPath:u.nsPath,hostConfig:r,terminal:!!n.Tty,devices:g,args:r.Init?["/sbin/tnc-init","init","--",...S]:S,env:v,mounts:[...r.Init?[{destination:"/sbin/tnc-init",type:"bind",source:C,options:["rbind","ro"]}]:[],{destination:"/etc/hostname",type:"bind",source:h(path.join(a,"hostname")),options:["rbind","rw"]},{destination:"/etc/hosts",type:"bind",source:h(path.join(a,"hosts")),options:["rbind","rw"]},{destination:"/etc/resolv.conf",type:"bind",source:h(path.join(a,"resolv.conf")),options:["rbind","rw"]},...e.runtime.ociMounts||[]]});fs.writeFileSync(path.join(a,"config.json"),JSON.stringify(E,null,2)),fs.rmSync(this.exitFile(o),{force:!0}),y.ip&&(d=await this.ports.start(o,r.PortBindings,y.ip));const{shim:b,crun:_}=this.host.binaries(),k=fs.existsSync(this.logFile(o))?fs.statSync(this.logFile(o)).size:0,$=["--log",h(this.logFile(o)),"--exit",h(this.exitFile(o)),"--ctl",this.rootfs.ctlFifo(o),...logRotationArgs(r.LogConfig)],I=[_,"--cgroup-manager=cgroupfs","run","--bundle",h(a)];n.OpenStdin&&$.push("--stdin",this.rootfs.stdinFifo(o)),n.Tty&&($.push("--console",this.rootfs.consoleSock(o)),I.push("--console-socket",this.rootfs.consoleSock(o))),await this.host.run([_,"delete","-f",o]),await this.host.check([b,...$,"--",...I,o]);const x=await this._waitForStart(o,k);return e=this.store.updateContainer(o,{state:"running",runtime:{...e.runtime,pid:x,health:null,networks:u.endpoints,network:{nsPath:u.nsPath},ip:y.ip||null,ports:d,startedAt:Date.now(),finishedAt:null,exitCode:0,error:"",manualStop:!1,paused:!1}}),this._refreshHosts(Object.keys(u.endpoints)),this.fsevents.track(o,e.runtime.apiMounts),this.emitEvent("container","start",o,this._attrs(e)),this._ensureReconciler(),this._startHealth(e),"started"}catch(t){this.ports&&this.ports.stop(o),u&&await this.network.detachAll(o,u.endpoints),await this.rootfs.unmount(o).catch(()=>{});const i=t.exitCode??128;throw this.store.updateContainer(o,{state:"created"===e.state?"created":"exited",runtime:{...e.runtime,error:t.message,exitCode:i,finishedAt:Date.now()}}),t.statusCode?t:httpError(500,t.message)}}async _waitForStart(t,e){const{crun:i}=this.host.binaries();for(let s=0;s<40;s++){if(fs.existsSync(this.exitFile(t))){const i=parseInt(fs.readFileSync(this.exitFile(t),"utf8"),10),s=this._crunErrorSince(t,e);if(s){fs.rmSync(this.exitFile(t),{force:!0});const e=httpError(400,`failed to create task for container: OCI runtime create failed: ${s}`);throw e.exitCode=/not found|No such file/.test(s)?127:/ermission denied/.test(s)?126:i||127,e}return 0}const s=await this.host.run([i,"state",t]);if(0===s.code)try{const t=JSON.parse(s.stdout);if("running"===t.status)return t.pid||0}catch(t){}await sleep(50)}return 0}_crunErrorSince(t,e){try{const i=fs.openSync(this.logFile(t),"r"),s=fs.fstatSync(i).size,n=Buffer.alloc(Math.max(0,s-e));fs.readSync(i,n,0,n.length,e),fs.closeSync(i);for(const t of n.toString("utf8").split("\n")){if(!t)continue;const e=JSON.parse(t);if("stderr"===e.stream&&CRUN_CREATE_ERROR.some(t=>t.test(e.log)))return e.log.replace(/^crun: /,"").trim()}}catch(t){}return null}_waitFor(t,e){return new Promise(i=>{this.waiters.has(t)||this.waiters.set(t,[]),this.waiters.get(t).push({condition:e,resolve:i})})}_resolveWaiters(t,e,i){const s=this.waiters.get(t)||[],n=[];for(const t of s)"removed"===e||"removed"!==t.condition?t.resolve({StatusCode:i,Error:null}):n.push(t);n.length?this.waiters.set(t,n):this.waiters.delete(t)}wait(t,e="not-running"){const i=this.findContainer(t);if(!["not-running","next-exit","removed"].includes(e))throw httpError(400,`invalid condition: "${e}"`);return"not-running"===e&&"running"!==i.state?Promise.resolve({StatusCode:i.runtime.exitCode||0,Error:null}):this._waitFor(i.id,e)}async _signal(t,e){const{crun:i}=this.host.binaries();return 0===(await this.host.run([i,"kill",t.id,String(e).replace(/^SIG/,"")])).code}async stop(t,e){const i=this.findContainer(t);if("running"!==i.state)return"notModified";await this.ready(),await this._thaw(i),this.store.updateContainer(i.id,{runtime:{...i.runtime,manualStop:!0}});const s=this.store.getImage(i.image)?.config?.config||{},n=i.config.Config.StopSignal||s.StopSignal||"SIGTERM",r=Number.isFinite(e)?e:i.config.Config.StopTimeout??10,o=this._waitFor(i.id,"next-exit");return await this._signal(i,n),await Promise.race([o,sleep(1e3*r).then(()=>null)])||(await this._signal(i,"SIGKILL"),await Promise.race([o,sleep(1e4)])),"stopped"}async kill(t,e="SIGKILL"){const i=this.findContainer(t);if("running"!==i.state)throw httpError(409,`Cannot kill container: ${t}: Container ${i.id} is not running`);if(await this.ready(),"KILL"!==String(e).replace(/^SIG/,"")&&9!==e&&"9"!==e||await this._thaw(i),this.store.updateContainer(i.id,{runtime:{...i.runtime,manualStop:!0}}),!await this._signal(i,e))throw httpError(500,`Cannot kill container ${t}: crun kill failed`)}async restart(t,e){await this.stop(t,e),await this.start(t)}async remove(t,{force:e=!1,volumes:i=!1}={}){let s=this.findContainer(t);if("running"===s.state&&!e)throw httpError(409,`You cannot remove a running container ${s.id}. Stop the container before attempting removal or force remove`);if("removing"===s.state)return;const n="running"===s.state;if(s=this.store.updateContainer(s.id,{state:"removing",runtime:{...s.runtime,manualStop:!0}}),n){await this.ready(),await this._thaw(s);const t=this._waitFor(s.id,"next-exit");await this._signal(s,"SIGKILL"),await Promise.race([t,sleep(1e4)]),s=this.store.getContainer(s.id)}await this._destroy(s,i)}async _destroy(t,e){if(this._host&&await this.host.isUp().catch(()=>!1)&&await this.rootfs.remove(t.id).catch(e=>this.log(`rootfs cleanup for ${t.id}: ${e.message}`)),fs.rmSync(this.containerDir(t.id),{recursive:!0,force:!0}),this.store.deleteContainer(t.id),e)for(const e of t.runtime.anonymousVolumes||[])try{this.volumes.remove(e)}catch(t){}this.emitEvent("container","destroy",t.id,this._attrs(t)),this._resolveWaiters(t.id,"removed",t.runtime.exitCode||0)}async pause(t){const e=this.findContainer(t);if("running"!==e.state)throw httpError(409,`Container ${e.id} is not running`);if(e.runtime.paused)throw httpError(409,`Container ${e.id} is already paused`);await this.ready();const{crun:i}=this.host.binaries(),s=await this.host.run([i,"pause",e.id]);if(0!==s.code)throw httpError(500,`Cannot pause container ${e.id}: ${(s.stderr||s.stdout).trim()}`);const n=this.store.getContainer(e.id);this.store.updateContainer(e.id,{runtime:{...n.runtime,paused:!0}}),this.emitEvent("container","pause",e.id,this._attrs(e))}async unpause(t){const e=this.findContainer(t);if(!e.runtime.paused||"running"!==e.state)throw httpError(409,`Container ${e.id} is not paused`);await this.ready(),await this._thaw(e,!0),this.emitEvent("container","unpause",e.id,this._attrs(e))}async _thaw(t,e=!1){const i=this.store.getContainer(t.id);if(!i?.runtime.paused)return;const{crun:s}=this.host.binaries(),n=await this.host.run([s,"resume",t.id]);if(0!==n.code&&e)throw httpError(500,`Cannot unpause container ${t.id}: ${(n.stderr||n.stdout).trim()}`);this.store.updateContainer(t.id,{runtime:{...this.store.getContainer(t.id).runtime,paused:!1}})}rename(t,e){const i=this.findContainer(t);if(!e||!NAME_RE.test(e))throw httpError(400,`Invalid container name (${e}), only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed`);const s=e.replace(/^\//,"");if(s===i.name)throw httpError(409,"Renaming a container with the same name as its current name");const n=this.store.findContainer(s);if(n&&n.name===s)throw httpError(409,`Conflict. The container name "/${s}" is already in use by container "${n.id}". You have to remove (or rename) that container to be able to reuse that name.`);this.store.updateContainer(i.id,{name:s}),this._refreshHosts(Object.keys(i.runtime.networks||{})),this.emitEvent("container","rename",i.id,this._attrs({...i,name:s},{oldName:`/${i.name}`}))}_saveConfig(t,e){return this.store.db.prepare("UPDATE containers SET config = ? WHERE id = ?").run(JSON.stringify(e),t),this.store.getContainer(t)}async update(t,e={}){const i=this.findContainer(t),s=Object.fromEntries(["Memory","MemorySwap","MemoryReservation","NanoCpus","CpuShares","CpuQuota","CpuPeriod","CpusetCpus","CpusetMems","PidsLimit","BlkioWeight"].filter(t=>void 0!==e[t]&&null!==e[t]&&""!==e[t]&&0!==e[t]).map(t=>[t,e[t]])),n={...i.config.HostConfig,...s};if(e.RestartPolicy?.Name){if(n.AutoRemove&&"no"!==e.RestartPolicy.Name)throw httpError(400,"Restart policy cannot be updated because AutoRemove is enabled for the container");n.RestartPolicy={Name:e.RestartPolicy.Name,MaximumRetryCount:e.RestartPolicy.MaximumRetryCount||0}}if(n.Memory>0&&n.MemoryReservation>n.Memory)throw httpError(400,"Minimum memory limit can not be less than memory reservation limit, see usage");if("running"===i.state&&Object.keys(s).length){await this.ready();const{devices:t,...e}=buildSpec({id:i.id,rootfs:"/",args:["x"],env:[],cwd:"/",user:{uid:0,gid:0},hostname:"x",mounts:[],hostConfig:{...n,Ulimits:[],Sysctls:{}}}).linux.resources,s=path.join(this.containerDir(i.id),"update.json");fs.writeFileSync(s,JSON.stringify(e));const{crun:r}=this.host.binaries(),o=await this.host.run([r,"update","--resources",this.host.guestPath(s),i.id]);if(fs.rmSync(s,{force:!0}),0!==o.code)throw httpError(500,`Cannot update container ${i.id}: ${(o.stderr||o.stdout).trim()}`)}return this._saveConfig(i.id,{...i.config,HostConfig:n}),this.emitEvent("container","update",i.id,this._attrs(i)),{Warnings:[]}}async commit(t,{repo:e,tag:i,comment:s="",author:n="",pause:r=!0,changes:o=[],config:a={}}={}){const{applyChanges:h}=require("./build/changes"),{COMMIT:c}=require("./build/builder"),l=this.findContainer(t);await this.ready();const u=this.store.getImage(l.image);if(!u)throw httpError(404,`No such image: ${l.config.imageRef}`);const d="running"===l.state&&r&&!l.runtime.paused;let m;d&&await this.pause(l.id);try{m=await(async()=>{const t=this.rootfs.root,e=`${t}/containers/${l.id}/commit`,i=path.join(this.paths.blobs,`commit-${l.id.slice(0,12)}-${Date.now()}.tmp`),{shim:s}=this.host.binaries();await this.host.sh('set -e; rm -rf "$2"; mkdir -p "$1" "$2"; cp -a "$1/." "$2/"; for f in /etc/hosts /etc/hostname /etc/resolv.conf /sbin/tnc-init; do [ -f "$2$f" ] && [ ! -s "$2$f" ] && rm -f "$2$f"; done; true',[`${t}/containers/${l.id}/upper`,e]);try{const t=(await this.host.sh(c,[e,this.host.guestPath(i),s,"vm"===this.host.kind?1:0])).stdout.trim().split("\n").pop();if("EMPTY"===t)return null;const[n,r,o]=t.split(" ");return fs.renameSync(i,path.join(this.paths.blobs,n)),{digest:`sha256:${n}`,diffId:`sha256:${r}`,size:Number(o)}}finally{await this.host.sh('rm -rf "$1"',[e]).catch(()=>{})}})()}finally{d&&await this.unpause(l.id).catch(()=>{})}const f=u.config||{},g=h({...f.config||{},...stripEmpty(a)},o),p=(new Date).toISOString(),w={...f,created:p,...n?{author:n}:{},...s?{comment:s}:{},config:g,rootfs:{type:"layers",diff_ids:[...f.rootfs?.diff_ids||[],...m?[m.diffId]:[]]},history:[...f.history||[],{created:p,created_by:`thinknsh commit ${l.name}`,...s?{comment:s}:{},...m?{}:{empty_layer:!0}}]},y=JSON.stringify(w),v=`sha256:${crypto.createHash("sha256").update(y).digest("hex")}`;fs.writeFileSync(path.join(this.paths.blobs,v.slice(7)),y);const C=[...u.layers||[],...m?[{digest:m.digest,size:m.size,mediaType:"application/vnd.oci.image.layer.v1.tar+gzip"}]:[]],S=e?[parseReference(`${e}:${i||"latest"}`).familiar]:[];this.store.saveImage({id:v,repoTags:S,repoDigests:[],digest:null,size:C.reduce((t,e)=>t+e.size,0),layers:C,config:w,architecture:w.architecture,os:"linux",createdAt:Date.now()}),this.emitEvent("container","commit",l.id,this._attrs(l,{comment:s}));for(const t of S)this.emitEvent("image","tag",v,{name:t});return{Id:v}}async importImage(t,{repo:e,tag:i,changes:s=[],message:n="",platform:r}={}){const{applyChanges:o}=require("./build/changes"),a=parsePlatform(r),h=await archive.importRootfs(t,{blobsDir:this.paths.blobs,arch:a?.arch||this.linuxArch(),config:o({Env:["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]},s),comment:n}),c=e?[parseReference(i?`${e}:${i}`:e).familiar]:[],l=this.store.saveImage({...h,repoTags:c});return this.emitEvent("image","import",l.id,c.length?{name:c[0]}:{}),l}saveSelection(t){if(!t.length)throw httpError(400,"no images given to save");const e=new Map;for(const i of t){const t=this.requireImage(i);let s=[],n=null;try{n=parseReference(i).familiar}catch(t){}n&&t.repoTags.includes(n)?s=[n]:/^(sha256:)?[0-9a-f]{12,64}$/.test(i)||(s=t.repoTags.filter(t=>n&&t.startsWith(n.replace(/:latest$/,"")+":")));const r=e.get(t.id)||{image:t,tags:[]};r.tags=[...new Set([...r.tags,...s])],e.set(t.id,r)}return[...e.values()]}async saveImages(t,e){const i=this.saveSelection(t);await archive.saveImages(i,{blobsDir:this.paths.blobs,sink:e});for(const{image:t}of i)this.emitEvent("image","save",t.id,{})}async loadImages(t,e){const i=path.join(this.paths.home,"build",`load-${crypto.randomBytes(6).toString("hex")}`);fs.mkdirSync(i,{recursive:!0});try{await archive.extractTo(t,i);const s=archive.readArchive(i,{blobsDir:this.paths.blobs,arch:this.linuxArch()});if(!s.length)throw httpError(400,"the archive holds no images");for(const t of s){const i=this.store.saveImage(t);if(this.emitEvent("image","load",i.id,{}),t.repoTags.length)for(const i of t.repoTags)e({stream:`Loaded image: ${i}\n`});else e({stream:`Loaded image ID: ${i.id}\n`})}}finally{fs.rmSync(i,{recursive:!0,force:!0})}}imageHistory(t){return archive.imageHistory(this.requireImage(t),t=>t.repoTags)}async _onExit(t,e,i=""){if(!this.exiting.has(t.id)){this.exiting.add(t.id);try{this.ports&&this.ports.stop(t.id),this.fsevents.untrack(t.id),this._stopHealth(t.id),this._host&&(await this.network.detachAll(t.id,t.runtime.networks),await this.host.isUp().catch(()=>!1)&&await this.rootfs.unmount(t.id).catch(()=>{}));const s="removing"===t.state;if(t=this.store.updateContainer(t.id,{state:s?"removing":"exited",runtime:{...t.runtime,exitCode:e,error:i,finishedAt:Date.now(),pid:0,ip:null,ports:{},networks:{},paused:!1}}),this._refreshHosts(Object.keys(t.config.networks?Object.fromEntries(t.config.networks.map(t=>[t.name,1])):{})),this.emitEvent("container","die",t.id,this._attrs(t,{exitCode:String(e)})),this._resolveWaiters(t.id,"exit",e),s)return;if(t.config.HostConfig.AutoRemove)return void await this._destroy(t,!0);this._maybeRestart(t,e)}finally{this.exiting.delete(t.id)}}}_maybeRestart(t,e){const i=t.config.HostConfig.RestartPolicy||{},s=i.Name||"no";if("no"===s||t.runtime.manualStop)return;if("on-failure"===s){if(0===e)return;if(i.MaximumRetryCount>0&&t.runtime.restartCount>=i.MaximumRetryCount)return}const n=Math.min(100*2**(t.runtime.restartCount||0),6e4);this.store.updateContainer(t.id,{runtime:{...t.runtime,restartCount:(t.runtime.restartCount||0)+1}}),setTimeout(()=>{const e=this.store.getContainer(t.id);e&&"exited"===e.state&&!e.runtime.manualStop&&this.start(t.id).catch(e=>this.log(`restart of ${t.name} failed: ${e.message}`))},n).unref()}_ensureReconciler(){this.timer||(this.timer=setInterval(()=>this.reconcile().catch(t=>this.log(`reconcile: ${t.message}`)),300),this.timer.unref())}async reconcile(){const t=this.store.listContainers().filter(t=>"running"===t.state||"removing"===t.state&&t.runtime.pid);if(0===t.length)return clearInterval(this.timer),void(this.timer=null);for(const e of t)if(fs.existsSync(this.exitFile(e.id))){const t=parseInt(fs.readFileSync(this.exitFile(e.id),"utf8"),10);await this._onExit(e,Number.isInteger(t)?t:255)}if(Date.now()-this.lastLiveness<3e3)return;this.lastLiveness=Date.now();const e=await this.host.isUp().catch(()=>!1);let i=null;if(e){const t=await this.host.run([this.host.binaries().crun,"list","-q"]);0===t.code&&(i=new Set(t.stdout.split("\n").map(t=>t.trim()).filter(Boolean)))}for(const s of t){const t=this.store.getContainer(s.id);t&&"running"===t.state&&!fs.existsSync(this.exitFile(s.id))&&(!e||i&&!i.has(s.id)&&Date.now()-(t.runtime.startedAt||0)>2e3)&&await this._onExit(t,255,"container runtime lost (the Linux host or MicroVM restarted)")}}async recover(){const t=this.store.listContainers().filter(t=>"running"===t.state);if(0!==t.length){this.host;for(const e of t)if(e.runtime.ip&&Object.keys(e.config.HostConfig.PortBindings||{}).length)try{await this.ports.start(e.id,e.config.HostConfig.PortBindings,e.runtime.ip)}catch(t){this.log(`could not restore ports for ${e.name}: ${t.message}`)}for(const e of t)this._startHealth(e);for(const e of t)this.fsevents.track(e.id,e.runtime.apiMounts);this.lastLiveness=0,this._ensureReconciler()}}touch(){this.lastActivity=Date.now()}async track(t){this.streams=(this.streams||0)+1;try{return await t()}finally{this.streams-=1,this.touch()}}startResourceManager(){if(!["darwin","win32"].includes(process.platform)||this.resourceTimer)return;const t=6e4*Number(process.env.TNC_DOCK_IDLE_MINUTES??5);this.lastActivity=Date.now(),this.resourceTimer=setInterval(()=>this._manageResources(t).catch(t=>this.log(`resources: ${t.message}`)),1e4),this.resourceTimer.unref()}async _manageResources(t){if(this._host&&["vm","wsl"].includes(this.host.kind)&&!this._managing){this._managing=!0;try{if(!await this.host.isUp())return;if(!(this.store.listContainers().some(t=>"running"===t.state)||this.streams>0||this.execs.size>0&&[...this.execs.values()].some(t=>t.Running))&&t>0&&Date.now()-this.lastActivity>t)return this.log("idle: stopping the MicroVM (no containers, no activity)"),this.emitEvent("daemon","vm-sleep","microvm",{}),void await this.host.stop()}finally{this._managing=!1}}}shutdown(){this.resourceTimer&&clearInterval(this.resourceTimer),this.resourceTimer=null;for(const t of[...this.healthTimers.keys()])this._stopHealth(t);this.timer&&clearInterval(this.timer),this.timer=null,this.ports&&this.ports.stopAll(),this.fsevents.stopAll(),this.hostfwd.stop()}_healthConfig(t){const e={...this.store.getImage(t.image)?.config?.config?.Healthcheck||{},...t.config.Config.Healthcheck||{}};if(!e.Test||!e.Test.length||"NONE"===e.Test[0])return null;const i=(t,e)=>t>0?t/1e6:e;return{argv:"CMD-SHELL"===e.Test[0]?["/bin/sh","-c",e.Test.slice(1).join(" ")]:"CMD"===e.Test[0]?e.Test.slice(1):e.Test,interval:i(e.Interval,3e4),timeout:i(e.Timeout,3e4),startPeriod:i(e.StartPeriod,0),startInterval:i(e.StartInterval,5e3),retries:e.Retries>0?e.Retries:3}}_startHealth(t){const e=this._healthConfig(t);if(!e)return;const i=Date.now(),s={Status:"starting",FailingStreak:0,Log:[]};this._saveHealth(t.id,s);const n=async()=>{const r=this.store.getContainer(t.id);if(!r||"running"!==r.state)return;if(r.runtime.paused)return void this.healthTimers.set(t.id,setTimeout(n,e.interval));const o=Date.now(),{crun:a}=this.host.binaries(),h=this.host.spawn([a,"exec",t.id,...e.argv]);let c="";h.stdout.on("data",t=>{c.length<4096&&(c+=t)}),h.stderr.on("data",t=>{c.length<4096&&(c+=t)}),h.end();const l=setTimeout(()=>h.kill("SIGKILL"),e.timeout);let u=await h.exited;clearTimeout(l),Date.now()-o>=e.timeout&&(u=-1,c=`Health check exceeded timeout (${e.timeout}ms)`);const d=this.store.getContainer(t.id)?.runtime.health||s,m=Date.now()-i<e.startPeriod,f=d.Status;if(0===u?(d.Status="healthy",d.FailingStreak=0):m&&"healthy"!==d.Status||(d.FailingStreak+=1,d.FailingStreak>=e.retries&&(d.Status="unhealthy")),d.Log=[...d.Log,{Start:iso(o),End:iso(Date.now()),ExitCode:u,Output:c}].slice(-5),!this._saveHealth(t.id,d))return;d.Status!==f&&this.emitEvent("container",`health_status: ${d.Status}`,t.id,this._attrs(t));const g=Date.now()-i<e.startPeriod?e.startInterval:e.interval;this.healthTimers.set(t.id,setTimeout(n,g))};this.healthTimers.set(t.id,setTimeout(n,e.startPeriod>0?e.startInterval:e.interval))}_saveHealth(t,e){const i=this.store.getContainer(t);return!(!i||"running"!==i.state||(this.store.updateContainer(t,{runtime:{...i.runtime,health:e}}),0))}_stopHealth(t){clearTimeout(this.healthTimers.get(t)),this.healthTimers.delete(t)}createExec(t,e={}){const i=this.findContainer(t);if("running"!==i.state)throw httpError(409,`Container ${i.id} is not running`);if(i.runtime.paused)throw httpError(409,`Container ${i.id} is paused, unpause the container before exec`);if(!Array.isArray(e.Cmd)||0===e.Cmd.length)throw httpError(400,"No exec command specified");const s=crypto.randomBytes(32).toString("hex");return this.log(`exec create ${i.name}: ${JSON.stringify(redactExecBody(e))}`),this.execs.set(s,{ID:s,containerId:i.id,config:e,Running:!1,ExitCode:null,Pid:0}),this.emitEvent("container",`exec_create: ${e.Cmd.join(" ")}`,i.id,this._attrs(i,{execID:s})),{Id:s}}getExec(t){const e=this.execs.get(t);if(!e)throw httpError(404,`No such exec instance: ${t}`);return e}async startExec(t){const e=this.getExec(t);if(e.Running||null!==e.ExitCode)throw httpError(409,`exec ${t} has already run`);const i=this.findContainer(e.containerId);if("running"!==i.state)throw httpError(409,`Container ${i.id} is not running`);await this.ready();const{crun:s,shim:n}=this.host.binaries();let r=[s,"exec"];if(e.config.Tty){const i=`${this.rootfs.root}/exec-${t.slice(0,16)}.sock`;e.ctl=`${this.rootfs.root}/exec-${t.slice(0,16)}.ctl`,r=[n,"relay","--console",i,"--ctl",e.ctl,"--",s,"exec","--tty","--console-socket",i]}e.config.WorkingDir&&r.push("--cwd",e.config.WorkingDir);const o=this.store.getImage(i.image)?.config?.config?.Env||[],a=this._hostname(i);for(const t of mergeEnv(o,[`HOSTNAME=${a}`,...i.config.Config.Env||[],...e.config.Env||[]]))r.push("--env",t);if(e.config.User){const t=e.config.User.split(":").some(t=>!/^\d+$/.test(t))?resolveUser(e.config.User,await this.rootfs.readFile(i.id,"/etc/passwd"),await this.rootfs.readFile(i.id,"/etc/group")):resolveUser(e.config.User);r.push("--user",`${t.uid}:${t.gid}`)}r.push(i.id,...e.config.Cmd),e.Running=!0,this.emitEvent("container",`exec_start: ${e.config.Cmd.join(" ")}`,i.id,this._attrs(i,{execID:t}));const h=this.host.spawn(r);return h.exited.then(s=>{e.Running=!1,e.ExitCode=s,this.emitEvent("container","exec_die",i.id,this._attrs(i,{execID:t,exitCode:String(s)}))}),h}execInspect(t){const e=this.getExec(t);return{ID:e.ID,Running:e.Running,ExitCode:e.ExitCode,ProcessConfig:{tty:!!e.config.Tty,entrypoint:e.config.Cmd[0],arguments:e.config.Cmd.slice(1),privileged:!1,user:e.config.User||""},OpenStdin:!!e.config.AttachStdin,OpenStderr:!!e.config.AttachStderr,OpenStdout:!!e.config.AttachStdout,CanRemove:!1,ContainerID:e.containerId,DetachKeys:"",Pid:e.Pid}}async probe(t,e){const i=this.findContainer(t);if("running"!==i.state)return!1;const s=Object.values(i.runtime.networks||{}).find(t=>t.ip),n=s?.ip||(i.runtime.networks?.host?"127.0.0.1":null);return!!(n&&e>0)&&("function"==typeof this.host.probe?this.host.probe(n,e):new Promise(t=>{const i=this.host.connect(n,e),s=e=>{clearTimeout(r),i.destroy(),t(e)},r=setTimeout(()=>s(!1),1500);i.once("ready",()=>s(!0)),i.once("connect",()=>s(!0)),i.once("error",()=>s(!1))}))}async _ctl(t,e){await this.host.run(["timeout","2","sh","-c",'printf "%s\\n" "$1" > "$2"',"dock",e,t]).catch(()=>{})}async resize(t,e,i){const s=this.findContainer(t);if("running"!==s.state)throw httpError(409,`Container ${s.id} is not running`);s.config.Config.Tty&&e>0&&i>0&&await this._ctl(this.rootfs.ctlFifo(s.id),`resize ${e} ${i}`)}async resizeExec(t,e,i){const s=this.getExec(t);s.ctl&&s.Running&&e>0&&i>0&&await this._ctl(s.ctl,`resize ${e} ${i}`)}openStdin(t){const e=this.host.spawn(["sh","-c",'exec cat > "$1"',"dock",this.rootfs.stdinFifo(t.id)]);return e.stdout.resume(),e.stderr.resume(),{write:t=>e.write(t),end:async()=>{e.end(),await e.exited,t.config.Config.StdinOnce&&await this._ctl(this.rootfs.ctlFifo(t.id),"eof")}}}_status(t){const e=t.runtime;if("running"===t.state){if(e.paused)return`Up ${humanDuration(Date.now()-e.startedAt)} (Paused)`;const t=e.health?` (${"starting"===e.health.Status?"health: starting":e.health.Status})`:"";return`Up ${humanDuration(Date.now()-e.startedAt)}${t}`}return"created"===t.state?"Created":"removing"===t.state?"Removal In Progress":`Exited (${e.exitCode}) ${humanDuration(Date.now()-(e.finishedAt||Date.now()))} ago`}_portsList(t){const e=[],i=t.runtime.ports||{},s=this.store.getImage(t.image)?.config?.config?.ExposedPorts||{};for(const n of Object.keys({...s,...t.config.Config.ExposedPorts||{},...i})){const[t,s="tcp"]=n.split("/"),r=i[n];if(r?.length)for(const i of r)e.push({IP:i.HostIp,PrivatePort:Number(t),PublicPort:Number(i.HostPort),Type:s});else e.push({PrivatePort:Number(t),Type:s})}return e}_networks(t){const e={};for(const i of t.config.networks||[{name:"bridge",aliases:[]}]){const s=t.runtime.networks?.[i.name]||{};let n=null;try{n=this.network.resolve(i.name)}catch(t){}e[i.name]={IPAMConfig:null,Links:null,Aliases:i.aliases?.length?i.aliases:null,NetworkID:n?.id||i.name,EndpointID:s.ip?`${t.id.slice(0,32)}${(n?.id||"").slice(0,32)}`:"",Gateway:s.gateway||"",IPAddress:s.ip||"",IPPrefixLen:s.ip?s.prefixLen:0,IPv6Gateway:"",GlobalIPv6Address:"",GlobalIPv6PrefixLen:0,MacAddress:s.mac||"",DNSNames:s.ip&&n?.dns?[...new Set([t.name,...i.aliases||[],t.id.slice(0,12)])]:null}}return e}async connectNetwork(t,e,i={}){this.host;const s=this.network.resolve(t);let n=this.findContainer(e);const r=n.config.networks||[];if(r.some(t=>t.name===s.name))throw httpError(403,`endpoint with name ${n.name} already exists in network ${s.name}`);if(["host","null"].includes(s.driver)||r.some(t=>["host","none"].includes(t.name)))throw httpError(400,"container cannot be connected to multiple networks with one of the networks in host or none mode");const o={name:s.name,aliases:i.Aliases||[]},a=[...r,o];if("running"===n.state){await this.ready();const t=Object.values(n.runtime.networks||{}).map(t=>t.idx??-1),e=Math.max(-1,...t)+1,i=await this.network.attachOne(n.id,s,e,o.aliases);n=this.store.updateContainer(n.id,{runtime:{...n.runtime,networks:{...n.runtime.networks,[s.name]:i}}})}this.store.db.prepare("UPDATE containers SET config = ? WHERE id = ?").run(JSON.stringify({...n.config,networks:a}),n.id),this.emitEvent("network","connect",s.id,{container:n.id,name:s.name,type:s.driver}),this._refreshHosts([s.name])}async disconnectNetwork(t,e){this.host;const i=this.network.resolve(t);let s=this.findContainer(e);const n=(s.config.networks||[]).filter(t=>t.name!==i.name);if(n.length===(s.config.networks||[]).length)throw httpError(404,`container ${s.id} is not connected to network ${i.name}`);const r=s.runtime.networks?.[i.name];if("running"===s.state&&r){await this.network.detachOne(s.id,r);const t={...s.runtime.networks};delete t[i.name],s=this.store.updateContainer(s.id,{runtime:{...s.runtime,networks:t}})}if(this.store.db.prepare("UPDATE containers SET config = ? WHERE id = ?").run(JSON.stringify({...s.config,networks:n}),s.id),this.emitEvent("network","disconnect",i.id,{container:s.id,name:i.name,type:i.driver}),this._refreshHosts([i.name]),"running"===s.state)try{fs.writeFileSync(path.join(this.containerDir(s.id),"hosts"),this._hostsContent(s))}catch(t){}}async removeNetwork(t){this.host;const e=this.network.resolve(t);if(this.store.listContainers().filter(t=>(t.config.networks||[]).some(t=>t.name===e.name)).length)throw httpError(403,`error while removing network: network ${e.name} id ${e.id} has active endpoints`);await this.network.remove(e),this.emitEvent("network","destroy",e.id,{name:e.name,type:e.driver})}networkInspect(t){const e={};for(const i of this.store.listContainers()){const s=i.runtime.networks?.[t.name];"running"===i.state&&s&&(e[i.id]={Name:i.name,EndpointID:i.id,MacAddress:s.mac||"",IPv4Address:s.ip?`${s.ip}/${s.prefixLen}`:"",IPv6Address:""})}return{Name:t.name,Id:t.id,Created:t.createdAt?iso(t.createdAt):"0001-01-01T00:00:00Z",Scope:"local",Driver:t.driver,EnableIPv6:!1,IPAM:{Driver:"default",Options:null,Config:t.subnet?[{Subnet:t.subnet,Gateway:t.gateway}]:[]},Internal:!!t.internal,Attachable:!1,Ingress:!1,ConfigFrom:{Network:""},ConfigOnly:!1,Containers:e,Options:t.options||{},Labels:t.labels||{}}}containerSummary(t){const e=processArgs(t.config.Config,this.store.getImage(t.image)?.config?.config||{});return{Id:t.id,Names:[`/${t.name}`],Image:t.config.imageRef,ImageID:t.image,Command:e.join(" "),Created:Math.floor(t.createdAt/1e3),Ports:this._portsList(t),Labels:t.config.Config.Labels||{},State:"removing"===t.state?"removing":t.runtime.paused&&"running"===t.state?"paused":t.state,Status:this._status(t),HostConfig:{NetworkMode:t.config.HostConfig.NetworkMode||"bridge"},NetworkSettings:{Networks:this._networks(t)},Mounts:t.runtime.apiMounts||[]}}listContainers({all:t=!1,filters:e={}}={}){let i=this.store.listContainers();t||e.status||(i=i.filter(t=>"running"===t.state));const s=(t,s)=>{e[t]?.length&&(i=i.filter(i=>e[t].some(t=>s(i,t))))};return s("status",(t,e)=>"paused"===e?"running"===t.state&&!!t.runtime.paused:"running"===e?"running"===t.state&&!t.runtime.paused:t.state===e),s("name",(t,e)=>t.name.includes(e.replace(/^\//,""))),s("id",(t,e)=>t.id.startsWith(e)),e.label?.length&&(i=i.filter(t=>e.label.every(e=>labelMatch(t.config.Config.Labels,e)))),s("ancestor",(t,e)=>{const i=this.findImage(e);return i&&i.id===t.image}),i.map(t=>this.containerSummary(t))}inspect(t){const e=this.findContainer(t),i=e.runtime,s=this.store.getImage(e.image)?.config?.config||{},n=processArgs(e.config.Config,s),r="running"===e.state,o=Object.values(i.networks||{})[0]||{};return{Id:e.id,Created:iso(e.createdAt),Path:n[0]||"",Args:n.slice(1),State:{Status:"removing"===e.state?"removing":r&&i.paused?"paused":e.state,Running:r,Paused:!(!r||!i.paused),Restarting:!1,OOMKilled:!1,Dead:!1,Pid:r&&i.pid||0,ExitCode:i.exitCode||0,Error:i.error||"",StartedAt:iso(i.startedAt),...r&&i.health?{Health:i.health}:{},FinishedAt:iso(i.finishedAt)},Image:e.image,ResolvConfPath:path.join(this.containerDir(e.id),"resolv.conf"),HostnamePath:path.join(this.containerDir(e.id),"hostname"),HostsPath:path.join(this.containerDir(e.id),"hosts"),LogPath:this.logFile(e.id),Name:`/${e.name}`,RestartCount:i.restartCount||0,Driver:"overlayfs",Platform:"linux",MountLabel:"",ProcessLabel:"",AppArmorProfile:"",HostConfig:{NetworkMode:"default",RestartPolicy:{Name:"no",MaximumRetryCount:0},...e.config.HostConfig},GraphDriver:{Name:"overlayfs",Data:null},Mounts:i.apiMounts||[],Config:{Hostname:e.config.Config.Hostname||e.id.slice(0,12),Domainname:"",User:e.config.Config.User||s.User||"",AttachStdin:!!e.config.Config.AttachStdin,AttachStdout:!!e.config.Config.AttachStdout,AttachStderr:!!e.config.Config.AttachStderr,ExposedPorts:{...s.ExposedPorts||{},...e.config.Config.ExposedPorts||{}},Tty:!!e.config.Config.Tty,OpenStdin:!!e.config.Config.OpenStdin,StdinOnce:!!e.config.Config.StdinOnce,Env:mergeEnv(s.Env,e.config.Config.Env||[]),Cmd:e.config.Config.Cmd||s.Cmd||null,Image:e.config.imageRef,Volumes:s.Volumes||null,WorkingDir:e.config.Config.WorkingDir||s.WorkingDir||"",Entrypoint:e.config.Config.Entrypoint||s.Entrypoint||null,Labels:{...s.Labels||{},...e.config.Config.Labels||{}},StopSignal:e.config.Config.StopSignal||s.StopSignal,...s.Healthcheck||e.config.Config.Healthcheck?{Healthcheck:{...s.Healthcheck||{},...e.config.Config.Healthcheck||{}}}:{}},NetworkSettings:{Bridge:"",SandboxID:"",HairpinMode:!1,SandboxKey:i.network?.nsPath||"",Ports:i.ports||{},Gateway:o.gateway||"",IPAddress:o.ip||"",IPPrefixLen:o.ip?o.prefixLen:0,MacAddress:o.mac||"",Networks:this._networks(e)}}}}const COMPRESSED=[[31,139],[66,90,104],[253,55,122,88,90],[40,181,47,253]];function completeTar(t){const e=fs.openSync(t,"r+");try{const t=Buffer.alloc(6);if(fs.readSync(e,t,0,t.length,0),COMPRESSED.some(e=>e.every((e,i)=>t[i]===e)))return;const i=fs.fstatSync(e).size;fs.writeSync(e,Buffer.alloc((512-i%512)%512+1024),0,void 0,i)}finally{fs.closeSync(e)}}function joinedContainer(t){return"string"==typeof t&&t.startsWith("container:")&&t.length>10?t.slice(10):null}function sizeBytes(t){const e=/^(\d+(?:\.\d+)?)\s*([kmg]?)b?$/i.exec(String(t||"").trim());return e?Math.round(parseFloat(e[1])*{"":1,k:1024,m:1048576,g:1024**3}[e[2].toLowerCase()]):0}function logRotationArgs(t={}){const e=t?.Type||"json-file";if("json-file"!==e&&"local"!==e)throw httpError(400,`log driver "${e}" is not supported; use json-file`);const i=t?.Config||{},s=sizeBytes(i["max-size"]||process.env.TNC_DOCK_LOG_MAX_SIZE||"20m"),n=parseInt(i["max-file"]||process.env.TNC_DOCK_LOG_MAX_FILE||"5",10);return s>0?["--log-max-bytes",String(s),"--log-max-files",String(Math.max(1,n))]:[]}function stripEmpty(t={}){return Object.fromEntries(Object.entries(t||{}).filter(([,t])=>!(null==t||""===t||Array.isArray(t)&&!t.length||"object"==typeof t&&!Array.isArray(t)&&!Object.keys(t).length)))}function labelMatch(t={},e){const i=e.indexOf("=");return-1===i?Object.prototype.hasOwnProperty.call(t||{},e):(t||{})[e.slice(0,i)]===e.slice(i+1)}function humanDuration(t){const e=Math.round(t/1e3);if(e<1)return"Less than a second";if(1===e)return"1 second";if(e<60)return`${e} seconds`;const i=Math.round(e/60);if(1===i)return"About a minute";if(i<60)return`${i} minutes`;const s=Math.round(e/3600);if(1===s)return"About an hour";if(s<48)return`${s} hours`;const n=Math.round(s/24);return n<14?`${n} days`:n<60?`${Math.round(n/7)} weeks`:n<730?`${Math.round(n/30)} months`:`${Math.round(n/365)} years`}module.exports={Engine,humanDuration,labelMatch,redactExecBody,completeTar};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),path=require("path"),zlib=require("zlib"),crypto=require("crypto"),{Transform}=require("stream"),{entryHeader,pad,END,TarReader}=require("./tar"),{parseReference}=require("./reference"),MEDIA={INDEX:"application/vnd.oci.image.index.v1+json",MANIFEST:"application/vnd.oci.image.manifest.v1+json",CONFIG:"application/vnd.oci.image.config.v1+json",LAYER_GZIP:"application/vnd.oci.image.layer.v1.tar+gzip",LAYER:"application/vnd.oci.image.layer.v1.tar"};function httpError(e,t){const s=new Error(t);return s.statusCode=e,s}const hex=e=>e.replace(/^sha256:/,""),sha256=e=>crypto.createHash("sha256").update(e).digest("hex");function isGzip(e){const t=fs.openSync(e,"r"),s=Buffer.alloc(2);return fs.readSync(t,s,0,2,0),fs.closeSync(t),31===s[0]&&139===s[1]}function sha256File(e){const t=crypto.createHash("sha256"),s=fs.openSync(e,"r"),r=Buffer.alloc(1<<20);let n;for(;(n=fs.readSync(s,r,0,r.length,null))>0;)t.update(r.subarray(0,n));return fs.closeSync(s),t.digest("hex")}async function saveImages(e,{blobsDir:t,sink:s}){const r=Date.now()/1e3,n=new Set,i=async e=>{s.write(e)||await s.drained()},a=async(e,t)=>{for(const s of entryHeader({name:e,mode:420,size:t.length,mtime:r,type:"0"}))await i(s);await i(t),await i(pad(t.length))},o=async e=>{const s=`blobs/sha256/${hex(e)}`;if(n.has(s))return s;n.add(s);const a=path.join(t,hex(e));if(!fs.existsSync(a))throw httpError(404,`blob ${hex(e).slice(0,12)} is missing from the local store; pull the image again`);const o=fs.statSync(a).size;for(const e of entryHeader({name:s,mode:420,size:o,mtime:r,type:"0"}))await i(e);const c=fs.openSync(a,"r");try{const e=Buffer.alloc(1<<20);let t;for(;(t=fs.readSync(c,e,0,e.length,null))>0;)await i(Buffer.from(e.subarray(0,t)))}finally{fs.closeSync(c)}return await i(pad(o)),s};for(const e of["blobs/","blobs/sha256/"])for(const t of entryHeader({name:e,mode:493,size:0,mtime:r,type:"5"}))await i(t);const c={schemaVersion:2,mediaType:MEDIA.INDEX,manifests:[]},f=[];for(const{image:s,tags:r}of e){const e=await o(s.id),i=[];for(const e of s.layers||[]){if(/zstd/.test(e.mediaType||""))throw httpError(501,"saving images with zstd layers is not supported");i.push(await o(e.digest))}const l=Buffer.from(JSON.stringify({schemaVersion:2,mediaType:MEDIA.MANIFEST,config:{mediaType:MEDIA.CONFIG,digest:s.id,size:fs.statSync(path.join(t,hex(s.id))).size},layers:(s.layers||[]).map(e=>({mediaType:/gzip/.test(e.mediaType||"")?MEDIA.LAYER_GZIP:MEDIA.LAYER,digest:e.digest,size:fs.statSync(path.join(t,hex(e.digest))).size}))})),p=`sha256:${sha256(l)}`,h=`blobs/sha256/${hex(p)}`;n.has(h)||(n.add(h),await a(h,l));const d=r.length?r:[null];for(const e of d){const t={};if(e){const s=parseReference(e);t["io.containerd.image.name"]=`${s.domain}/${s.path}:${s.tag}`,t["org.opencontainers.image.ref.name"]=s.tag}c.manifests.push({mediaType:MEDIA.MANIFEST,digest:p,size:l.length,...e?{annotations:t}:{}})}f.push({Config:e,RepoTags:r.length?r:null,Layers:i})}await a("oci-layout",Buffer.from(JSON.stringify({imageLayoutVersion:"1.0.0"}))),await a("index.json",Buffer.from(JSON.stringify(c))),await a("manifest.json",Buffer.from(JSON.stringify(f))),await i(END)}function safeRel(e){const t=path.posix.normalize(e.replace(/^\.\/+/,"").replace(/\/+$/,""));return!t||"."===t||t.startsWith("../")||".."===t||path.posix.isAbsolute(t)?null:t}async function extractTo(e,t){const s=[],r=new TarReader(e=>{const r=safeRel(e.name);if(!r)return null;const n=path.join(t,r);if("5"===e.type)return fs.mkdirSync(n,{recursive:!0}),null;if("2"===e.type||"1"===e.type)return s.push({rel:r,target:e.linkname,hard:"1"===e.type}),null;if("0"!==e.type&&"7"!==e.type)return null;fs.mkdirSync(path.dirname(n),{recursive:!0});const i=fs.openSync(n,"w");return{write:e=>fs.writeSync(i,e),end:()=>fs.closeSync(i)}});let n=!0,i=null;const a=new Promise((e,t)=>{r.resolve=e,r.reject=t}),o=e=>r.push(e);for await(const t of e)n&&(n=!1,31===t[0]&&139===t[1]&&(i=zlib.createGunzip(),i.on("data",o),i.on("error",e=>r.reject(httpError(400,`invalid gzip data: ${e.message}`))),i.on("end",()=>r.resolve()))),i?i.write(t)||await new Promise(e=>i.once("drain",e)):o(t);i&&(i.end(),await a),r.end();for(const e of s){const s=safeRel(e.hard?e.target:path.posix.join(path.posix.dirname(e.rel),e.target));if(!s)continue;const r=path.join(t,s);fs.existsSync(r)&&fs.statSync(r).isFile()&&(fs.mkdirSync(path.dirname(path.join(t,e.rel)),{recursive:!0}),fs.copyFileSync(r,path.join(t,e.rel)))}}function readJson(e,t){try{return JSON.parse(fs.readFileSync(e,"utf8"))}catch(e){throw httpError(400,`invalid ${t} in the image archive`)}}function storeLayer(e,t){const s=`sha256:${sha256File(e)}`,r=fs.statSync(e).size,n=isGzip(e)?MEDIA.LAYER_GZIP:MEDIA.LAYER,i=path.join(t,hex(s));return fs.existsSync(i)||fs.copyFileSync(e,i),{digest:s,size:r,mediaType:n}}function imageRecord(e,t,s,r){const n=JSON.parse(e.toString("utf8")),i=`sha256:${sha256(e)}`;fs.writeFileSync(path.join(r,hex(i)),e);const a=n.rootfs?.diff_ids||[];if(a.length!==t.length)throw httpError(400,`image ${i.slice(7,19)}: config lists ${a.length} layers, archive has ${t.length}`);return{id:i,repoTags:s,repoDigests:[],digest:null,size:t.reduce((e,t)=>e+t.size,0),layers:t,config:n,architecture:n.architecture,os:n.os,createdAt:Date.parse(n.created)||Date.now()}}const familiar=e=>{try{return parseReference(e).familiar}catch(e){return null}};function readArchive(e,{blobsDir:t,arch:s}){const r=[];if(fs.existsSync(path.join(e,"manifest.json"))){for(const s of readJson(path.join(e,"manifest.json"),"manifest.json")){const n=safeRel(s.Config||"");if(!n||!fs.existsSync(path.join(e,n)))throw httpError(400,`image archive is missing ${s.Config}`);const i=(s.Layers||[]).map(s=>{const r=safeRel(s);if(!r||!fs.existsSync(path.join(e,r)))throw httpError(400,`image archive is missing layer ${s}`);return storeLayer(path.join(e,r),t)});r.push(imageRecord(fs.readFileSync(path.join(e,n)),i,(s.RepoTags||[]).map(familiar).filter(Boolean),t))}return r}if(!fs.existsSync(path.join(e,"index.json")))throw httpError(400,"not an image archive: it has neither manifest.json nor index.json");const n=t=>{const s=path.join(e,"blobs","sha256",hex(t));if(!/^[0-9a-f]{64}$/.test(hex(t))||!fs.existsSync(s))throw httpError(400,`image archive is missing blob ${t}`);return s},i=(e,a)=>{const o=readJson(n(e.digest),"manifest");if(o.manifests){const e=o.manifests.filter(e=>!e.platform||"linux"===e.platform.os&&e.platform.architecture===s),t=e.length?e:o.manifests.filter(e=>"unknown"!==e.platform?.architecture).slice(0,1);for(const e of t.slice(0,1))i(e,a);return}const c=(o.layers||[]).map(e=>{if(/zstd/.test(e.mediaType||""))throw httpError(501,"images with zstd layers are not supported");return storeLayer(n(e.digest),t)});r.push(imageRecord(fs.readFileSync(n(o.config.digest)),c,a,t))},a=readJson(path.join(e,"index.json"),"index.json"),o=new Map;for(const e of a.manifests||[]){const t=e.annotations?.["io.containerd.image.name"]||e.annotations?.["org.opencontainers.image.ref.name"],s=t&&t.includes(":")||t?.includes("/")?familiar(t):null;o.has(e.digest)||o.set(e.digest,{desc:e,tags:[]}),s&&o.get(e.digest).tags.push(s)}for(const{desc:e,tags:t}of o.values())i(e,t);return r}function hasher(){const e=crypto.createHash("sha256"),t=new Transform({transform(t,s,r){e.update(t),r(null,t)}});return t.digest=()=>e.digest("hex"),t}async function importRootfs(e,{blobsDir:t,arch:s,config:r={},comment:n=""}){const i=path.join(t,`import-${crypto.randomBytes(6).toString("hex")}.tmp`),a=fs.createWriteStream(i),o=hasher(),c=hasher(),f=zlib.createGzip();o.pipe(f).pipe(c).pipe(a);const l=new Promise((e,t)=>{a.on("finish",e),a.on("error",t),f.on("error",t)});let p=!0,h=null;for await(const t of e){p&&(p=!1,31===t[0]&&139===t[1]&&(h=zlib.createGunzip(),h.pipe(o)));const e=h||o;e.write(t)||await new Promise(t=>e.once("drain",t))}(h||o).end(),await l;const d=`sha256:${c.digest()}`,m=fs.statSync(i).size;fs.renameSync(i,path.join(t,hex(d)));const u={architecture:s,os:"linux",created:(new Date).toISOString(),config:{Env:["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],...r},rootfs:{type:"layers",diff_ids:[`sha256:${o.digest()}`]},history:[{created:(new Date).toISOString(),created_by:"thinknsh import",...n?{comment:n}:{}}]};return imageRecord(Buffer.from(JSON.stringify(u)),[{digest:d,size:m,mediaType:MEDIA.LAYER_GZIP}],[],t)}function imageHistory(e,t){const s=e.config?.history||[],r=[...e.layers||[]];let n=0;const i=s.map(e=>{const t=e.empty_layer?null:r[n++];return{Created:Math.floor((Date.parse(e.created)||0)/1e3),CreatedBy:e.created_by||"",Size:t?t.size:0,Comment:e.comment||""}});return i.reverse(),i.map((s,r)=>({Id:0===r?e.id:"<missing>",...s,Tags:0===r?t(e):null}))}module.exports={saveImages,extractTo,readArchive,importRootfs,imageHistory,MEDIA,safeRel};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),path=require("path"),crypto=require("crypto"),{DEFAULT_REGISTRY,DEFAULT_REGISTRY_HOST,DEFAULT_REGISTRY_API}=require("../api/wire"),DEFAULT_ALIASES=new Set([DEFAULT_REGISTRY_HOST,DEFAULT_REGISTRY_API,new URL(DEFAULT_REGISTRY).host]);function normalizeRegistry(e){let r=String(e||"").trim().toLowerCase();if(!r)return DEFAULT_REGISTRY_HOST;if(r=r.replace(/^[a-z][a-z0-9+.-]*:\/\//,"").split("/")[0],!r||/[\s@]/.test(r)){const r=new Error(`invalid registry address "${e}"`);throw r.statusCode=400,r}return DEFAULT_ALIASES.has(r)?DEFAULT_REGISTRY_HOST:r}class AuthStore{constructor(e){this.file=e}_read(){try{const e=JSON.parse(fs.readFileSync(this.file,"utf8"));return e&&"object"==typeof e?e:{}}catch(e){if("ENOENT"===e.code)return{};throw new Error(`cannot read ${this.file}: ${e.code||"invalid JSON"}`)}}_write(e){fs.mkdirSync(path.dirname(this.file),{recursive:!0,mode:448});const r=`${this.file}.${crypto.randomBytes(6).toString("hex")}.tmp`;fs.writeFileSync(r,JSON.stringify(e,null,2)+"\n",{mode:384}),fs.chmodSync(r,384),fs.renameSync(r,this.file)}get(e){const r=this._read()[normalizeRegistry(e)];return r&&r.username&&"string"==typeof r.password?{username:r.username,password:r.password}:null}set(e,{username:r,password:t}){const s=this._read();s[normalizeRegistry(e)]={username:r,password:t},this._write(s)}remove(e){const r=this._read(),t=normalizeRegistry(e);return t in r&&(delete r[t],this._write(r),!0)}registries(){return Object.keys(this._read())}}module.exports={AuthStore,normalizeRegistry};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const{DEFAULT_REGISTRY_HOST,DEFAULT_REGISTRY_API,INDEX_HOST}=require("../api/wire"),DIGEST=/^sha256:[0-9a-f]{64}$/,NAME_COMPONENT=/^[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*$/,TAG=/^[\w][\w.-]{0,127}$/;function invalid(e,t){const i=new Error(`invalid reference format: "${e}" (${t})`);return i.statusCode=400,i}function parseReference(e){if(!e||"string"!=typeof e)throw invalid(String(e),"empty");let t=e,i=null;const n=t.indexOf("@");if(-1!==n&&(i=t.slice(n+1),t=t.slice(0,n),!DIGEST.test(i)))throw invalid(e,"bad digest");let s=null;const a=t.lastIndexOf(":");if(a>t.lastIndexOf("/")&&(s=t.slice(a+1),t=t.slice(0,a),!TAG.test(s)))throw invalid(e,"bad tag");const r=t.split("/");let l=DEFAULT_REGISTRY_HOST;if(r.length>1&&(r[0].includes(".")||r[0].includes(":")||"localhost"===r[0])&&(l=r.shift()),l===INDEX_HOST&&(l=DEFAULT_REGISTRY_HOST),l===DEFAULT_REGISTRY_HOST&&1===r.length&&r.unshift("library"),0===r.length||!r.every(e=>NAME_COMPONENT.test(e)))throw invalid(e,"bad repository name");s||i||(s="latest");const T=r.join("/"),E=l===DEFAULT_REGISTRY_HOST?T.startsWith("library/")?T.slice(8):T:`${l}/${T}`;return{domain:l,path:T,tag:s,digest:i,apiHost:l===DEFAULT_REGISTRY_HOST?DEFAULT_REGISTRY_API:l,familiarName:E,familiar:i?`${E}@${i}`:`${E}:${s}`,tagged:s?`${E}:${s}`:null}}module.exports={parseReference};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),path=require("path"),crypto=require("crypto"),{Readable}=require("stream"),{MANIFEST_LIST_V2,MANIFEST_V2,DEFAULT_REGISTRY_HOST,DEFAULT_REGISTRY_API}=require("../api/wire"),MEDIA={OCI_INDEX:"application/vnd.oci.image.index.v1+json",OCI_MANIFEST:"application/vnd.oci.image.manifest.v1+json",OCI_CONFIG:"application/vnd.oci.image.config.v1+json",OCI_LAYER:"application/vnd.oci.image.layer.v1.tar",OCI_LAYER_GZIP:"application/vnd.oci.image.layer.v1.tar+gzip",OCI_LAYER_ZSTD:"application/vnd.oci.image.layer.v1.tar+zstd",V2_LIST:MANIFEST_LIST_V2,V2_MANIFEST:MANIFEST_V2},ACCEPT=[MEDIA.OCI_INDEX,MEDIA.V2_LIST,MEDIA.OCI_MANIFEST,MEDIA.V2_MANIFEST].join(", ");function httpError(t,e){const s=new Error(e);return s.statusCode=t,s}function parseAuthenticate(t){const e=/^\s*(Bearer|Basic)\b\s*(.*)$/i.exec(t||"");if(!e)return null;const s={scheme:e[1].toLowerCase()};for(const[,t,a,r]of e[2].matchAll(/([\w-]+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))/g))s[t.toLowerCase()]=void 0!==a?a.replace(/\\(.)/g,"$1"):r;return"bearer"!==s.scheme||s.realm?s:null}function parseChallenge(t){const e=parseAuthenticate(t);return e&&"bearer"===e.scheme?e:null}function registryScheme(t,e=process.env){const s=t.replace(/:\d+$/,"").replace(/^\[|\]$/g,"");if("localhost"===s||s.endsWith(".localhost")||"::1"===s||/^127\./.test(s))return"http";const a=(e.TNC_DOCK_INSECURE_REGISTRIES||"").split(",").map(t=>t.trim().toLowerCase()).filter(Boolean);return a.includes(t.toLowerCase())||a.includes(s.toLowerCase())?"http":"https"}const basic=t=>"Basic "+Buffer.from(`${t.username}:${t.password}`).toString("base64");class RegistryClient{constructor(t,{userAgent:e="tnc-dock",credentials:s=null,actions:a=["pull"],mountFrom:r=[]}={}){this.ref=t,this.origin=`${registryScheme(t.apiHost)}://${t.apiHost}`,this.base=`${this.origin}/v2/${t.path}`,this.userAgent=e,this.credentials=s,this.scopes=t.path?[`repository:${t.path}:${a.join(",")}`,...r.map(t=>`repository:${t}:pull`)]:[],this.authorization=null,this.token=null}get name(){return this.ref.familiarName||this.ref.apiHost}async _authenticate(t){if("basic"===t.scheme){if(!this.credentials)throw httpError(401,`${this.ref.apiHost} requires a login: run thinknsh login ${this.ref.domain||this.ref.apiHost}`);return void(this.authorization=basic(this.credentials))}const e=new URL(t.realm);t.service&&e.searchParams.set("service",t.service);const s=new Set(this.scopes);for(const e of(t.scope||"").split(" ").filter(Boolean))s.add(e);for(const t of s)e.searchParams.append("scope",t);const a={"User-Agent":this.userAgent};this.credentials&&(a.Authorization=basic(this.credentials));const r=await fetch(e,{headers:a});if(401===r.status||403===r.status)throw await(r.body?.cancel()),httpError(401,this.credentials?`unauthorized: incorrect username or password for ${this.ref.apiHost}`:`${this.name} needs a login (HTTP ${r.status}): run thinknsh login ${this.ref.domain||this.ref.apiHost}`);if(!r.ok)throw await(r.body?.cancel()),httpError(502,`registry auth failed for ${this.name}: HTTP ${r.status}`);const i=await r.json().catch(()=>({}));if(this.token=i.token||i.access_token,!this.token)throw httpError(502,`registry auth for ${this.name} returned no token`);this.authorization=`Bearer ${this.token}`}async request(t,e,{headers:s={},body:a,retried:r=!1}={}){const i={method:t,headers:{"User-Agent":this.userAgent,...s,...this.authorization?{Authorization:this.authorization}:{}},redirect:"follow"};void 0!==a&&(i.body="function"==typeof a?a():a,i.body&&"function"==typeof i.body[Symbol.asyncIterator]&&(i.duplex="half"));const o=await fetch(e,i);if(401===o.status&&!r){const r=parseAuthenticate(o.headers.get("www-authenticate"));return await(o.body?.cancel()),r?(await this._authenticate(r),this.request(t,e,{headers:s,body:a,retried:!0})):o}return o}async _get(t,e={}){const s=()=>httpError(404,`pull access denied for ${this.name}, repository does not exist or may require authentication`+(this.credentials?"":`: run thinknsh login ${this.ref.domain}`));let a;try{a=await this.request("GET",t,{headers:e})}catch(t){if(401===t.statusCode&&!this.credentials)throw s();throw t}if(401===a.status||403===a.status||404===a.status)throw await(a.body?.cancel()),s();if(!a.ok)throw await(a.body?.cancel()),httpError(502,`registry error for ${this.name}: HTTP ${a.status}`);return a}async manifest(t){const e=await this._get(`${this.base}/manifests/${t}`,{Accept:ACCEPT}),s=Buffer.from(await e.arrayBuffer()),a="sha256:"+crypto.createHash("sha256").update(s).digest("hex");if(t.startsWith("sha256:")&&a!==t)throw httpError(502,`manifest digest mismatch for ${this.ref.familiarName}: expected ${t}, got ${a}`);const r=JSON.parse(s.toString("utf8"));return{body:r,digest:a,mediaType:r.mediaType||e.headers.get("content-type")}}async blob(t,e,s,a){if(fs.existsSync(e)&&fs.statSync(e).size===s)return!1;const r=await this._get(`${this.base}/blobs/${t}`),i=`${e}.${crypto.randomBytes(6).toString("hex")}.part`,o=crypto.createHash("sha256"),n=fs.createWriteStream(i);let c=0,h=0;try{for await(const t of Readable.fromWeb(r.body))o.update(t),c+=t.length,n.write(t)||await new Promise(t=>n.once("drain",t)),a&&Date.now()-h>100&&(h=Date.now(),a(c,s));await new Promise((t,e)=>n.end(s=>s?e(s):t()));const l="sha256:"+o.digest("hex");if(l!==t)throw httpError(502,`blob digest mismatch: expected ${t}, got ${l}`);return fs.renameSync(i,e),a?.(c,s),!0}catch(t){throw n.destroy(),fs.rmSync(i,{force:!0}),t}}_pushDenied(t){return httpError(404===t.status?404:403,`denied: requested access to ${this.name} is denied (HTTP ${t.status})`+(this.credentials?"":`; log in first: thinknsh login ${this.ref.domain}`))}async _expect(t,e,s){if(e.includes(t.status))return t;let a="";try{const e=await t.json();a=e?.errors?.[0]?.message?`: ${e.errors[0].message}`:""}catch(t){}if(401===t.status||403===t.status)throw this._pushDenied(t);throw httpError(502,`${s} failed for ${this.name}: HTTP ${t.status}${a}`)}async hasBlob(t){const e=await this.request("HEAD",`${this.base}/blobs/${t}`);return await(e.body?.cancel()),200===e.status||(404===e.status||await this._expect(e,[200,404],"blob check"),!1)}async mountBlob(t,e){const s=await this.request("POST",`${this.base}/blobs/uploads/?mount=${encodeURIComponent(t)}&from=${encodeURIComponent(e)}`);return await(s.body?.cancel()),201===s.status||(202===s.status&&s.headers.get("location")?new URL(s.headers.get("location"),this.origin):null)}async startUpload(){const t=await this._expect(await this.request("POST",`${this.base}/blobs/uploads/`),[202],"upload start");await(t.body?.cancel());const e=t.headers.get("location");if(!e)throw httpError(502,`registry ${this.ref.apiHost} gave no upload location`);return new URL(e,this.origin)}async uploadBlob(t,e,s,a,r){const i=new URL(t);i.searchParams.set("digest",e);const o=await this.request("PUT",i,{headers:{"Content-Type":"application/octet-stream","Content-Length":String(a)},body:()=>async function*(){let t=0,e=0;for await(const i of fs.createReadStream(s,{highWaterMark:1<<20}))t+=i.length,r&&Date.now()-e>100&&(e=Date.now(),r(t,a)),yield i}()});await this._expect(o,[201,204],"blob upload"),await(o.body?.cancel()),r?.(a,a)}async putManifest(t,e,s){const a=await this.request("PUT",`${this.base}/manifests/${t}`,{headers:{"Content-Type":s},body:e});await this._expect(a,[200,201],"manifest upload"),await(a.body?.cancel())}async verify(){const t=await this.request("GET",`${this.origin}/v2/`);if(await(t.body?.cancel()),401===t.status||403===t.status)throw httpError(401,`unauthorized: incorrect username or password for ${this.ref.apiHost}`);if(!t.ok)throw httpError(502,`login to ${this.ref.apiHost} failed: HTTP ${t.status}`)}}const shortId=t=>t.replace(/^sha256:/,"").slice(0,12);function pickPlatform(t,e,s,a){const r=(t.manifests||[]).filter(t=>t.platform&&"linux"===t.platform.os&&t.platform.architecture===e&&(a?(t.platform.variant||("arm64"===e?"v8":""))===a:"arm64"!==e||!t.platform.variant||"v8"===t.platform.variant));if(0===r.length){const r=["linux",e,a].filter(Boolean).join("/"),i=(t.manifests||[]).filter(t=>t.platform&&"unknown"!==t.platform.architecture).map(t=>[t.platform.os,t.platform.architecture,t.platform.variant].filter(Boolean).join("/")).join(", ");throw httpError(404,`no matching manifest for ${r} in the manifest list entries for ${s.familiar} (available: ${i})`)}return r[0]}async function pullImage(t,{blobsDir:e,arch:s,variant:a,credentials:r=null,progress:i=()=>{}}){fs.mkdirSync(e,{recursive:!0});const o=new RegistryClient(t,{credentials:r});i({status:`Pulling from ${t.familiarName}`,id:t.tag||t.digest});let n=await o.manifest(t.digest||t.tag);const c=`${t.familiarName}@${n.digest}`;let h=n;if(n.body.manifests){const e=pickPlatform(n.body,s,t,a);h=await o.manifest(e.digest)}if(!h.body.config||!Array.isArray(h.body.layers))throw httpError(502,`unsupported manifest type ${h.mediaType} for ${t.familiar}`);const l=t=>path.join(e,t.replace(/^sha256:/,"")),d=h.body.config;await o.blob(d.digest,l(d.digest),d.size);const u=JSON.parse(fs.readFileSync(l(d.digest),"utf8"));!n.body.manifests&&u.architecture&&s&&u.architecture!==s&&i({status:`Warning: ${t.familiar} is a linux/${u.architecture} image (requested linux/${s})`});const p=h.body.layers.map(t=>({digest:t.digest,size:t.size,mediaType:t.mediaType}));for(const e of p)if(/zstd/.test(e.mediaType))throw httpError(501,`zstd-compressed layers are not supported yet (${t.familiar})`);let f=!1;for(const t of p)i({status:"Pulling fs layer",progressDetail:{},id:shortId(t.digest)});for(const t of p){const e=shortId(t.digest),s=await o.blob(t.digest,l(t.digest),t.size,(t,s)=>i({status:"Downloading",progressDetail:{current:t,total:s},id:e}));s&&(f=!0),i({status:s?"Download complete":"Already exists",progressDetail:{},id:e}),s&&i({status:"Pull complete",progressDetail:{},id:e})}return i({status:`Digest: ${n.digest}`}),{fresh:f,image:{id:d.digest,repoTags:t.tagged?[t.tagged]:[],repoDigests:[c],digest:h.digest,size:p.reduce((t,e)=>t+e.size,0),layers:p,config:u,architecture:u.architecture,os:u.os,createdAt:Date.parse(u.created)||Date.now()}}}function ociLayerType(t=""){return/zstd$/.test(t)?MEDIA.OCI_LAYER_ZSTD:/gzip$/.test(t)?MEDIA.OCI_LAYER_GZIP:/\.tar$|layer\.v1\.tar$/.test(t)?MEDIA.OCI_LAYER:t||MEDIA.OCI_LAYER_GZIP}async function pushImage(t,e,{blobsDir:s,credentials:a=null,sources:r=[],progress:i=()=>{}}){if(!t.tag)throw httpError(400,`push needs a tag: ${t.familiar}`);const o=[...new Set(r)].filter(e=>e&&e!==t.path),n=new RegistryClient(t,{credentials:a,actions:["pull","push"],mountFrom:o}),c=t=>path.join(s,t.replace(/^sha256:/,"")),h=t=>{try{return fs.statSync(c(t)).size}catch(t){return-1}};i({status:`The push refers to repository [${t.domain}/${t.path}]`});const l=h(e.id);if(l<0)throw httpError(404,`image config ${shortId(e.id)} is missing from the local store; rebuild or pull ${t.familiarName}`);const d=(e.layers||[]).map(t=>({mediaType:ociLayerType(t.mediaType),digest:t.digest,size:t.size}));for(const t of d){const e=h(t.digest);if(e<0)throw httpError(404,`layer ${shortId(t.digest)} is missing from the local store; pull or rebuild the image`);t.size=e}const u=async(t,e,s)=>{if(await n.hasBlob(t))return void(s&&i({status:"Layer already exists",progressDetail:{},id:s}));let a=null;for(const e of o){const r=await n.mountBlob(t,e);if(!0===r)return void(s&&i({status:`Mounted from ${e}`,progressDetail:{},id:s}));if(r){a=r;break}}a=a||await n.startUpload(),await n.uploadBlob(a,t,c(t),e,s?(t,e)=>i({status:"Pushing",progressDetail:{current:t,total:e},id:s}):null),s&&i({status:"Pushed",progressDetail:{},id:s})},p=[...d].reverse();for(const t of p)i({status:"Preparing",progressDetail:{},id:shortId(t.digest)});for(const t of p)await u(t.digest,t.size,shortId(t.digest));await u(e.id,l,null);const f={schemaVersion:2,mediaType:MEDIA.OCI_MANIFEST,config:{mediaType:MEDIA.OCI_CONFIG,digest:e.id,size:l},layers:d},g=Buffer.from(JSON.stringify(f)),m="sha256:"+crypto.createHash("sha256").update(g).digest("hex");return await n.putManifest(t.tag,g,MEDIA.OCI_MANIFEST),i({status:`${t.tag}: digest: ${m} size: ${g.length}`}),i({aux:{Tag:t.tag,Digest:m,Size:g.length}}),{digest:m,size:g.length}}async function verifyLogin(t,e){const s=t===DEFAULT_REGISTRY_HOST?DEFAULT_REGISTRY_API:t;await new RegistryClient({apiHost:s,domain:t,path:"",familiarName:t},{credentials:e}).verify()}module.exports={pullImage,pushImage,verifyLogin,RegistryClient,MEDIA,shortId,parseAuthenticate,parseChallenge,registryScheme,ociLayerType};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";function octal(t,e){return t.toString(8).padStart(e-1,"0")+"\0"}function header({name:t,mode:e,size:i,mtime:n,type:r,linkname:a="",uid:s=0,gid:h=0}){const u=Buffer.alloc(512);u.write(t,0,100,"utf8"),u.write(octal(4095&e,8),100),u.write(octal(s,8),108),u.write(octal(h,8),116),u.write(octal(i,12),124),u.write(octal(Math.floor(n),12),136),u.write(" ",148),u.write(r,156),u.write(a,157,100,"utf8"),u.write("ustar\0",257),u.write("00",263);let f=0;for(const t of u)f+=t;return u.write(octal(f,7)+" ",148),u}function pad(t){return Buffer.alloc((512-t%512)%512)}function paxRecord(t,e){const i=` ${t}=${e}\n`,n=Buffer.byteLength(i);let r=n+1;for(;String(r).length+n!==r;)r=String(r).length+n;return`${r}${i}`}function entryHeader(t){const e=[];if(Buffer.byteLength(t.name)>100||Buffer.byteLength(t.linkname||"")>100){const i=Buffer.from(paxRecord("path",t.name)+(t.linkname?paxRecord("linkpath",t.linkname):""));e.push(header({name:"PaxHeader",mode:420,size:i.length,mtime:t.mtime,type:"x"}),i,pad(i.length))}return e.push(header({...t,name:t.name.slice(0,100),linkname:(t.linkname||"").slice(0,100)})),e}function entry(t,e,i){t.push(...entryHeader(e)),i&&t.push(i,pad(i.length))}const END=Buffer.alloc(1024);function readString(t,e,i){const n=t.subarray(e,e+i),r=n.indexOf(0);return n.subarray(0,-1===r?n.length:r).toString("utf8")}function readOctal(t,e,i){if(128&t[e]){let n=0;for(let r=e+1;r<e+i;r++)n=256*n+t[r];return n}const n=readString(t,e,i).trim();return n?parseInt(n,8):0}function parsePax(t){const e={};let i=0;for(;i<t.length;){const n=t.indexOf(32,i);if(-1===n)break;const r=parseInt(t.subarray(i,n).toString("utf8"),10);if(!r)break;const a=t.subarray(n+1,i+r-1).toString("utf8"),s=a.indexOf("=");-1!==s&&(e[a.slice(0,s)]=a.slice(s+1)),i+=r}return e}class TarReader{constructor(t){this.onEntry=t,this.buf=Buffer.alloc(0),this.state="header",this.remaining=0,this.padding=0,this.sink=null,this.meta={},this.metaKind=null,this.metaBufs=[],this.done=!1}push(t){for(this.buf=this.buf.length?Buffer.concat([this.buf,t]):t;;){if(this.done)return void(this.buf=Buffer.alloc(0));if("header"===this.state){if(this.buf.length<512)return;const t=this.buf.subarray(0,512);if(this.buf=this.buf.subarray(512),t.every(t=>0===t)){this.done=!0;continue}this._header(t)}else if("data"===this.state){if(this.remaining>0){if(!this.buf.length)return;const t=Math.min(this.remaining,this.buf.length),e=this.buf.subarray(0,t);if(this.buf=this.buf.subarray(t),this.remaining-=t,this.metaKind?this.metaBufs.push(e):this.sink&&this.sink.write(e),this.remaining>0)return}if(this.buf.length<this.padding)return;this.buf=this.buf.subarray(this.padding),this._finishEntry(),this.state="header"}}}_header(t){let e=readString(t,0,100);const i=readString(t,345,155);i&&readString(t,257,6).startsWith("ustar")&&(e=`${i}/${e}`);const n=String.fromCharCode(t[156]||48);let r=readOctal(t,124,12);const a={name:e,type:"\0"===n?"0":n,size:r,mode:readOctal(t,100,8),linkname:readString(t,157,100),mtime:readOctal(t,136,12)};if(this.remaining=r,this.padding=(512-r%512)%512,this.state="data",["x","L","K","g"].includes(a.type))return this.metaKind=a.type,void(this.metaBufs=[]);this.meta.path&&(a.name=this.meta.path),this.meta.linkpath&&(a.linkname=this.meta.linkpath),this.meta.size&&(a.size=Number(this.meta.size),this.remaining=a.size,this.padding=(512-a.size%512)%512),this.meta={},this.metaKind=null,this.sink=this.onEntry(a)||null}_finishEntry(){if(this.metaKind){const t=Buffer.concat(this.metaBufs);return"x"===this.metaKind?Object.assign(this.meta,parsePax(t)):"L"===this.metaKind?this.meta.path=readString(t,0,t.length):"K"===this.metaKind&&(this.meta.linkpath=readString(t,0,t.length)),this.metaKind=null,void(this.metaBufs=[])}this.sink&&this.sink.end(),this.sink=null}end(){if("data"===this.state&&this.remaining>0)throw new Error("unexpected end of tar archive")}}module.exports={header,pad,paxRecord,entry,entryHeader,END,TarReader};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";module.exports={...require("./version"),...require("./paths"),...require("./daemon"),client:require("./client"),Store:require("./state/store")};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),os=require("os"),net=require("net"),path=require("path"),{spawn:cpSpawn}=require("child_process"),{PassThrough}=require("stream"),{EventEmitter}=require("events"),FRAME={STDIN:0,STDOUT:1,STDERR:2,EXIT:3,SIGNAL:4},SIGNALS={SIGHUP:1,SIGINT:2,SIGQUIT:3,SIGKILL:9,SIGUSR1:10,SIGUSR2:12,SIGTERM:15};function collect(t){return new Promise(s=>{const e=[],n=[];t.stdout.on("data",t=>e.push(t)),t.stderr.on("data",t=>n.push(t)),t.exited.then(t=>s({code:t,stdout:Buffer.concat(e).toString("utf8"),stderr:Buffer.concat(n).toString("utf8")}))})}class LinuxHostBase{async run(t,s={}){const e=this.spawn(t,s),n=collect(e);return void 0!==s.input&&e.write(s.input),e.end(),n}async check(t,s={}){const e=await this.run(t,s);if(0!==e.code){const s="/bin/sh"===t[0]?"Linux step":`${t[0]} ${t.slice(1,3).join(" ")}`,n=new Error(`${s} failed (exit ${e.code}): ${(e.stderr||e.stdout).trim().slice(0,500)||"no output"}`);throw n.result=e,n}return e}sh(t,s=[],e={}){return this.check(["/bin/sh","-c",t,"dock",...s.map(String)],e)}}class LocalLinuxHost extends LinuxHostBase{constructor(){super(),this.kind="local"}async ready(){if(process.getuid&&0!==process.getuid())throw new Error("dock on Linux must run as root (it manages namespaces, mounts and iptables)")}guestPath(t){return t}async isUp(){return!0}binaries(){const t="arm64"===process.arch?"aarch64":"x86_64",s=path.join(os.homedir(),".tnc","bin",`crun-linux-${t}`),e=process.env.TNC_BUSYBOX||path.join(os.homedir(),".tnc","bin",`tnc-busybox-linux-${t}`);return{crun:process.env.TNC_CRUN||(fs.existsSync(s)?s:"crun"),shim:process.env.TNC_SHIM||path.join(os.homedir(),".tnc","bin",`tnc-shim-linux-${t}`),busybox:fs.existsSync(e)?e:null}}spawn(t,{env:s,cwd:e}={}){const n=cpSpawn(t[0],t.slice(1),{cwd:e||"/",env:s?Object.fromEntries(s.map(t=>{const s=t.indexOf("=");return[t.slice(0,s),t.slice(s+1)]})):process.env,stdio:["pipe","pipe","pipe"]}),r=new Promise(t=>{n.on("error",()=>t(127)),n.on("close",(s,e)=>t(s??128+(os.constants.signals[e]||0)))});return n.stdin.on("error",()=>{}),{stdout:n.stdout,stderr:n.stderr,write:t=>n.stdin.write(t),end:()=>n.stdin.end(),kill:(t="SIGTERM")=>{try{n.kill(t)}catch(t){}},exited:r}}connect(t,s){return net.createConnection({host:t,port:s})}}const TNC_DIR=path.join(os.homedir(),".tnc"),VM_SOCK=path.join(TNC_DIR,"vmctl.sock"),TNC_VZ=path.join(TNC_DIR,"bin","tnc-vz"),VZ_CTL=path.join(TNC_DIR,"vzctl.sock"),DATA_DISK=path.join(TNC_DIR,"dock","vm-disk.img");function vmRequest(t,s=5e3){return new Promise((e,n)=>{const r=net.createConnection(VM_SOCK);let o=Buffer.alloc(0);const i=setTimeout(()=>{r.destroy(),n(new Error(`MicroVM did not answer "${t.cmd}"`))},s),a=t=>{o=Buffer.concat([o,t]);const s=o.indexOf(10);if(-1===s)return;let c;clearTimeout(i),r.removeListener("data",a),r.pause();try{c=JSON.parse(o.subarray(0,s).toString("utf8"))}catch(t){return r.destroy(),n(new Error(`bad MicroVM reply: ${o.subarray(0,s)}`))}e({sock:r,reply:c,rest:o.subarray(s+1)})};r.on("connect",()=>r.write(JSON.stringify(t)+"\n")),r.on("data",a),r.on("error",t=>{clearTimeout(i),n(new Error(`MicroVM unreachable: ${t.message}`))})})}const b64argv=t=>Buffer.from(t.map(t=>t+"\0").join(""),"utf8").toString("base64");function vmShares(t=[]){return[{host:"/private/tmp",also:["/tmp"],guest:"/tmp_host"},{host:"/private/var/folders",also:["/var/folders"],guest:"/mnt/tnc/varfolders",tag:"tnc_varfolders"},{host:"/Volumes",also:[],guest:"/mnt/tnc/volumes",tag:"tnc_volumes"},...t.map((t,s)=>({host:t,also:[],guest:`/mnt/tnc/share-${s}`,tag:`tnc_share_${s}`}))]}class VmLinuxHost extends LinuxHostBase{constructor({log:t=()=>{},memMB:s,cpus:e}={}){super();const n=require("../config").effective();this.kind="vm",this.log=t,this.memMB=s||n.memoryMB,this.cpus=e||n.cpus,this.diskGB=n.diskGB,this.home=os.homedir(),this.shares=vmShares(n.shares),this.unmounted=new Set,this._sharesReady=!1,this._booting=null}guestPath(t){const s=path.resolve(t);if(s===this.home)return"/workspace";if(s.startsWith(this.home+path.sep))return"/workspace"+s.slice(this.home.length).split(path.sep).join("/");for(const t of this.shares)for(const e of[t.host,...t.also])if(s===e||s.startsWith(e+path.sep)){if(t.tag&&this.unmounted.has(t.tag))break;return t.guest+s.slice(e.length).split(path.sep).join("/")}const e=[this.home,...this.shares.filter(t=>!this.unmounted.has(t.tag)).map(t=>t.host)].join(", "),n=new Error(`${s} is not in a folder shared with the MicroVM (${e}); add one with: thinknsh dock config --share /path`);throw n.statusCode=400,n}async _mountShares(){if(this._sharesReady)return;let t=this.shares.filter(t=>t.tag);for(let s=0;s<15&&t.length;s++){s&&await new Promise(t=>setTimeout(t,200));const e=await this.run(["/bin/sh","-c",'while [ $# -gt 1 ]; do mkdir -p "$2"; mountpoint -q "$2" || mount -t virtiofs "$1" "$2" 2>/dev/null || echo "$1"; shift 2; done',"dock",...t.flatMap(t=>[t.tag,t.guest])]),n=new Set(e.stdout.split("\n").map(t=>t.trim()).filter(Boolean));t=t.filter(t=>n.has(t.tag))}this.unmounted=new Set(t.map(t=>t.tag)),this.unmounted.size&&this.log(`shared folders not available in this MicroVM: ${[...this.unmounted].join(", ")} (an older tnc-vz? run: thinknsh dock setup)`),this._sharesReady=!0}binaries(){return{crun:"/usr/bin/crun",shim:"/usr/bin/tnc-shim",busybox:"/bin/busybox.static"}}balloon(t){return new Promise(s=>{const e=net.createConnection(VZ_CTL,()=>e.write(t?`balloon ${Math.round(t)}\n`:"target\n"));let n="";e.on("data",t=>{n+=t}),e.on("end",()=>{const t=/^ok (\d+)/.exec(n);s(t?Number(t[1]):null)}),e.on("error",()=>s(null)),e.setTimeout(3e3,()=>{e.destroy(),s(null)})})}async stop(){this._sharesReady=!1,await new Promise(t=>{const s=cpSpawn(TNC_VZ,["stop"],{stdio:"ignore"});s.on("close",t),s.on("error",t)})}async _ping(){try{const{sock:t,reply:s}=await vmRequest({cmd:"ping"},1500);return t.destroy(),s}catch(t){return null}}async isUp(){const t=await this._ping();return!(!t||1!==t.dock)}async ready(){const t=await this._ping();if(t&&1===t.dock)return this._mountShares();if(t&&!t.dock)throw new Error("The running MicroVM has no dock runtime. Run: thinknsh dock setup");return this._sharesReady=!1,this._booting||(this._booting=this._boot().finally(()=>{this._booting=null})),await this._booting,this._mountShares()}async _boot(){if(!fs.existsSync(TNC_VZ))throw new Error("The dock runtime is not installed. Run: thinknsh dock setup");this.log(`booting MicroVM (${this.cpus} CPUs, ${this.memMB} MB)`);const t=fs.openSync(path.join(TNC_DIR,"tnc-vz.log"),"a");fs.mkdirSync(path.dirname(DATA_DISK),{recursive:!0}),cpSpawn(TNC_VZ,["start","--cpus",String(this.cpus),"--mem",String(this.memMB),"--workspace",this.home,"--disk",DATA_DISK,"--disk-size",String(this.diskGB||64),"--shares",this.shares.filter(t=>t.tag).map(t=>`${t.tag}=${t.host}`).join(",")],{detached:!0,stdio:["ignore",t,t]}).unref(),fs.closeSync(t);const s=Date.now()+3e4;for(;Date.now()<s;){const t=await this._ping();if(t){if(!t.dock)throw new Error("MicroVM booted without the dock runtime. Run: thinknsh dock setup");return}await new Promise(t=>setTimeout(t,200))}throw new Error("MicroVM did not become ready within 30s (see ~/.tnc/tnc-vz.log)")}spawn(t,{env:s,cwd:e}={}){const n=new PassThrough,r=new PassThrough,o=new EventEmitter;let i=null,a=null;const c=[],u=(t,s=Buffer.alloc(0))=>{const e=Buffer.alloc(5);e[0]=t,e.writeUInt32BE(s.length,1);const n=Buffer.concat([e,s]);i?i.write(n):c.push(n)},h=new Promise(t=>o.once("exit",t)),d=t=>{null===a&&(a=t,n.end(),r.end(),o.emit("exit",t))},l={cmd:"spawn",argv_b64:b64argv(t),cwd:e||"/"};return s&&(l.env_b64=b64argv(s)),vmRequest(l,1e4).then(({sock:t,reply:s,rest:e})=>{if(!s.ok)return r.write(`${s.error}\n`),t.destroy(),d(127);i=t;for(const t of c.splice(0))i.write(t);let o=e;const u=()=>{for(;o.length>=5;){const t=o.readUInt32BE(1);if(o.length<5+t)return;const s=o[0],e=o.subarray(5,5+t);o=o.subarray(5+t),s===FRAME.STDOUT?n.write(e):s===FRAME.STDERR?r.write(e):s===FRAME.EXIT&&(d(e.readInt32BE(0)),i.end())}};i.on("data",t=>{o=Buffer.concat([o,t]),u()}),i.on("close",()=>d(a??255)),i.on("error",()=>{}),i.resume(),u()}).catch(t=>{r.write(`${t.message}\n`),d(127)}),{stdout:n,stderr:r,write:t=>u(FRAME.STDIN,Buffer.isBuffer(t)?t:Buffer.from(String(t))),end:()=>u(FRAME.STDIN),kill:(t="SIGTERM")=>u(FRAME.SIGNAL,Buffer.from([SIGNALS[t]||15])),exited:h}}connect(t,s){const e=new PassThrough,n=new PassThrough,r=require("stream").Duplex.from({writable:e,readable:n});return vmRequest({cmd:"connect",host:t,port:s},5e3).then(({sock:t,reply:s,rest:o})=>{if(!s.ok)return t.destroy(),void r.destroy(new Error(s.error));o.length&&n.write(o),r.emit("ready"),t.pipe(n),e.pipe(t),t.on("error",t=>r.destroy(t)),r.on("close",()=>t.destroy()),t.resume()}).catch(t=>r.destroy(t)),r}}function windowsToWsl(t){const s=/^([A-Za-z]):[\\/]?(.*)$/.exec(t);if(!s){const s=new Error(`${t} is not a local Windows drive path; dock can only share drive paths (C:\\...) with WSL`);throw s.statusCode=400,s}const e=s[2].split(/[\\/]+/).filter(Boolean).join("/");return`/mnt/${s[1].toLowerCase()}${e?"/"+e:""}`}class WslLinuxHost extends LinuxHostBase{constructor({log:t=()=>{},distro:s=process.env.TNC_WSL_DISTRO||"tnc-dock",wsl:e=process.env.TNC_WSL_EXE||"wsl.exe"}={}){super(),this.kind="wsl",this.log=t,this.distro=s,this.wslCmd=Array.isArray(e)?e:[e],this._readyAt=0}guestPath(t){return windowsToWsl(path.win32.resolve(t))}binaries(){return{crun:"/usr/bin/crun",shim:"/usr/bin/tnc-shim",busybox:"/bin/busybox.static"}}_wslArgs(t,{env:s,cwd:e}={}){return["-d",this.distro,"-u","root","--cd",e||"/","--exec",...s?["/usr/bin/env","-i",...s]:[],...t]}_list(t){return new Promise(s=>{const e=cpSpawn(this.wslCmd[0],[...this.wslCmd.slice(1),"--list","--quiet",...t?["--running"]:[]],{windowsHide:!0}),n=[];e.stdout.on("data",t=>n.push(t)),e.on("error",()=>s(null)),e.on("close",e=>{if(0!==e)return s(t?[]:null);const r=Buffer.concat(n),o=r.includes(0)?r.toString("utf16le"):r.toString("utf8");s(o.split(/\r?\n/).map(t=>t.replace(/\0/g,"").trim()).filter(Boolean))})})}async isUp(){const t=await this._list(!0);return!!t&&t.includes(this.distro)}async ready(){if(Date.now()-this._readyAt<5e3)return;const t=await this._list(!1);if(null===t)throw new Error("WSL is not installed. Run in an admin PowerShell: wsl --install --no-distribution, then: thinknsh dock setup");if(!t.includes(this.distro))throw new Error(`The WSL distro "${this.distro}" is missing. Run: thinknsh dock setup`);const s=await this.run(["/usr/local/bin/tnc-wsl-init"]);if(0!==s.code)throw new Error(`could not start WSL distro ${this.distro}: ${s.stderr||s.stdout}`);this._readyAt=Date.now()}async stop(){this._readyAt=0,await new Promise(t=>{const s=cpSpawn(this.wslCmd[0],[...this.wslCmd.slice(1),"--terminate",this.distro],{stdio:"ignore",windowsHide:!0});s.on("close",t),s.on("error",t)})}spawn(t,s={}){const e=cpSpawn(this.wslCmd[0],[...this.wslCmd.slice(1),...this._wslArgs(t,s)],{stdio:["pipe","pipe","pipe"],windowsHide:!0}),n=new Promise(t=>{e.on("error",()=>t(127)),e.on("close",s=>t(s??255))});return e.stdin.on("error",()=>{}),{stdout:e.stdout,stderr:e.stderr,write:t=>e.stdin.write(t),end:()=>e.stdin.end(),kill:(t="SIGTERM")=>{try{e.kill(t)}catch(t){}},exited:n}}connect(t,s){const e=cpSpawn(this.wslCmd[0],[...this.wslCmd.slice(1),...this._wslArgs(["/usr/bin/tnc-shim","connect",t,String(s)])],{stdio:["pipe","pipe","ignore"],windowsHide:!0}),n=require("stream").Duplex.from({writable:e.stdin,readable:e.stdout});return e.on("error",t=>n.destroy(t)),e.on("close",e=>{e&&n.destroy(Object.assign(new Error(`connect ${t}:${s} failed`),{code:"ECONNREFUSED"}))}),n.on("close",()=>{try{e.kill()}catch(t){}}),n}async probe(t,s){return 0===(await this.run(["/usr/bin/tnc-shim","probe",t,String(s)])).code}}function createLinuxHost(t={}){if("linux"===process.platform)return new LocalLinuxHost(t);if("darwin"===process.platform)return new VmLinuxHost(t);if("win32"===process.platform)return new WslLinuxHost(t);throw new Error(`dock does not support ${process.platform}`)}module.exports={createLinuxHost,LocalLinuxHost,VmLinuxHost,WslLinuxHost,windowsToWsl,vmRequest,vmShares};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const path=require("path"),os=require("os"),crypto=require("crypto");function dockHome(){return process.env.TNC_DOCK_HOME||path.join(os.homedir(),".tnc","dock")}function dockPaths(o=dockHome()){let t;return t="win32"===process.platform?"\\\\.\\pipe\\tnc-dock"+(o===path.join(os.homedir(),".tnc","dock")?"":"-"+crypto.createHash("sha1").update(o).digest("hex").slice(0,8)):path.join(o,"dock.sock"),{home:o,socket:t,db:path.join(o,"state.db"),pidFile:path.join(o,"dock.pid"),logFile:path.join(o,"dock.log"),auth:path.join(o,"auth.json"),blobs:path.join(o,"blobs","sha256"),containers:path.join(o,"containers"),volumes:path.join(o,"volumes")}}module.exports={dockHome,dockPaths};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";function httpError(t,r){const n=new Error(r);return n.statusCode=t,n}const GO_ARCH={x64:"amd64",arm64:"arm64"};function parsePlatform(t){if(!t)return null;const r=String(t).toLowerCase().split("/"),[n,s,e]=1===r.length?["linux",r[0]]:r,i={x86_64:"amd64",x86:"386",aarch64:"arm64",armhf:"arm"}[s]||s;if("linux"!==n)throw httpError(400,`"${t}": only linux images are supported`);if(!i)throw httpError(400,`invalid platform "${t}" (example: linux/amd64)`);return{os:n,arch:i,variant:e}}const platformString=({os:t="linux",arch:r,variant:n})=>[t,r,n].filter(Boolean).join("/"),ROSETTA_RULE=":rosetta:M::\\x7fELF\\x02\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x3e\\x00:\\xff\\xff\\xff\\xff\\xff\\xfe\\xfe\\x00\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xfe\\xff\\xff\\xff:/run/tnc-rosetta/rosetta:CF",ROSETTA='\nset -e\nbf=/proc/sys/fs/binfmt_misc\n[ -e "$bf/register" ] || mount -t binfmt_misc binfmt_misc "$bf" 2>/dev/null || { echo "binfmt_misc is not available in this MicroVM" >&2; exit 4; }\n[ -e "$bf/rosetta" ] && exit 0\nmkdir -p /run/tnc-rosetta\nmountpoint -q /run/tnc-rosetta || mount -t virtiofs rosetta /run/tnc-rosetta 2>/dev/null || exit 3\nprintf \'%s\' "$1" > "$bf/register"\n',QEMU={amd64:"x86_64",arm64:"aarch64",arm:"arm",386:"i386",ppc64le:"ppc64le",s390x:"s390x",riscv64:"riscv64"};class Emulation{constructor(t){this.engine=t,this.ready=new Set}hostArch(){return GO_ARCH[process.arch]||process.arch}async ensure(t){if(!t||t===this.hostArch()||this.ready.has(t))return;const r=this.engine.host;if("vm"===r.kind){if("amd64"!==t||"arm64"!==this.hostArch())throw httpError(400,`linux/${t} images cannot run on this Mac (${this.hostArch()}); only linux/amd64 on Apple Silicon runs through Rosetta 2`);const n=await r.run(["/bin/sh","-c",ROSETTA,"dock",ROSETTA_RULE]);if(3===n.code)throw httpError(400,"linux/amd64 images need Rosetta 2. Install it with: softwareupdate --install-rosetta --agree-to-license, then: thinknsh dock stop && thinknsh dock start");if(0!==n.code)throw httpError(500,`cannot enable Rosetta 2 for linux/amd64: ${(n.stderr||n.stdout).trim()||`exit ${n.code}`}. Update the runtime: thinknsh dock setup`)}else{const n=QEMU[t]||t;if(!(await r.run(["/bin/sh","-c","ls /proc/sys/fs/binfmt_misc/ 2>/dev/null"])).stdout.split(/\s+/).some(r=>r===`qemu-${n}`||"amd64"===t&&"rosetta"===r))throw httpError(400,`linux/${t} images need a binfmt handler for ${n} on this machine; install qemu-user-static (binfmt) first`)}"vm"!==r.kind&&this.ready.add(t)}}module.exports={Emulation,parsePlatform,platformString};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),path=require("path"),FLUSH_MS=80,MAX_BATCH=2e3,SKIP=/(^|[\\/])\.git([\\/]|$)/,RELAY='for p; do if [ -e "$p" ] || [ -L "$p" ]; then touch -c -h -r "$p" "$p" 2>/dev/null; else d="${p%/*}"; [ -d "$d" ] && touch -c -r "$d" "$d" 2>/dev/null; fi; done; true';class FsEventRelay{constructor({engine:t,log:e=()=>{}}){this.engine=t,this.log=e,this.watches=new Map,this.byContainer=new Map,this.pending=new Set,this.lastMtime=new Map,this.timer=null,this.flushing=!1}get enabled(){const t=this.engine._host?.kind;return("vm"===t||"wsl"===t)&&"0"!==process.env.TNC_DOCK_FS_EVENTS}_watchTarget(t){let e;try{e=fs.statSync(t)}catch(t){return null}if(!e.isDirectory()&&!e.isFile())return null;const s=this.engine.paths?.home&&path.resolve(this.engine.paths.home),i=path.resolve(t);return s&&(i===s||i.startsWith(s+path.sep)||s.startsWith(i+path.sep))?null:{path:i,recursive:e.isDirectory()}}track(t,e=[]){if(!this.enabled)return;const s=new Map;for(const t of e){if("bind"!==t.Type||!t.Source)continue;const e=this._watchTarget(t.Source);e&&s.set(e.path,e)}const i=[...s.keys()];for(const e of i){let i=this.watches.get(e);if(!i){const{recursive:t}=s.get(e);try{const s=fs.watch(e,{recursive:t,persistent:!1},(s,i)=>{if(!t||!i)return this._queue(e);const n=String(i);SKIP.test(n)||this._queue(path.join(e,n))});s.on("error",t=>this.log(`fs events for ${e}: ${t.message}`)),i={watcher:s,users:new Set},this.watches.set(e,i)}catch(t){this.log(`fs events: cannot watch ${e}: ${t.message}`);continue}}i.users.add(t)}this.byContainer.set(t,i)}untrack(t){for(const e of this.byContainer.get(t)||[]){const s=this.watches.get(e);s&&(s.users.delete(t),s.users.size||(s.watcher.close(),this.watches.delete(e)))}this.byContainer.delete(t)}_queue(t){this.pending.add(t),this.timer||(this.timer=setTimeout(()=>this._flush(),80))}async _flush(){if(this.timer=null,this.flushing)return void(this.timer=setTimeout(()=>this._flush(),80));const t=[...this.pending];this.pending.clear();let e=[];for(const s of t){let t=null;try{t=fs.lstatSync(s)}catch(t){}if(t){if(this.lastMtime.get(s)===t.mtimeMs)continue;this.lastMtime.set(s,t.mtimeMs)}else this.lastMtime.delete(s);e.push(s)}if(this.lastMtime.size>5e4&&this.lastMtime.clear(),!e.length)return;e.length>2e3&&(e=[...new Set(e.map(t=>path.dirname(t)))].slice(0,2e3));const s=this.engine.host,i=[];for(const t of e)try{i.push(s.guestPath(t))}catch(t){}if(i.length){this.flushing=!0;try{if(!await s.isUp())return;for(let t=0;t<i.length;t+=200)await s.run(["/bin/sh","-c",RELAY,"dock",...i.slice(t,t+200)])}catch(t){this.log(`fs events: ${t.message}`)}finally{this.flushing=!1}}}stopAll(){for(const t of this.watches.values())t.watcher.close();this.watches.clear(),this.byContainer.clear(),clearTimeout(this.timer),this.timer=null}}module.exports={FsEventRelay};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const path=require("path"),{ENV_MARKER}=require("../api/wire");function httpError(t,i){const n=new Error(i);return n.statusCode=t,n}const GO_MODE={dir:2**31,symlink:2**27,device:2**26,pipe:2**25,socket:2**24,setuid:2**23,setgid:2**22,char:2**21,sticky:2**20};function goFileMode(t){const i=parseInt(t,16);let n=511&i;const e=61440&i;return 16384===e?n+=GO_MODE.dir:40960===e?n+=GO_MODE.symlink:24576===e?n+=GO_MODE.device:8192===e?n+=GO_MODE.device+GO_MODE.char:4096===e?n+=GO_MODE.pipe:49152===e&&(n+=GO_MODE.socket),2048&i&&(n+=GO_MODE.setuid),1024&i&&(n+=GO_MODE.setgid),512&i&&(n+=GO_MODE.sticky),{mode:n,isDir:16384===e,isLink:40960===e}}function cleanPath(t){if("string"!=typeof t||!t)throw httpError(400,"path is required");const i=/\/\.$/.test(t)||"."===t;return{abs:path.posix.resolve("/",t),contents:i}}const DIFF_IGNORE=new Set(["/etc/hosts","/etc/hostname","/etc/resolv.conf","/dev","/proc","/sys",ENV_MARKER,"/sbin/tnc-init"]),DIFF='\nupper="$1"; shift\ncd "$upper" || exit 0\nfind . -mindepth 1 | while IFS= read -r f; do\n p="${f#.}"\n if [ -c "$f" ] && [ "$(stat -c %t:%T "$f")" = 0:0 ]; then echo "D $p"; continue; fi\n kind=A\n for l; do if [ -e "$l$p" ] || [ -L "$l$p" ]; then kind=C; break; fi; done\n echo "$kind $p"\ndone\n',WITH_MOUNTS='\nroot="$1"; n="$2"; shift 2\ni=0\nwhile [ "$i" -lt "$n" ]; do\n src="$1"; dst="$2"; mode="$3"; shift 3; i=$((i+1))\n [ -e "$src" ] || continue\n case "$dst" in *..*) continue;; esac\n p="$root"; ok=1; IFS=/\n for part in $dst; do [ -z "$part" ] && continue; p="$p/$part"; if [ -L "$p" ]; then ok=0; break; fi; done\n unset IFS\n [ "$ok" = 1 ] || continue\n t="$root$dst"\n if [ -d "$src" ]; then mkdir -p "$t"; else mkdir -p "${t%/*}"; [ -e "$t" ] || : > "$t"; fi\n mount --bind "$src" "$t" || exit 1\n [ "$mode" = ro ] && mount -o remount,bind,ro "$t"\ndone\nexec "$@"\n';class ContainerFs{constructor(t){this.engine=t,this.busy=new Map,this.mountsFor=new Map}get host(){return this.engine.host}_tools(){const{shim:t,busybox:i}=this.host.binaries();if(!i)throw httpError(501,"this dock runtime has no file tools for cp; update it with: thinknsh dock setup");return{shim:t,busybox:i}}_inroot(t,i,n){const{shim:e,busybox:s}=this._tools(),o=[e,"inroot",t,s,i,...n],r=this.mountsFor.get(t);if(!r||!r.length)return o;const a=r.flatMap(t=>[t.source,t.destination,(t.options||[]).includes("ro")?"ro":"rw"]);return["unshare","-m","--propagation","private","/bin/sh","-c",WITH_MOUNTS,"dock",t,String(r.length),...a,...o]}idle(t){return!this.busy.get(t)}async withRoot(t,i,{volumes:n=!0}={}){const e=this.engine,s=e.findContainer(t);if(await e.ready(),"running"===s.state&&s.runtime.pid>0)return i(`/proc/${s.runtime.pid}/root`,s);const o=e.store.getImage(s.image);if(!o)throw httpError(404,`No such image: ${s.config.imageRef}`);this.busy.set(s.id,(this.busy.get(s.id)||0)+1);try{const t=await e.rootfs.prepare(s.id,o,"/"),r=n?(s.runtime.ociMounts||[]).filter(t=>"bind"===t.type&&t.source&&t.destination):[];r.length&&this.mountsFor.set(t,[...r].sort((t,i)=>t.destination.split("/").length-i.destination.split("/").length));try{return await i(t,s)}finally{(!this.busy.get(s.id)||this.busy.get(s.id)<=1)&&this.mountsFor.delete(t)}}finally{const t=(this.busy.get(s.id)||1)-1;t?this.busy.set(s.id,t):this.busy.delete(s.id);const i=e.store.getContainer(s.id);t||!i||"running"===i.state||e.starting.has(s.id)||await e.rootfs.unmount(s.id).catch(()=>{})}}async _stat(t,i,{follow:n=!1}={}){const e=await this.host.run(this._inroot(t,"stat",[...n?["-L"]:[],"-c","%f %s %Y",i]));if(0!==e.code&&/tnc-shim: open|usage: tnc-shim/.test(e.stderr))throw httpError(501,"this dock runtime is too old for cp; update it with: thinknsh dock setup");if(0!==e.code){if(/No such file|can't stat/i.test(e.stderr))return null;throw httpError(500,`stat ${i}: ${(e.stderr||e.stdout).trim()}`)}const[s,o,r]=e.stdout.trim().split(/\s+/),a=goFileMode(s);let h="";if(a.isLink){const n=await this.host.run(this._inroot(t,"readlink",["-f",i]));h=0===n.code?n.stdout.trim():""}return{name:"/"===i?"/":path.posix.basename(i),size:Number(o),mode:a.mode,isDir:a.isDir,mtime:new Date(1e3*Number(r)).toISOString(),linkTarget:h}}async stat(t,i){const{abs:n}=cleanPath(i);return this.withRoot(t,async e=>{const s=await this._stat(e,n);if(!s)throw httpError(404,`Could not find the file ${i} in container ${t}`);const{isDir:o,...r}=s;return r})}async getArchive(t,i,n,e){const{abs:s,contents:o}=cleanPath(i);return this.withRoot(t,async r=>{const a=await this._stat(r,s);if(!a)throw httpError(404,`Could not find the file ${i} in container ${t}`);const{isDir:h,...c}=a,d=o&&h?["-c","-f","-","-C",s,"."]:["-c","-f","-","-C",path.posix.dirname(s),path.posix.basename(s)||"."];await n(c),await this._pipeOut(this._inroot(r,"tar",d),e)})}async putArchive(t,i,n,{copyUIDGID:e=!1,noOverwriteDirNonDir:s=!1}={}){const{abs:o}=cleanPath(i),r=this.engine.findContainer(t);if(r.config.HostConfig.ReadonlyRootfs)throw httpError(403,"container rootfs is marked read-only");return this.withRoot(t,async a=>{const h=await this._stat(a,o,{follow:!0});if(!h)throw httpError(404,`Could not find the file ${i} in container ${t}`);if(!h.isDir)throw httpError(400,`extraction point is not a directory: ${i}`);const c=["-x","-f","-","-C",o];e||c.push("-o"),s&&c.push("-k");const d=this.host.spawn(this._inroot(a,"tar",c));let u="";d.stdout.resume(),d.stderr.on("data",t=>{u.length<4096&&(u+=t)});for await(const t of n)d.write(t);d.end();const p=await d.exited;if(0!==p)throw httpError(400,`extracting into ${i} failed: ${u.trim()||`exit ${p}`}`);this.engine.emitEvent("container","extract-to-dir",r.id,this.engine._attrs(r))})}async exportTar(t,i){const n=this.engine.findContainer(t);await this.engine.ready();const e=this.engine.store.getImage(n.image);this.busy.set(n.id,(this.busy.get(n.id)||0)+1);try{const t="running"===n.state?this.engine.rootfs.mergedPath(n.id):await this.engine.rootfs.prepare(n.id,e,"/");await this._pipeOut(this._inroot(t,"tar",["-c","-f","-","-C","/","."]),i),this.engine.emitEvent("container","export",n.id,this.engine._attrs(n))}finally{const t=(this.busy.get(n.id)||1)-1;t?this.busy.set(n.id,t):this.busy.delete(n.id);const i=this.engine.store.getContainer(n.id);t||!i||"running"===i.state||this.engine.starting.has(n.id)||await this.engine.rootfs.unmount(n.id).catch(()=>{})}}async changes(t){const i=this.engine.findContainer(t);await this.engine.ready();const n=this.engine.store.getImage(i.image),e=this.engine.rootfs.root,s=[...n?.layers||[]].reverse().map(t=>`${e}/layers/${t.digest.replace(/^sha256:/,"")}`),o=await this.host.run(["/bin/sh","-c",DIFF,"dock",`${e}/containers/${i.id}/upper`,...s]),r={C:0,A:1,D:2};return o.stdout.split("\n").filter(Boolean).map(t=>({Kind:r[t[0]],Path:t.slice(2)})).filter(t=>void 0!==t.Kind&&!DIFF_IGNORE.has(t.Path))}async _pipeOut(t,i){const n=this.host.spawn(t);let e="";n.stderr.on("data",t=>{e.length<4096&&(e+=t)}),n.stdout.on("data",t=>i.write(t)),n.end();const s=await n.exited;0!==s&&this.engine.log(`tar in container: exit ${s}: ${e.trim()}`)}}module.exports={ContainerFs,goFileMode,cleanPath};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),net=require("net"),path=require("path"),os=require("os"),crypto=require("crypto"),HOST_IP="192.168.65.254",RELAY_PORT=2380,GUEST_DIR="/run/tnc-dock",ENGINE_SOCK=`${GUEST_DIR}/engine.sock`,FWD_SOCK=path.join(os.homedir(),".tnc","dock","hostfwd.sock"),GUEST_SETUP='\nset -e\nmkdir -p "$1"; chmod 0700 "$1"\n# a relay left by an earlier daemon holds the port and an old token\npkill -f "[t]nc-shim hostfwd" 2>/dev/null || true # [t]: never matches this script itself\nfor chain in PREROUTING OUTPUT; do\n iptables -t nat -C "$chain" -d "$2" -p tcp -j REDIRECT --to-ports "$3" 2>/dev/null ||\n iptables -t nat -A "$chain" -d "$2" -p tcp -j REDIRECT --to-ports "$3"\ndone\n';class HostForward{constructor({engine:t,socketPath:e,log:s=()=>{}}){this.engine=t,this.socketPath=e,this.log=s,this.token=crypto.randomBytes(24).toString("hex"),this.server=null,this.relay=null,this.starting=null,this.unsupported=!1}get enabled(){const t=this.engine.paths?.home,e=!t||path.resolve(t)===path.dirname(FWD_SOCK);return"vm"===this.engine.host.kind&&!this.unsupported&&e}listen(){if(this.server||"darwin"!==process.platform||!this.enabled)return Promise.resolve();fs.mkdirSync(path.dirname(FWD_SOCK),{recursive:!0,mode:448});try{fs.unlinkSync(FWD_SOCK)}catch(t){}return this.server=net.createServer(t=>this._accept(t)),new Promise(t=>{this.server.once("error",e=>{this.log(`hostfwd: ${e.message}`),this.server=null,t()}),this.server.listen(FWD_SOCK,()=>{fs.chmodSync(FWD_SOCK,384),t()})})}_accept(t){let e=Buffer.alloc(0);t.on("error",()=>{});const s=r=>{e=Buffer.concat([e,r]);const n=e.indexOf(10);if(-1===n)return void(e.length>512&&t.destroy());t.removeListener("data",s),t.pause();const[i,o,h,a]=e.subarray(0,n).toString("utf8").trim().split(" "),l=e.subarray(n+1),c=Buffer.from(this.token),d=Buffer.from(o||"");if("TNC1"!==i||d.length!==c.length||!crypto.timingSafeEqual(d,c))return t.destroy();if("engine"===h)return this._pipe(t,l,[{path:this.socketPath}]);const u=Number(a);if("tcp"===h&&u>0&&u<65536)return this._pipe(t,l,[{host:"127.0.0.1",port:u},{host:"::1",port:u}]);t.destroy()};t.on("data",s)}_pipe(t,e,s){const r=n=>{if(n>=s.length)return t.destroy();const i=net.connect(s[n]);i.once("error",()=>r(n+1)),i.once("connect",()=>{i.removeAllListeners("error"),i.on("error",()=>t.destroy()),t.on("error",()=>i.destroy()),e.length&&i.write(e),t.pipe(i),i.pipe(t),t.resume()})};r(0)}async ensureGuest(){if(this.enabled&&!this.relay)return this.starting||(this.starting=(async()=>{const t=this.engine.host;await t.sh(GUEST_SETUP,[GUEST_DIR,HOST_IP,2380]),await t.run(["/bin/sh","-c",'umask 077; cat > "$1"',"dock",`${GUEST_DIR}/token`],{input:this.token});const{shim:e}=t.binaries(),s=t.spawn([e,"hostfwd","--token-file",`${GUEST_DIR}/token`,"--tcp",String(2380),"--unix",ENGINE_SOCK]);let r="";s.stdout.resume(),s.stderr.on("data",t=>{r.length<2e3&&(r+=t)}),s.end(),this.relay=s,s.exited.then(t=>{this.relay===s&&(this.relay=null),2===t||/usage/.test(r)?(this.unsupported=!0,this.log("hostfwd: this dock runtime has no relay (the host alias needs: thinknsh dock setup)")):t&&255!==t&&this.log(`hostfwd relay ended (exit ${t}): ${r.trim()}`)})})().catch(t=>{this.log(`hostfwd setup: ${t.message}`)}).finally(()=>{this.starting=null})),this.starting}stop(){this.relay&&this.relay.kill("SIGTERM"),this.relay=null,this.server&&this.server.close(),this.server=null}}module.exports={HostForward,HOST_IP,ENGINE_SOCK,FWD_SOCK};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const fs=require("fs"),STREAM_ID={stdin:0,stdout:1,stderr:2};function muxFrame(t,e){const n=Buffer.alloc(8);return n[0]=STREAM_ID[t]??1,n.writeUInt32BE(e.length,4),Buffer.concat([n,e])}function parseLine(t){try{const e=JSON.parse(t);return"string"==typeof e.log?e:null}catch(t){return null}}function timeMs(t){return Date.parse(t)}function readEntries(t,{since:e=0,until:n=1/0,tail:r="all"}={}){let l="";try{l=fs.readFileSync(t,"utf8")}catch(t){return{entries:[],offset:0}}let s="";for(let e=20;e>=1;e--)try{s+=fs.readFileSync(`${t}.${e}`,"utf8")}catch(t){}let o=(s+l).split("\n").filter(Boolean).map(parseLine).filter(Boolean).filter(t=>{const r=timeMs(t.time);return r>=e&&r<=n});const f="all"===r||void 0===r?NaN:parseInt(r,10);return Number.isInteger(f)&&f>=0&&(o=0===f?[]:o.slice(-f)),{entries:o,offset:Buffer.byteLength(l)}}function follow(t,e,n,r,l=100){let s=!1,o="",f=e,i=null,u=null;const c=()=>{try{i=fs.openSync(t,"r"),u=fs.fstatSync(i).ino}catch(t){i=null}return null!==i},a=()=>{if(null!==i){try{fs.closeSync(i)}catch(t){}i=null}};return{done:(async()=>{try{for(;!s;){const e=r();null===i&&c();let s=0;if(null!==i&&(s=fs.fstatSync(i).size,s<f&&(f=0,o="")),s>f){const t=Buffer.alloc(s-f);fs.readSync(i,t,0,t.length,f),f=s;const e=(o+t.toString("utf8")).split("\n");o=e.pop();for(const t of e){const e=t&&parseLine(t);e&&n(e)}continue}let m=null;try{m=fs.statSync(t).ino}catch(t){}if(null===i||null===m||m===u){if(e)return;await new Promise(t=>setTimeout(t,l))}else a(),f=0,o=""}}finally{a()}})(),pin:()=>{null!==i||s||c()},cancel:()=>{s=!0}}}function formatEntry(t,{timestamps:e=!1}={}){return Buffer.from((e?`${t.time} `:"")+t.log,"utf8")}module.exports={muxFrame,readEntries,follow,formatEntry,timeMs};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const crypto=require("crypto"),DEFAULT_BRIDGE={id:"bridge",name:"bridge",driver:"bridge",builtin:!0,dns:!1,bridgeIf:"tnc0",subnet:"172.28.0.0/16",gateway:"172.28.0.1",prefixLen:16,poolStart:"172.28.128.1",poolEnd:"172.28.255.254"},HOST={id:"host",name:"host",driver:"host",builtin:!0},NONE={id:"none",name:"none",driver:"null",builtin:!0},USER_POOL={base:"172.29",first:0,last:255},ipToInt=e=>e.split(".").reduce((e,n)=>(e<<8|Number(n))>>>0,0),intToIp=e=>[e>>>24,e>>>16&255,e>>>8&255,255&e].join(".");function httpError(e,n){const t=new Error(n);return t.statusCode=e,t}const BASE_SETUP="\nmkdir -p /var/run/netns\necho 1 > /proc/sys/net/ipv4/ip_forward\niptables -N TNC-ISOLATION 2>/dev/null || true\niptables -C FORWARD -j TNC-ISOLATION 2>/dev/null || iptables -I FORWARD 1 -j TNC-ISOLATION\niptables -C FORWARD -i tnc+ -j ACCEPT 2>/dev/null || iptables -A FORWARD -i tnc+ -j ACCEPT\niptables -C FORWARD -o tnc+ -j ACCEPT 2>/dev/null || iptables -A FORWARD -o tnc+ -j ACCEPT\n",BRIDGE_SETUP=`\n${BASE_SETUP}\nbr="$1"; gw="$2"; plen="$3"; subnet="$4"; chain="$5"; internal="$6"\nip link show "$br" >/dev/null 2>&1 || ip link add "$br" type bridge\nip -4 addr show dev "$br" | grep -q "$gw/$plen" || ip addr add "$gw/$plen" dev "$br"\nip link set "$br" up\nif [ "$internal" = 1 ]; then\n iptables -t nat -D POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE 2>/dev/null || true\nelse\n iptables -t nat -C POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE\nfi\nif ! iptables -L "$chain" -n >/dev/null 2>&1; then\n iptables -N "$chain"\n iptables -A "$chain" -o "$br" -j RETURN\n iptables -A "$chain" -o tnc+ -j DROP\n if [ "$internal" = 1 ]; then iptables -A "$chain" -j DROP; fi\n iptables -A "$chain" -j RETURN\nfi\niptables -C TNC-ISOLATION -i "$br" -j "$chain" 2>/dev/null || iptables -A TNC-ISOLATION -i "$br" -j "$chain"\n`,BRIDGE_REMOVE='\nbr="$1"; subnet="$2"; chain="$3"\niptables -D TNC-ISOLATION -i "$br" -j "$chain" 2>/dev/null\niptables -F "$chain" 2>/dev/null; iptables -X "$chain" 2>/dev/null\niptables -t nat -D POSTROUTING -s "$subnet" ! -o "$br" -j MASQUERADE 2>/dev/null\nip link del "$br" 2>/dev/null\ntrue\n',ATTACH='\nset -e\nns="$1"; br="$2"; vh="$3"; vp="$4"; ifn="$5"; ip="$6"; plen="$7"; mac="$8"; gw="$9"; route="${10}"\nmkdir -p /var/run/netns\n[ -e "/var/run/netns/$ns" ] || { ip netns add "$ns"; ip -n "$ns" link set lo up; }\nip link del "$vh" 2>/dev/null || true\nip link add "$vh" type veth peer name "$vp"\nip link set "$vh" master "$br"\nip link set "$vh" up\nip link set "$vp" netns "$ns"\nip -n "$ns" link set "$vp" name "$ifn"\nip -n "$ns" link set "$ifn" address "$mac"\nip -n "$ns" addr add "$ip/$plen" dev "$ifn"\n# an address may have belonged to a container that just left: drop the bridge\'s\n# stale neighbour entry and announce the new MAC (gratuitous ARP on link up)\nip neigh flush to "$ip" dev "$br" 2>/dev/null || true\nip netns exec "$ns" sh -c "echo 1 > /proc/sys/net/ipv4/conf/$ifn/arp_notify" 2>/dev/null || true\nip -n "$ns" link set "$ifn" up\nif [ "$route" = 1 ]; then ip -n "$ns" route replace default via "$gw"; fi\n',NETNS_ONLY='set -e; mkdir -p /var/run/netns; [ -e "/var/run/netns/$1" ] || { ip netns add "$1"; ip -n "$1" link set lo up; }',DETACH_ONE='ip -n "$1" link del "$2" 2>/dev/null; ip link del "$3" 2>/dev/null; true',DETACH_ALL='ip netns del "$1" 2>/dev/null; true';class Network{constructor({host:e,store:n}){this.host=e,this.store=n,this.reserved=new Set,this.lastIp=new Map}builtins(){return[DEFAULT_BRIDGE,HOST,NONE]}resolve(e){if(!e||"default"===e||"bridge"===e)return DEFAULT_BRIDGE;if("host"===e)return HOST;if("none"===e)return NONE;const n=this.store.findNetwork(e);if(!n)throw httpError(404,`network ${e} not found`);return{...n,dns:!0,prefixLen:24}}list(){return[...this.builtins(),...this.store.listNetworks().map(e=>({...e,dns:!0,prefixLen:24}))]}create({Name:e,Driver:n,Labels:t,Internal:i=!1,Options:s}){if(n=n||"bridge",t=t||{},s=s||{},!e)throw httpError(400,"network name is required");if(!/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(e))throw httpError(400,`invalid network name "${e}", only [a-zA-Z0-9][a-zA-Z0-9_.-] are allowed`);if(["bridge","host","none","default"].includes(e))throw httpError(403,`${e} is a pre-defined network and cannot be created`);if("bridge"!==n)throw httpError(400,`network driver "${n}" is not supported; only bridge`);if(this.store.findNetwork(e)?.name===e)throw httpError(409,`network with name ${e} already exists`);const r=new Set(this.store.listNetworks().map(e=>e.subnet));let a=null;for(let e=USER_POOL.first;e<=USER_POOL.last;e++){const n=`${USER_POOL.base}.${e}.0/24`;if(!r.has(n)){a=n;break}}if(!a)throw httpError(500,"no free subnets left in 172.29.0.0/16");const o=crypto.randomBytes(32).toString("hex"),l=a.split("/")[0].split(".").slice(0,3).join(".");return this.store.insertNetwork({id:o,name:e,driver:"bridge",subnet:a,gateway:`${l}.1`,poolStart:`${l}.2`,poolEnd:`${l}.254`,bridgeIf:`tncb-${o.slice(0,8)}`,chain:`TNC-ISO-${o.slice(0,8)}`,labels:t,internal:!!i,options:s})}async remove(e){if(e.builtin)throw httpError(403,`${e.name} is a pre-defined network and cannot be removed`);await this.host.isUp().catch(()=>!1)&&await this.host.sh(BRIDGE_REMOVE,[e.bridgeIf,e.subnet,e.chain]),this.store.deleteNetwork(e.id)}_allocate(e,n){const t=new Set(this.reserved);for(const e of this.store.listContainers())if(e.id!==n){for(const n of Object.values(e.runtime.networks||{}))n.ip&&t.add(n.ip);e.runtime.ip&&t.add(e.runtime.ip)}const i=ipToInt(e.poolStart),s=ipToInt(e.poolEnd)-i+1,r=this.lastIp.get(e.id),a=r?(ipToInt(r)-i+1)%s:0;for(let n=0;n<s;n++){const r=intToIp(i+(a+n)%s);if(!t.has(r))return this.reserved.add(r),this.lastIp.set(e.id,r),r}throw httpError(500,`no free addresses left in network ${e.name}`)}names(e,n){return{ns:`tnc-${e.slice(0,12)}`,vh:`vt${e.slice(0,8)}${n}`,vp:`vp${e.slice(0,8)}${n}`}}async _ensureBridge(e){const n=e.builtin?"TNC-ISO-default":e.chain;await this.host.sh(BRIDGE_SETUP,[e.bridgeIf,e.gateway,e.prefixLen,e.subnet,n,e.internal?1:0])}async attachOne(e,n,t,i=[]){const{ns:s,vh:r,vp:a}=this.names(e,t);if("null"===n.driver)return await this.host.sh(NETNS_ONLY,[s]),{networkId:"none",ifname:null,aliases:i};await this._ensureBridge(n);const o=this._allocate(n,e),l="02:42:"+crypto.createHash("sha1").update(`${e}:${n.id}`).digest("hex").slice(0,8).match(/../g).join(":");try{await this.host.sh(ATTACH,[s,n.bridgeIf,r,a,`eth${t}`,o,n.prefixLen,l,n.gateway,0===t?1:0])}catch(e){throw this.reserved.delete(o),e}return{networkId:n.id,ifname:`eth${t}`,idx:t,ip:o,mac:l,gateway:n.gateway,prefixLen:n.prefixLen,aliases:i}}async attachAll(e,n){const t=n.map(e=>({net:this.resolve(e.name),aliases:e.aliases||[]}));if("host"===t[0].net.driver)return{nsPath:null,endpoints:{host:{networkId:"host"}}};const i={};try{for(let n=0;n<t.length;n++)i[t[n].net.name]=await this.attachOne(e,t[n].net,n,t[n].aliases)}catch(n){throw await this.detachAll(e,i),n}return{nsPath:`/var/run/netns/${this.names(e,0).ns}`,endpoints:i}}async detachOne(e,n){if(!n||void 0===n.idx)return;const{ns:t,vh:i}=this.names(e,n.idx);await this.host.sh(DETACH_ONE,[t,n.ifname,i]).catch(()=>{}),n.ip&&this.reserved.delete(n.ip)}async detachAll(e,n={}){if(!n.host){await this.host.sh(DETACH_ALL,[this.names(e,0).ns]).catch(()=>{});for(const e of Object.values(n))e.ip&&this.reserved.delete(e.ip)}}}module.exports={Network,DEFAULT_BRIDGE,ipToInt};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const net=require("net"),dgram=require("dgram"),UDP_IDLE_MS=6e4;function defaultAddress(){try{return require("../config").effective().publishAddress||"127.0.0.1"}catch(t){return"127.0.0.1"}}function parseBindings(t={},o=defaultAddress()){const r=[];for(const[e,s]of Object.entries(t||{})){const[t,n="tcp"]=e.split("/"),c=parseInt(t,10);if(Number.isInteger(c))for(const t of s||[{}]){const s=/^(\d+)-(\d+)$/.exec(String(t.HostPort||"")),i=s?Array.from({length:Number(s[2])-Number(s[1])+1},(t,o)=>Number(s[1])+o):[t.HostPort?parseInt(t.HostPort,10):0];r.push({key:e,containerPort:c,proto:n,hostIp:t.HostIp||o,hostPort:i[0],hostPorts:i})}}return r}function portError(t,o){const r=new Error("EADDRINUSE"===o.code?`driver failed programming external connectivity: Bind for ${t.hostIp}:${t.hostPorts.length>1?`${t.hostPorts[0]}-${t.hostPorts[t.hostPorts.length-1]}`:t.hostPort} failed: port is already allocated`:`cannot publish port ${t.hostPort}: ${o.message}`);return r.statusCode=500,r}class PortProxies{constructor({host:t,log:o=()=>{}}){this.host=t,this.log=o,this.byContainer=new Map}async start(t,o,r){const e=parseBindings(o),s=[],n={};try{for(const t of e){if("tcp"!==t.proto&&"udp"!==t.proto){const o=new Error(`publishing ${t.proto} ports is not supported; use tcp or udp`);throw o.statusCode=400,o}let o=null,e=null;for(const s of t.hostPorts)try{o="udp"===t.proto?await this._listenUdp({...t,hostPort:s},r):await this._listen({...t,hostPort:s},r);break}catch(t){if(e=t,"EADDRINUSE"!==t.code)break}if(!o)throw portError(t,e);s.push(o),(n[t.key]||=[]).push({HostIp:t.hostIp,HostPort:String(o.port)})}}catch(t){for(const t of s)t.close();throw t.statusCode?t:portError(e[0]||{},t)}return this.byContainer.set(t,s),n}_listen(t,o){return new Promise((r,e)=>{const s=new Set,n=net.createServer(r=>{const e=this.host.connect(o,t.containerPort);s.add(r),s.add(e);const n=()=>{r.destroy(),e.destroy(),s.delete(r),s.delete(e)};r.on("error",n),e.on("error",n),r.on("close",n),e.on("close",n),r.pipe(e),e.pipe(r)});n.once("error",e),n.listen(t.hostPort,t.hostIp,()=>r({port:n.address().port,close:()=>{n.close();for(const t of s)t.destroy()}}))})}_listenUdp(t,o){return new Promise((r,e)=>{const s=t.hostIp.includes(":"),n=dgram.createSocket({type:s?"udp6":"udp4",reuseAddr:!1}),c=new Map,i=r=>{const e=`${r.address}:${r.port}`;let s=c.get(e);if(s)return s;const i=t=>n.send(t,r.port,r.address,()=>{});return s="local"===this.host.kind?this._directUdp(o,t.containerPort,i):this._shimUdp(o,t.containerPort,i),s.seen=Date.now(),c.set(e,s),s},a=setInterval(()=>{for(const[t,o]of c)Date.now()-o.seen>6e4&&(o.close(),c.delete(t))},1e4);a.unref(),n.on("message",(t,o)=>{const r=i(o);r.seen=Date.now(),r.send(t)}),n.once("error",t=>{clearInterval(a),e(t)}),n.bind(t.hostPort,t.hostIp,()=>{n.removeAllListeners("error"),n.on("error",o=>this.log(`udp ${t.hostPort}: ${o.message}`)),r({port:n.address().port,close:()=>{clearInterval(a);for(const t of c.values())t.close();c.clear();try{n.close()}catch(t){}}})})})}_directUdp(t,o,r){const e=dgram.createSocket("udp4");e.on("message",r),e.on("error",()=>{}),e.connect(o,t);const s=[];return e.once("connect",()=>{for(const t of s.splice(0))e.send(t)}),{send:t=>{try{e.send(t)}catch(o){s.push(t)}},close:()=>{try{e.close()}catch(t){}}}}_shimUdp(t,o,r){const e=this.host.spawn([this.host.binaries().shim,"udp",t,String(o)]);let s=Buffer.alloc(0);return e.stdout.on("data",t=>{for(s=s.length?Buffer.concat([s,t]):t;s.length>=2;){const t=s.readUInt16BE(0);if(s.length<2+t)break;r(Buffer.from(s.subarray(2,2+t))),s=s.subarray(2+t)}}),e.stderr.resume(),{send:t=>{const o=Buffer.alloc(2);o.writeUInt16BE(Math.min(t.length,65535)),e.write(Buffer.concat([o,t.subarray(0,65535)]))},close:()=>{e.end(),e.kill("SIGTERM")}}}stop(t){for(const o of this.byContainer.get(t)||[])o.close();this.byContainer.delete(t)}stopAll(){for(const t of[...this.byContainer.keys()])this.stop(t)}}module.exports={PortProxies,parseBindings};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license ThinkNCollab Enterprise & Open Core Ecosystem
3
+ * Copyright (c) 2024-2026 ThinkNCollab Systems Inc. All rights reserved.
4
+ * PROPRIETARY INTELLECTUAL PROPERTY & TRADE SECRETS.
5
+ * UNAUTHORIZED REVERSE ENGINEERING, DECOMPILATION, OR RE-DISTRIBUTION IS STRICTLY PROHIBITED.
6
+ */
7
+ "use strict";const IO_CGROUP='\nif [ "$DOCK_IO_CGROUP" = 1 ] && [ -d /sys/fs/cgroup ]; then\n mkdir -p /sys/fs/cgroup/tnc-io 2>/dev/null\n echo +memory > /sys/fs/cgroup/cgroup.subtree_control 2>/dev/null\n echo 96M > /sys/fs/cgroup/tnc-io/memory.high 2>/dev/null\n echo $$ > /sys/fs/cgroup/tnc-io/cgroup.procs 2>/dev/null\nfi\ntrue\n',PREPARE=`\nset -e\nroot="$1"; tmpfs="$2"; shim="$3"; cid="$4"; workdir="$5"; shift 5\nDOCK_IO_CGROUP="$tmpfs"\n${IO_CGROUP}\nmkdir -p "$root"\nif [ "$tmpfs" = 1 ] && ! mountpoint -q "$root"; then mount -t tmpfs -o mode=0711 tnc-dock "$root"; fi\nmkdir -p "$root/layers" "$root/containers"\nlower=""\nwhile [ $# -gt 0 ]; do\n hex="$1"; blob="$2"; comp="$3"; shift 3\n d="$root/layers/$hex"\n if [ ! -f "$d.done" ]; then\n # containers that start together may share a layer: each unpacks into its\n # own folder, and only the first to finish moves it in (under a lock)\n x="$d.tmp.$$"\n rm -rf "$x"; mkdir -p "$x"\n if [ "$comp" = gzip ]; then tar -xzf "$blob" -C "$x"; else tar -xf "$blob" -C "$x"; fi\n find "$x" -name '.wh..wh..opq' -type f | while IFS= read -r f; do\n rm -f "$f"; "$shim" setxattr "$(dirname "$f")" trusted.overlay.opaque y\n done\n find "$x" -name '.wh.*' | while IFS= read -r f; do\n b=$(basename "$f"); t="$(dirname "$f")/\${b#.wh.}"\n rm -f "$f"; rm -rf "$t"; mknod "$t" c 0 0\n done\n n=0\n until mkdir "$d.lock" 2>/dev/null; do\n n=$((n+1)); [ "$n" -gt 600 ] && { rm -rf "$d.lock"; continue; } # a holder that died\n sleep 0.1\n done\n if [ -f "$d.done" ]; then rm -rf "$x"; else rm -rf "$d"; mv "$x" "$d"; touch "$d.done"; fi\n rmdir "$d.lock"\n fi\n lower="$d\${lower:+:$lower}"\ndone\nif [ -z "$lower" ]; then mkdir -p "$root/layers/empty"; lower="$root/layers/empty"; fi\nc="$root/containers/$cid"\nmkdir -p "$c/upper" "$c/work" "$c/merged"\nrm -f "$c/stdin" "$c/ctl" "$c/console.sock"; mkfifo "$c/stdin" "$c/ctl"\nmountpoint -q "$c/merged" || mount -t overlay overlay -o "lowerdir=$lower,upperdir=$c/upper,workdir=$c/work" "$c/merged"\nmkdir -p "$c/merged$workdir"\n`,UNMOUNT='c="$1/containers/$2"; mountpoint -q "$c/merged" && umount "$c/merged" || true',REMOVE='c="$1/containers/$2"; mountpoint -q "$c/merged" && umount -l "$c/merged"; rm -rf "$c"';function layerCompression(o){return/gzip/.test(o||"")?"gzip":"tar"}class Rootfs{constructor({host:o,blobsDir:t,root:r="/var/lib/tnc-dock"}){this.host=o,this.blobsDir=t,this.root=r}mergedPath(o){return`${this.root}/containers/${o}/merged`}stdinFifo(o){return`${this.root}/containers/${o}/stdin`}ctlFifo(o){return`${this.root}/containers/${o}/ctl`}consoleSock(o){return`${this.root}/containers/${o}/console.sock`}async prepare(o,t,r="/"){const{shim:n}=this.host.binaries(),e=[this.root,"vm"===this.host.kind?1:0,n,o,r];for(const o of t.layers||[]){const t=o.digest.replace(/^sha256:/,"");e.push(t,this.host.guestPath(`${this.blobsDir}/${t}`),layerCompression(o.mediaType))}return await this.host.sh(PREPARE,e),this.mergedPath(o)}unmount(o){return this.host.sh(UNMOUNT,[this.root,o])}remove(o){return this.host.sh(REMOVE,[this.root,o])}async readFile(o,t){const r=await this.host.run(["cat",`${this.mergedPath(o)}${t}`]);return 0===r.code?r.stdout:""}}module.exports={Rootfs,IO_CGROUP};