@helios-lang/contract-utils 0.3.21 → 0.3.23

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 (79) hide show
  1. package/README.md +52 -0
  2. package/package.json +10 -4
  3. package/src/artifacts/ChildArtifactWriter.js +18 -0
  4. package/src/artifacts/RootArtifact.js +8 -0
  5. package/src/cli/compile.js +211 -0
  6. package/src/cli/config.mjs +158 -0
  7. package/src/cli/login.mjs +117 -0
  8. package/src/cli/project.mjs +67 -0
  9. package/src/cli/wallet-file.mjs +14 -0
  10. package/src/cli/wallet.mjs +51 -0
  11. package/src/codegen/LoadedScriptsWriter.js +20 -3
  12. package/src/context/ContractContextBuilder.js +34 -1
  13. package/src/context/debugger.js +46 -0
  14. package/src/helios.mjs +50 -0
  15. package/src/hl2ts.js +5 -185
  16. package/src/index.js +10 -1
  17. package/types/artifacts/ChildArtifactWriter.d.ts.map +1 -1
  18. package/types/artifacts/RootArtifact.d.ts.map +1 -1
  19. package/types/cli/compile.d.ts +9 -0
  20. package/types/cli/compile.d.ts.map +1 -0
  21. package/types/cli/config.d.mts +5 -0
  22. package/types/cli/config.d.mts.map +1 -0
  23. package/types/cli/project.d.mts +11 -0
  24. package/types/cli/project.d.mts.map +1 -0
  25. package/types/codegen/LoadedScriptsWriter.d.ts +2 -1
  26. package/types/codegen/LoadedScriptsWriter.d.ts.map +1 -1
  27. package/types/context/ContractContextBuilder.d.ts.map +1 -1
  28. package/types/context/debugger.d.ts +9 -0
  29. package/types/context/debugger.d.ts.map +1 -0
  30. package/types/hl2ts.d.ts +1 -6
  31. package/types/hl2ts.d.ts.map +1 -1
  32. package/types/index.d.ts +18 -1
  33. package/types/index.d.ts.map +1 -1
  34. package/types/cast/PermissiveType.d.ts +0 -4
  35. package/types/cast/PermissiveType.d.ts.map +0 -1
  36. package/types/cast/StrictType.d.ts +0 -4
  37. package/types/cast/StrictType.d.ts.map +0 -1
  38. package/types/codegen/LoadedModule.d.ts +0 -17
  39. package/types/codegen/LoadedModule.d.ts.map +0 -1
  40. package/types/codegen/LoadedValidator.d.ts +0 -38
  41. package/types/codegen/LoadedValidator.d.ts.map +0 -1
  42. package/types/codegen/Module.d.ts +0 -24
  43. package/types/codegen/Module.d.ts.map +0 -1
  44. package/types/codegen/TypeCheckedModule.d.ts +0 -24
  45. package/types/codegen/TypeCheckedModule.d.ts.map +0 -1
  46. package/types/codegen/TypeCheckedValidator.d.ts +0 -9
  47. package/types/codegen/TypeCheckedValidator.d.ts.map +0 -1
  48. package/types/codegen/TypescriptWriter.d.ts +0 -50
  49. package/types/codegen/TypescriptWriter.d.ts.map +0 -1
  50. package/types/codegen/Validator.d.ts +0 -22
  51. package/types/codegen/Validator.d.ts.map +0 -1
  52. package/types/codegen/Writer.d.ts +0 -23
  53. package/types/codegen/Writer.d.ts.map +0 -1
  54. package/types/context/ContractContext.d.ts +0 -53
  55. package/types/context/ContractContext.d.ts.map +0 -1
  56. package/types/context/writeContractContextArtifacts.d.ts +0 -30
  57. package/types/context/writeContractContextArtifacts.d.ts.map +0 -1
  58. package/types/lib/Lib.d.ts +0 -41
  59. package/types/lib/Lib.d.ts.map +0 -1
  60. package/types/lib/Libg_v0_16.d.ts +0 -256
  61. package/types/lib/Libg_v0_16.d.ts.map +0 -1
  62. package/types/lib/index.d.ts +0 -3
  63. package/types/lib/index.d.ts.map +0 -1
  64. package/types/lib/load.d.ts +0 -6
  65. package/types/lib/load.d.ts.map +0 -1
  66. package/types/runtime/Cast.d.ts +0 -32
  67. package/types/runtime/Cast.d.ts.map +0 -1
  68. package/types/runtime/ContractContext.d.ts +0 -23
  69. package/types/runtime/ContractContext.d.ts.map +0 -1
  70. package/types/runtime/ContractContextBuilder.d.ts +0 -53
  71. package/types/runtime/ContractContextBuilder.d.ts.map +0 -1
  72. package/types/runtime/DagCompiler.d.ts +0 -64
  73. package/types/runtime/DagCompiler.d.ts.map +0 -1
  74. package/types/runtime/PermissiveType.d.ts +0 -3
  75. package/types/runtime/PermissiveType.d.ts.map +0 -1
  76. package/types/runtime/StrictType.d.ts +0 -3
  77. package/types/runtime/StrictType.d.ts.map +0 -1
  78. package/types/runtime/index.d.ts +0 -2
  79. package/types/runtime/index.d.ts.map +0 -1
package/README.md CHANGED
@@ -1,2 +1,54 @@
1
1
  # contract-utils
2
2
  Convenience and type-safety utilities for using Helios validators from within Typescript
3
+
4
+ ## Helios CLI
5
+
6
+ Requires Node.js 22 or newer. Install globally with npm:
7
+
8
+ ```sh
9
+ npm install -g @helios-lang/contract-utils
10
+ helios --help
11
+ helios compile --format typescript --out-dir ./generated
12
+ helios login
13
+ ```
14
+
15
+ `helios compile` and `hl2ts` share the same compiler and options. Compilation searches the current directory for `.hl` files and does not require login. `helios compile --help` lists compiler options. The compiler peer dependency is restricted to the supported 0.16/0.17 series.
16
+
17
+ `helios login` opens `https://helios-lang.io/console` with an expiring login request. Connect your wallet, create a project if you have no active projects, compare the code with your terminal, and click **Authorize CLI**. All active project API keys for that wallet are installed locally. The browser displays **You can now close this tab** only after the CLI confirms that the file was saved. Use `helios login --no-browser` to open the printed URL manually, including from another computer. Login expires after ten minutes; cancellation or failure leaves the existing file intact unless it was already saved and only the acknowledgment failed (reported explicitly).
18
+
19
+ Credentials are stored in a versioned `debugger.json` file:
20
+
21
+ | OS | Directory |
22
+ | --- | --- |
23
+ | Linux | `$XDG_CONFIG_HOME/helios`, or `~/.config/helios` |
24
+ | macOS | `~/Library/Application Support/Helios` |
25
+ | Windows | `%APPDATA%\Helios` |
26
+
27
+ Set `HELIOS_CONFIG_HOME` to override the directory. Unix directories/files use modes 700/600. Windows uses a current-user-only ACL. The JSON has `{version:1,selected,profiles}`, where `selected` is a project ID and `profiles` maps project IDs to `{id,name,created_at,apiKey,wallet,endpoint}`. Names need not be unique. Re-running login refreshes that wallet's snapshot and removes revoked projects while preserving other wallets. It keeps the selected project when available and otherwise selects the oldest returned project. No signatures, browser session cookies, or temporary login tokens are persisted.
28
+
29
+ API keys in this file are secrets: do not commit or share it. They authorize both capture uploads and reads for their project. Login never prints key values. The console's ordinary metadata endpoints do not return them; key recovery uses an explicitly approved, short-lived CLI request.
30
+
31
+ For development, run `pnpm test`. `node scripts/check-global-cli.mjs` packs the package, installs it into an isolated npm global prefix, and checks executable discovery and equivalent compilation. The CLI portability workflow runs these checks on Windows, macOS, and Linux with Node 22 and 24. Publish the package only after the website and Worker with CLI-login support have been deployed.
32
+
33
+ ### Compile for a debugger project
34
+
35
+ ```sh
36
+ helios login
37
+ helios compile --project "My project" --out-dir ./generated
38
+ # The compatibility command accepts the same option:
39
+ hl2ts --project "My project" --out-dir ./generated
40
+ ```
41
+
42
+ `--project` selects an exact configured project name and validates its key through `GET /v1/project` before writing output. Unknown names list all configured project names/IDs; an empty configuration explains how to run `helios login`. Duplicate names are rejected as ambiguous. Revoked keys, stale project metadata, and service outages stop compilation rather than generating a bundle with an unchecked key.
43
+
44
+ Generated JS/TS exports `$debugger: {projectId,name,apiKey,endpoint,sources}` and attaches it to each loaded validator/module. `makeContractContextBuilder()` carries that context onto the UPLC programs used by each validator's `$hash.context.program`, including source maps and unoptimized companions. Binding happens after cache lookup: changing projects does not reuse another project's credentials, and compilation caches remain credential-free. Written contract artifacts also preserve the context. Script CBOR and hashes are unchanged.
45
+
46
+ With the corresponding tx-utils release and ledger evaluation-observer support, `spendWithRedeemer()`, minting/staking methods, and attached programs automatically deliver failed builds to that project. No explicit `makeDebuggerService()` call is needed. `makeTxBuilder({isMainnet, debugger:false})` disables automatic capture; an explicit debugger service overrides bundle configuration. Unannotated bundles retain their existing behavior.
47
+
48
+ The API key is intentionally embedded in generated bundles, including browser bundles, and grants upload **and read** access to that project's feed. Use project-specific keys appropriate for that distribution. The key is sent in the authorization header, not in capture payloads. Omitting `--project` embeds no debugger credentials.
49
+
50
+ ### Offline wallet generation
51
+
52
+ `helios wallet create --network preprod` prints JSON containing a cryptographically random 24-word recovery phrase and its base address. It does not contact a service. Supported networks are `preprod` (default), `preview`, and `mainnet`.
53
+
54
+ Use `helios wallet create --network preprod --out wallet.json` to write a new private file instead of printing the phrase. Existing files are never overwritten. The output phrase can be pasted into a demo’s ignored `.env.local` file.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helios-lang/contract-utils",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "Convenience and type-safety utilities for using Helios validators from within Typescript",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
@@ -17,7 +17,8 @@
17
17
  ],
18
18
  "type": "module",
19
19
  "bin": {
20
- "hl2ts": "src/hl2ts.js"
20
+ "hl2ts": "src/hl2ts.js",
21
+ "helios": "src/helios.mjs"
21
22
  },
22
23
  "author": "Christian Schmitz",
23
24
  "license": "BSD-3-Clause",
@@ -33,7 +34,7 @@
33
34
  "singleQuote": false
34
35
  },
35
36
  "peerDependencies": {
36
- "@helios-lang/compiler": "*"
37
+ "@helios-lang/compiler": "^0.16.0 || ^0.17.0"
37
38
  },
38
39
  "dependencies": {
39
40
  "@helios-lang/cli-utils": "^0.1.10",
@@ -41,8 +42,13 @@
41
42
  "@helios-lang/compiler-utils": "^0.5.15",
42
43
  "@helios-lang/crypto": "0.2.3",
43
44
  "@helios-lang/ledger": "^0.7.8",
45
+ "@helios-lang/tx-utils": "^0.6.41",
44
46
  "@helios-lang/type-utils": "^0.3.0",
45
- "@helios-lang/uplc": "^0.7.15"
47
+ "@helios-lang/uplc": "^0.7.15",
48
+ "open": "^10.2.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=22"
46
52
  },
47
53
  "scripts": {
48
54
  "build": "pnpm run prettify && pnpm run build:types && pnpm run test:suite",
@@ -99,6 +99,24 @@ export class ChildArtifactWriter extends ArtifactWriter {
99
99
  throw new Error("unhandled Plutus version")
100
100
  }
101
101
 
102
+ const debuggerContext =
103
+ /** @type {UplcProgram & {$debugger?: import("../index.js").DebuggerContext}} */ (
104
+ program
105
+ ).$debugger
106
+ if (debuggerContext && includeDef) {
107
+ this.writeDefLine(
108
+ `export const $debugger = ${JSON.stringify(debuggerContext)}`
109
+ )
110
+ this.writeDeclLine(
111
+ `export declare const $debugger: import("@helios-lang/contract-utils").DebuggerContext`
112
+ )
113
+ this.writeDefLine(
114
+ `Object.defineProperty(${name}, "$debugger", {value:$debugger})`
115
+ )
116
+ this.writeDefLine(
117
+ `if (${name}.alt) Object.defineProperty(${name}.alt, "$debugger", {value:$debugger})`
118
+ )
119
+ }
102
120
  return this
103
121
  }
104
122
  }
@@ -43,6 +43,14 @@ export function writeContractContextArtifacts(context, options) {
43
43
  }
44
44
  }
45
45
 
46
+ if (context.$debugger) {
47
+ root.writeDefLine(
48
+ `export const $debugger = ${JSON.stringify(context.$debugger)}`
49
+ )
50
+ root.writeDeclLine(
51
+ `export declare const $debugger: import("@helios-lang/contract-utils").DebuggerContext`
52
+ )
53
+ }
46
54
  root.writeValidatorNameUtils(validatorNames)
47
55
  root.save()
48
56
  }
@@ -0,0 +1,211 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ promises,
5
+ readFileSync,
6
+ writeFileSync
7
+ } from "node:fs"
8
+ import { dirname, join, resolve } from "node:path"
9
+ import { Cli, EnumOpt, StringOpt } from "@helios-lang/cli-utils"
10
+ import { makeLoadedScriptsWriter } from "../codegen/index.js"
11
+ import { loadCompilerLib } from "../compiler/index.js"
12
+ import { typeCheckFiles } from "../compiler/ops.js"
13
+
14
+ /**
15
+ * @typedef {{
16
+ * name: string
17
+ * purpose: string
18
+ * sourceCode: string
19
+ * }} SourceDetails
20
+ */
21
+
22
+ /**
23
+ * @typedef {"javascript" | "typescript"} FormatKind
24
+ */
25
+
26
+ const NAME = "hl2ts"
27
+
28
+ /** @param {string[]} args */
29
+ export async function compile(args) {
30
+ const projectIndex = args.indexOf("--project")
31
+ if (projectIndex >= 0 && !args[projectIndex + 1]?.trim())
32
+ throw new Error(
33
+ "--project requires a project name; run helios login to configure projects"
34
+ )
35
+ const cli = new Cli(NAME, {
36
+ minArgs: 0,
37
+ maxArgs: 0,
38
+ options: {
39
+ project: new StringOpt({
40
+ long: "--project",
41
+ description: "Debugger project name (from helios login)",
42
+ default: ""
43
+ }),
44
+ outDir: new StringOpt({
45
+ long: "--out-dir",
46
+ short: "-o",
47
+ default: () => process.cwd()
48
+ }),
49
+ format: new EnumOpt({
50
+ long: "--format",
51
+ short: "-f",
52
+ variants: /** @type {FormatKind[]} */ ([
53
+ "javascript",
54
+ "typescript"
55
+ ]),
56
+ default: () => {
57
+ const path = findPackageJson()
58
+
59
+ if (
60
+ path &&
61
+ existsSync(join(dirname(path), "tsconfig.json"))
62
+ ) {
63
+ return "typescript"
64
+ } else {
65
+ return "javascript"
66
+ }
67
+ }
68
+ })
69
+ },
70
+ action: mainInternal
71
+ })
72
+
73
+ await cli.run(["node", "hl2ts", ...args])
74
+ }
75
+
76
+ /**
77
+ * @param {string[]} _args
78
+ * @param {{outDir: string, format: FormatKind, project: string}} options
79
+ * @returns {Promise<void>}
80
+ */
81
+ async function mainInternal(_args, { outDir, format, project }) {
82
+ const debugProject = project
83
+ ? await (await import("./project.mjs")).resolveProject(project)
84
+ : undefined
85
+ const lib = loadCompilerLib()
86
+
87
+ const filePaths = await listFiles(process.cwd(), ".hl")
88
+ const files = readFiles(filePaths)
89
+
90
+ console.log(`${NAME} using Helios compiler v${lib.version}`)
91
+ console.log(` Transpiling ${Object.keys(files).length} inputs:`)
92
+ filePaths.forEach((p) => console.log(` ${p}`))
93
+
94
+ const { modules, validators } = typeCheckFiles(lib, files, (current, rel) =>
95
+ resolve(join(dirname(current), rel))
96
+ )
97
+
98
+ const sources = debugProject
99
+ ? {
100
+ ...files,
101
+ ...Object.fromEntries(
102
+ [...Object.values(modules), ...Object.values(validators)].map(
103
+ (m) => [m.name, m.sourceCode]
104
+ )
105
+ )
106
+ }
107
+ : undefined
108
+ const [js, dts, ts] = makeLoadedScriptsWriter(
109
+ debugProject ? { ...debugProject, sources } : undefined
110
+ )
111
+ .writeModules(modules)
112
+ .writeValidators(validators)
113
+ .finalize()
114
+
115
+ if (format == "typescript") {
116
+ mkdirIfNotExists(resolve(outDir))
117
+ const tsPath = join(outDir, "index.ts")
118
+
119
+ writeFileSync(tsPath, ts)
120
+
121
+ console.log(` Output:`)
122
+ console.log(` ${tsPath}`)
123
+ } else {
124
+ const resolvedOutDir = resolve(outDir)
125
+
126
+ mkdirIfNotExists(resolvedOutDir)
127
+
128
+ const jsPath = join(resolvedOutDir, "index.js")
129
+ const dtsPath = join(resolvedOutDir, "index.d.ts")
130
+
131
+ writeFileSync(jsPath, js)
132
+ writeFileSync(dtsPath, dts)
133
+
134
+ console.log(` Output:`)
135
+ console.log(` ${jsPath}`)
136
+ console.log(` ${dtsPath}`)
137
+ }
138
+ }
139
+
140
+ /**
141
+ * @param {string} dir
142
+ * @param {string} ext
143
+ * @returns {Promise<string[]>}
144
+ */
145
+ async function listFiles(dir, ext) {
146
+ const entries = await promises.readdir(dir, { withFileTypes: true })
147
+
148
+ const files = await Promise.all(
149
+ entries.map((entry) => {
150
+ const res = resolve(dir, entry.name)
151
+
152
+ if (entry.isDirectory()) {
153
+ if (entry.name.endsWith("node_modules")) {
154
+ return []
155
+ } else {
156
+ return listFiles(res, ext)
157
+ }
158
+ } else {
159
+ return res
160
+ }
161
+ })
162
+ )
163
+
164
+ return files.flat().filter((name) => name.endsWith(ext))
165
+ }
166
+
167
+ /**
168
+ * @param {string[]} filePaths
169
+ * @returns {{[path: string]: string}}
170
+ */
171
+ function readFiles(filePaths) {
172
+ return Object.fromEntries(
173
+ filePaths.map((f) => {
174
+ return [f, readFileSync(f).toString()]
175
+ })
176
+ )
177
+ }
178
+
179
+ /**
180
+ * @param {string} fileName
181
+ * @returns {string | undefined}
182
+ */
183
+ function findPackageJson(fileName = "package.json") {
184
+ let dir = process.cwd()
185
+
186
+ let path = join(dir, fileName)
187
+ let found = existsSync(path)
188
+
189
+ while (!found) {
190
+ const parent = dirname(dir)
191
+ if (parent === dir) return undefined
192
+ dir = parent
193
+ path = join(dir, fileName)
194
+ found = existsSync(path)
195
+ }
196
+
197
+ if (found) {
198
+ return path
199
+ } else {
200
+ return undefined
201
+ }
202
+ }
203
+
204
+ /**
205
+ * @param {string} dir
206
+ */
207
+ function mkdirIfNotExists(dir) {
208
+ if (!existsSync(dir)) {
209
+ mkdirSync(dir, { recursive: true })
210
+ }
211
+ }
@@ -0,0 +1,158 @@
1
+ import { homedir } from "node:os"
2
+ import { join, dirname, win32, posix } from "node:path"
3
+ import {
4
+ lstat,
5
+ mkdir,
6
+ readFile,
7
+ writeFile,
8
+ rename,
9
+ chmod,
10
+ unlink
11
+ } from "node:fs/promises"
12
+ import { randomUUID } from "node:crypto"
13
+ import { execFile } from "node:child_process"
14
+ import { promisify } from "node:util"
15
+ const exec = promisify(execFile)
16
+ export async function windowsAcl(path, directory, set = false) {
17
+ // Pass paths through the environment, never interpolate them into PowerShell.
18
+ const script = `
19
+ $ErrorActionPreference = 'Stop'
20
+ $path = $env:HELIOS_CONFIG_ACL_PATH
21
+ $sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
22
+ if (${set ? "$true" : "$false"}) {
23
+ $acl = New-Object System.Security.AccessControl.${directory ? "DirectorySecurity" : "FileSecurity"}
24
+ $acl.SetOwner($sid)
25
+ $acl.SetAccessRuleProtection($true, $false)
26
+ $rule = New-Object System.Security.AccessControl.FileSystemAccessRule($sid, 'FullControl', '${directory ? "ContainerInherit, ObjectInherit" : "None"}', 'None', 'Allow')
27
+ $acl.AddAccessRule($rule)
28
+ Set-Acl -LiteralPath $path -AclObject $acl
29
+ } else {
30
+ $acl = Get-Acl -LiteralPath $path
31
+ if ($acl.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $sid.Value) { throw 'Configuration must be owned by the current user' }
32
+ foreach ($rule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) {
33
+ if ($rule.AccessControlType -eq 'Allow' -and $rule.IdentityReference.Value -ne $sid.Value) { throw 'Configuration must be accessible only by the current user' }
34
+ }
35
+ }`
36
+ await exec(
37
+ "powershell.exe",
38
+ [
39
+ "-NoProfile",
40
+ "-NonInteractive",
41
+ "-EncodedCommand",
42
+ Buffer.from(script, "utf16le").toString("base64")
43
+ ],
44
+ {
45
+ windowsHide: true,
46
+ env: { ...process.env, HELIOS_CONFIG_ACL_PATH: path }
47
+ }
48
+ )
49
+ }
50
+ export function configPath(
51
+ platform = process.platform,
52
+ env = process.env,
53
+ home = homedir()
54
+ ) {
55
+ const join = platform === "win32" ? win32.join : posix.join
56
+ const directory =
57
+ env.HELIOS_CONFIG_HOME ||
58
+ (platform === "win32"
59
+ ? join(env.APPDATA || join(home, "AppData", "Roaming"), "Helios")
60
+ : platform === "darwin"
61
+ ? join(home, "Library", "Application Support", "Helios")
62
+ : join(env.XDG_CONFIG_HOME || join(home, ".config"), "helios"))
63
+ return join(directory, "debugger.json")
64
+ }
65
+ async function inspect(path, directory = false) {
66
+ try {
67
+ const stat = await lstat(path)
68
+ if (
69
+ stat.isSymbolicLink() ||
70
+ !(directory ? stat.isDirectory() : stat.isFile())
71
+ )
72
+ throw new Error(`Unsafe configuration path: ${path}`)
73
+ if (
74
+ process.platform !== "win32" &&
75
+ (stat.mode & 0o077 || stat.uid !== process.getuid?.())
76
+ )
77
+ throw new Error(`Configuration must be owner-only: ${path}`)
78
+ if (process.platform === "win32") await windowsAcl(path, directory)
79
+ return true
80
+ } catch (error) {
81
+ if (error.code === "ENOENT") return false
82
+ throw error
83
+ }
84
+ }
85
+ export async function readConfig(path = configPath()) {
86
+ await inspect(dirname(path), true)
87
+ if (!(await inspect(path)))
88
+ return { version: 1, selected: null, profiles: {} }
89
+ const data = JSON.parse(await readFile(path, "utf8"))
90
+ if (
91
+ data.version !== 1 ||
92
+ !data.profiles ||
93
+ typeof data.profiles !== "object" ||
94
+ Array.isArray(data.profiles)
95
+ )
96
+ throw new Error("Unsupported or invalid Helios configuration")
97
+ return data
98
+ }
99
+ export async function installProjects(
100
+ wallet,
101
+ projects,
102
+ endpoint,
103
+ path = configPath()
104
+ ) {
105
+ if (
106
+ !/^[a-f0-9]{56}$/.test(wallet) ||
107
+ !Array.isArray(projects) ||
108
+ !projects.length
109
+ )
110
+ throw new Error("Invalid or empty project response")
111
+ const ids = new Set()
112
+ for (const p of projects) {
113
+ if (
114
+ !/^[a-f0-9-]{36}$/.test(p.id) ||
115
+ ids.has(p.id) ||
116
+ typeof p.name !== "string" ||
117
+ !p.name.trim() ||
118
+ !Number.isSafeInteger(p.created_at) ||
119
+ p.created_at < 0 ||
120
+ !/^hdbg_[a-f0-9]{64}$/.test(p.apiKey)
121
+ )
122
+ throw new Error("Invalid project response")
123
+ ids.add(p.id)
124
+ }
125
+ const data = await readConfig(path)
126
+ for (const [id, profile] of Object.entries(data.profiles))
127
+ if (profile.wallet === wallet && profile.endpoint === endpoint)
128
+ delete data.profiles[id]
129
+ for (const project of projects)
130
+ data.profiles[project.id] = { ...project, wallet, endpoint }
131
+ if (!data.profiles[data.selected])
132
+ data.selected = [...projects].sort(
133
+ (a, b) => a.created_at - b.created_at || a.id.localeCompare(b.id)
134
+ )[0].id
135
+ const dir = dirname(path)
136
+ const existed = await inspect(dir, true)
137
+ await mkdir(dir, { recursive: true, mode: 0o700 })
138
+ if (!existed && process.platform === "win32")
139
+ await windowsAcl(dir, true, true)
140
+ await inspect(dir, true)
141
+ const temporary = join(dir, `.debugger-${randomUUID()}.tmp`)
142
+ try {
143
+ await writeFile(temporary, JSON.stringify(data, null, 2) + "\n", {
144
+ flag: "wx",
145
+ mode: 0o600
146
+ })
147
+ if (process.platform === "win32")
148
+ await windowsAcl(temporary, false, true)
149
+ else await chmod(temporary, 0o600)
150
+ await inspect(path)
151
+ await rename(temporary, path)
152
+ } finally {
153
+ await unlink(temporary).catch((error) => {
154
+ if (error.code !== "ENOENT") throw error
155
+ })
156
+ }
157
+ return path
158
+ }
@@ -0,0 +1,117 @@
1
+ import { installProjects } from "./config.mjs"
2
+ const sleep = (ms, signal) =>
3
+ new Promise((resolve, reject) => {
4
+ const onAbort = () => {
5
+ clearTimeout(timer)
6
+ reject(signal.reason)
7
+ }
8
+ const timer = setTimeout(() => {
9
+ signal.removeEventListener("abort", onAbort)
10
+ resolve()
11
+ }, ms)
12
+ signal.addEventListener("abort", onAbort, { once: true })
13
+ if (signal.aborted) onAbort()
14
+ })
15
+ export async function login({
16
+ noBrowser = false,
17
+ endpoint = "https://debugger.helios-lang.io",
18
+ fetchImpl = fetch,
19
+ openBrowser,
20
+ save = installProjects,
21
+ log = console.log,
22
+ wait = sleep,
23
+ signal = AbortSignal.timeout(600000)
24
+ } = {}) {
25
+ async function request(path, method = "GET", token) {
26
+ const response = await fetchImpl(endpoint + "/v1/" + path, {
27
+ method,
28
+ redirect: "error",
29
+ signal: AbortSignal.any([signal, AbortSignal.timeout(15000)]),
30
+ headers: token ? { Authorization: `Bearer ${token}` } : {}
31
+ })
32
+ const data = await response.json()
33
+ if (!response.ok)
34
+ throw Object.assign(
35
+ new Error(data.error || `HTTP ${response.status}`),
36
+ { status: response.status }
37
+ )
38
+ return data
39
+ }
40
+ const flow = await request("cli/logins", "POST")
41
+ if (
42
+ !/^[a-f0-9-]{36}$/.test(flow.id) ||
43
+ !/^hcli_[a-f0-9]{64}$/.test(flow.token) ||
44
+ !/^[A-F0-9]{8}$/.test(flow.code) ||
45
+ !Number.isFinite(flow.expires)
46
+ )
47
+ throw new Error("Invalid login response")
48
+ const url = new URL(flow.verificationUri)
49
+ if (
50
+ url.origin !== "https://helios-lang.io" ||
51
+ url.pathname !== "/console" ||
52
+ url.searchParams.get("cli_login") !== flow.id
53
+ )
54
+ throw new Error("Invalid Console login URL")
55
+ log(
56
+ `Open ${url.href}\nVerify code: ${flow.code}\nConnect your wallet, then authorize the CLI.`
57
+ )
58
+ if (!noBrowser) {
59
+ try {
60
+ await (openBrowser ?? (await import("open")).default)(url.href)
61
+ } catch {
62
+ log("Could not open your browser. Open the URL above manually.")
63
+ }
64
+ }
65
+ let failures = 0
66
+ while (!signal.aborted && Date.now() / 1000 < flow.expires) {
67
+ let result
68
+ try {
69
+ result = await request(`cli/logins/${flow.id}`, "GET", flow.token)
70
+ failures = 0
71
+ } catch (error) {
72
+ if (
73
+ signal.aborted ||
74
+ (error.status && error.status < 500 && error.status !== 429) ||
75
+ ++failures > 5
76
+ )
77
+ throw error
78
+ await wait(Math.min(5000 * 2 ** failures, 30000), signal)
79
+ continue
80
+ }
81
+ if (result.state === "approved") {
82
+ const path = await save(result.wallet, result.projects, endpoint)
83
+ for (let attempt = 0; ; attempt++) {
84
+ try {
85
+ await request(
86
+ `cli/logins/${flow.id}/complete`,
87
+ "POST",
88
+ flow.token
89
+ )
90
+ break
91
+ } catch (error) {
92
+ if (
93
+ signal.aborted ||
94
+ attempt === 4 ||
95
+ (error.status &&
96
+ error.status < 500 &&
97
+ error.status !== 429)
98
+ )
99
+ throw new Error(
100
+ `Keys saved to ${path}, but completion could not be confirmed. ${error.message}`
101
+ )
102
+ await wait(5000, signal)
103
+ }
104
+ }
105
+ log(`Installed ${result.projects.length} project(s) in ${path}:`)
106
+ for (const project of result.projects)
107
+ log(` ${JSON.stringify(project.name)}`)
108
+ return
109
+ }
110
+ if (result.state === "cancelled")
111
+ throw new Error("Login cancelled in browser")
112
+ if (result.state !== "pending")
113
+ throw new Error("Login already completed; run helios login again")
114
+ await wait(5000, signal)
115
+ }
116
+ throw new Error("Login expired or cancelled; run helios login again")
117
+ }
@@ -0,0 +1,67 @@
1
+ import { readConfig } from "./config.mjs"
2
+ export async function resolveProject(
3
+ name,
4
+ { read = readConfig, fetchImpl = fetch } = {}
5
+ ) {
6
+ const config = await read()
7
+ const projects = Object.values(config.profiles)
8
+ const available = projects.length
9
+ ? "Configured projects: " +
10
+ projects.map((p) => `${JSON.stringify(p.name)} (${p.id})`).join(", ")
11
+ : "No projects are configured. Run `helios login` to connect your wallet and install project keys."
12
+ const matches = projects.filter((project) => project.name === name)
13
+ if (!matches.length)
14
+ throw new Error(
15
+ `Project ${JSON.stringify(name)} does not exist. ${available}`
16
+ )
17
+ if (matches.length > 1)
18
+ throw new Error(
19
+ `Project name ${JSON.stringify(name)} is ambiguous. Give projects distinct names before compiling. ${available}`
20
+ )
21
+ const project = matches[0]
22
+ if (!/^hdbg_[a-f0-9]{64}$/.test(project.apiKey))
23
+ throw new Error("Invalid configured API key. Run `helios login` again.")
24
+ const endpoint = new URL(project.endpoint)
25
+ if (
26
+ endpoint.protocol !== "https:" ||
27
+ endpoint.username ||
28
+ endpoint.password ||
29
+ endpoint.pathname !== "/" ||
30
+ endpoint.search ||
31
+ endpoint.hash
32
+ )
33
+ throw new Error(
34
+ "Project endpoint must be an HTTPS origin. Run `helios login` again."
35
+ )
36
+ let response
37
+ try {
38
+ response = await fetchImpl(`${endpoint.origin}/v1/project`, {
39
+ headers: { Authorization: `Bearer ${project.apiKey}` },
40
+ signal: AbortSignal.timeout(10000),
41
+ redirect: "error"
42
+ })
43
+ } catch {
44
+ throw new Error(
45
+ "Could not validate the project API key with the debugger service. Check your connection and try again."
46
+ )
47
+ }
48
+ if (response.status === 401)
49
+ throw new Error(
50
+ `The API key for ${JSON.stringify(name)} is invalid or revoked. Run \`helios login\` again.`
51
+ )
52
+ if (!response.ok)
53
+ throw new Error(
54
+ `Debugger project validation failed (HTTP ${response.status}). Try again later.`
55
+ )
56
+ const remote = await response.json()
57
+ if (remote.id !== project.id || remote.name !== project.name)
58
+ throw new Error(
59
+ "Configured project metadata is outdated. Run `helios login` again."
60
+ )
61
+ return {
62
+ projectId: remote.id,
63
+ name: remote.name,
64
+ apiKey: project.apiKey,
65
+ endpoint: endpoint.origin
66
+ }
67
+ }