Change how login works
This commit is contained in:
+1
-1
@@ -97,6 +97,6 @@ async function run(ctx, argv) {
|
||||
'Docs: man <command> | man handbook (full handbook, section 7) | man bare-os-shell | man -k <word> | man -l'
|
||||
)
|
||||
ctx.console.log(
|
||||
'Identity: login [--new] <passphrase> | logout [--save] | savevault (encrypt copy of personal drive under /.bare/vault/)'
|
||||
'Identity: login [--new] (passphrase at prompt; multi-word ok) | logout [--save] | savevault (encrypt copy of personal drive under /.bare/vault/)'
|
||||
)
|
||||
}
|
||||
|
||||
+221
-85
@@ -87,85 +87,102 @@ function bareOsEmitRaw(ctx, chunk) {
|
||||
return false
|
||||
}
|
||||
|
||||
function loginDecodeChunk(chunk) {
|
||||
if (typeof chunk === 'string') return chunk
|
||||
if (chunk instanceof Uint8Array) return new TextDecoder().decode(chunk)
|
||||
if (
|
||||
typeof Buffer !== 'undefined' &&
|
||||
typeof Buffer.isBuffer === 'function' &&
|
||||
Buffer.isBuffer(chunk)
|
||||
) {
|
||||
return chunk.toString('utf8')
|
||||
/**
|
||||
* Plain-stream masked line input (same pattern as agent --config / bareAgentPromptSetupLine).
|
||||
* Used by /bin/login so the Fish REPL is suspended and stdin/stdout are the TTY streams.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown> | undefined} ctx
|
||||
* @param {import('stream').Writable | undefined} out
|
||||
* @param {string} s
|
||||
*/
|
||||
function bareLoginInteractiveWrite(ctx, out, s) {
|
||||
const text =
|
||||
ctx && ctx.bareOsPtyStdoutCrlf
|
||||
? String(s).replace(/\r?\n/g, '\r\n')
|
||||
: String(s)
|
||||
if (out && typeof out.write === 'function') {
|
||||
try {
|
||||
out.write(text)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return String(chunk)
|
||||
}
|
||||
|
||||
async function loginReadMaskedLine(ctx, prompt) {
|
||||
const stdin = ctx.stdin
|
||||
const stdout = ctx.stdout || globalThis.process?.stdout
|
||||
if (!stdin || !stdout || typeof stdin.on !== 'function' || typeof stdout.write !== 'function') {
|
||||
if (typeof ctx.readLine === 'function') return String((await ctx.readLine(prompt)) || '')
|
||||
return ''
|
||||
}
|
||||
stdout.write(prompt)
|
||||
const ttyIn = stdin
|
||||
if (!ttyIn.isTTY || typeof ttyIn.setRawMode !== 'function') {
|
||||
return await new Promise((resolve) => {
|
||||
let acc = ''
|
||||
function onData(chunk) {
|
||||
const s = loginDecodeChunk(chunk)
|
||||
acc += s
|
||||
const nl = acc.indexOf('\n')
|
||||
if (nl >= 0) {
|
||||
cleanup()
|
||||
resolve(acc.slice(0, nl).replace(/\r$/, ''))
|
||||
}
|
||||
}
|
||||
function onEnd() {
|
||||
/**
|
||||
* @param {import('stream').Readable} stdin
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function bareLoginReadStreamLineOnce(stdin) {
|
||||
return new Promise((resolve) => {
|
||||
let acc = ''
|
||||
/** @param {string | Uint8Array | Buffer} chunk */
|
||||
function onData(chunk) {
|
||||
let s = ''
|
||||
if (typeof chunk === 'string') s = chunk
|
||||
else if (chunk instanceof Uint8Array) s = new TextDecoder().decode(chunk)
|
||||
else if (
|
||||
typeof Buffer !== 'undefined' &&
|
||||
typeof Buffer.isBuffer === 'function' &&
|
||||
Buffer.isBuffer(chunk)
|
||||
)
|
||||
s = chunk.toString('utf8')
|
||||
else s = String(chunk)
|
||||
acc += s
|
||||
const n = acc.indexOf('\n')
|
||||
if (n >= 0) {
|
||||
cleanup()
|
||||
resolve(acc.replace(/\r$/, ''))
|
||||
resolve(acc.slice(0, n).replace(/\r$/, ''))
|
||||
}
|
||||
function cleanup() {
|
||||
stdin.removeListener('data', onData)
|
||||
stdin.removeListener('end', onEnd)
|
||||
stdin.removeListener('error', onEnd)
|
||||
}
|
||||
stdin.on('data', onData)
|
||||
stdin.once('end', onEnd)
|
||||
stdin.once('error', onEnd)
|
||||
if (typeof stdin.resume === 'function') stdin.resume()
|
||||
})
|
||||
}
|
||||
return await new Promise((resolve, reject) => {
|
||||
const chars = []
|
||||
let done = false
|
||||
function finish(ok, err) {
|
||||
if (done) return
|
||||
done = true
|
||||
}
|
||||
function onEnd() {
|
||||
cleanup()
|
||||
stdout.write('\n')
|
||||
if (ok) resolve(chars.join(''))
|
||||
else reject(err || new Error('masked_input_failed'))
|
||||
resolve(acc.replace(/\r$/, ''))
|
||||
}
|
||||
function cleanup() {
|
||||
stdin.removeListener('data', onData)
|
||||
stdin.removeListener('end', onEnd)
|
||||
stdin.removeListener('error', onErr)
|
||||
try {
|
||||
ttyIn.setRawMode(false)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
function onEnd() {
|
||||
finish(true)
|
||||
}
|
||||
function onErr(e) {
|
||||
finish(false, e instanceof Error ? e : new Error(String(e)))
|
||||
stdin.removeListener('error', onEnd)
|
||||
}
|
||||
stdin.on('data', onData)
|
||||
stdin.once('end', onEnd)
|
||||
stdin.once('error', onEnd)
|
||||
if (typeof stdin.resume === 'function') stdin.resume()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {import('stream').Readable} stdin
|
||||
* @param {import('stream').Writable | undefined} stdout
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
function bareLoginReadMaskedLineOnce(ctx, stdin, stdout) {
|
||||
const ttyIn = /** @type {{ setRawMode?: (v: boolean) => void, isTTY?: boolean }} */ (
|
||||
stdin
|
||||
)
|
||||
if (!ttyIn || typeof ttyIn.setRawMode !== 'function' || !ttyIn.isTTY) {
|
||||
return bareLoginReadStreamLineOnce(stdin)
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
/** @type {string[]} */
|
||||
const chars = []
|
||||
let done = false
|
||||
/** @param {string | Uint8Array | Buffer} chunk */
|
||||
function onData(chunk) {
|
||||
if (done) return
|
||||
const s = loginDecodeChunk(chunk)
|
||||
let s = ''
|
||||
if (typeof chunk === 'string') s = chunk
|
||||
else if (chunk instanceof Uint8Array) s = new TextDecoder().decode(chunk)
|
||||
else if (
|
||||
typeof Buffer !== 'undefined' &&
|
||||
typeof Buffer.isBuffer === 'function' &&
|
||||
Buffer.isBuffer(chunk)
|
||||
) {
|
||||
s = chunk.toString('utf8')
|
||||
} else s = String(chunk)
|
||||
for (const ch of s) {
|
||||
const code = ch.charCodeAt(0)
|
||||
if (ch === '\r' || ch === '\n') {
|
||||
@@ -179,21 +196,48 @@ async function loginReadMaskedLine(ctx, prompt) {
|
||||
if (ch === '\u007f' || ch === '\b') {
|
||||
if (chars.length) {
|
||||
chars.pop()
|
||||
stdout.write('\b \b')
|
||||
bareLoginInteractiveWrite(ctx, stdout, '\b \b')
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (code >= 32 && code !== 127) {
|
||||
chars.push(ch)
|
||||
stdout.write('*')
|
||||
bareLoginInteractiveWrite(ctx, stdout, '*')
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @param {boolean} ok @param {Error} [err] */
|
||||
function finish(ok, err) {
|
||||
if (done) return
|
||||
done = true
|
||||
cleanup()
|
||||
if (ok) resolve(chars.join(''))
|
||||
else reject(err || new Error('masked_input_failed'))
|
||||
}
|
||||
function cleanup() {
|
||||
stdin.removeListener('data', onData)
|
||||
stdin.removeListener('end', onEnd)
|
||||
stdin.removeListener('error', onErr)
|
||||
try {
|
||||
ttyIn.setRawMode(false)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
bareLoginInteractiveWrite(ctx, stdout, '\n')
|
||||
}
|
||||
function onEnd() {
|
||||
finish(true)
|
||||
}
|
||||
/** @param {unknown} e */
|
||||
function onErr(e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
finish(false, new Error(msg))
|
||||
}
|
||||
try {
|
||||
ttyIn.setRawMode(true)
|
||||
} catch {
|
||||
finish(true)
|
||||
return
|
||||
return resolve('')
|
||||
}
|
||||
stdin.on('data', onData)
|
||||
stdin.once('end', onEnd)
|
||||
@@ -202,30 +246,122 @@ async function loginReadMaskedLine(ctx, prompt) {
|
||||
})
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
const rest = argv.slice(1)
|
||||
let createNew = false
|
||||
if (rest[0] === '--new') {
|
||||
createNew = true
|
||||
rest.shift()
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {string} prompt
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function bareLoginPromptMaskedLine(ctx, prompt) {
|
||||
const stdin = /** @type {import('stream').Readable | undefined} */ (
|
||||
ctx.replStdin || ctx.stdin
|
||||
)
|
||||
const stdout = /** @type {import('stream').Writable | undefined} */ (
|
||||
ctx.replStdout || ctx.stdout
|
||||
)
|
||||
if (!stdin || typeof stdin.on !== 'function') {
|
||||
throw new Error('login: stdin stream unavailable (need an interactive TTY)')
|
||||
}
|
||||
const passphrase = rest.length
|
||||
? rest.join(' ')
|
||||
: await loginReadMaskedLine(ctx, createNew ? 'New passphrase: ' : 'Passphrase: ')
|
||||
if (!passphrase) {
|
||||
ctx.console.error('usage: login [--new] [passphrase]')
|
||||
bareLoginInteractiveWrite(ctx, stdout, '\x1b[?25h\x1b[0m' + prompt)
|
||||
return bareLoginReadMaskedLineOnce(ctx, stdin, stdout)
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
const argv0 = argv[0] || 'login'
|
||||
const args = argv.slice(1)
|
||||
if (args.includes('-h') || args.includes('--help')) {
|
||||
ctx.console.log(
|
||||
'usage: ' +
|
||||
argv0 +
|
||||
' [--new]\n\n' +
|
||||
'Unlock or register your identity. Run this command with no passphrase on the line;\n' +
|
||||
'then type your passphrase at the prompt (hidden). A passphrase may be several words.\n' +
|
||||
'Passphrases must not be given as command-line arguments (they would appear in shell history).\n\n' +
|
||||
' --new register a new identity instead of unlocking an existing one.'
|
||||
)
|
||||
ctx.exitCode = 0
|
||||
return
|
||||
}
|
||||
|
||||
let createNew = false
|
||||
if (args[0] === '--new') {
|
||||
createNew = true
|
||||
args.shift()
|
||||
}
|
||||
if (args.length > 0) {
|
||||
ctx.console.error(
|
||||
'login: passphrase must not be given on the command line (it would appear in shell history).'
|
||||
)
|
||||
ctx.console.error('Run: login' + (createNew ? ' --new' : ''))
|
||||
ctx.console.error(
|
||||
'Then type your passphrase at the prompt. It may be multiple words; typing is hidden.'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const reg = ctx.applyRegister
|
||||
const unlock = ctx.applyUnlock
|
||||
if (typeof reg !== 'function' || typeof unlock !== 'function') {
|
||||
ctx.console.error('login: not supported in this environment')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
const stdin = ctx.replStdin || ctx.stdin
|
||||
const stdout = ctx.replStdout || ctx.stdout
|
||||
const tty = /** @type {{ isTTY?: boolean }} */ (stdin)
|
||||
if (
|
||||
!stdin ||
|
||||
typeof stdin.on !== 'function' ||
|
||||
!tty.isTTY ||
|
||||
!stdout ||
|
||||
typeof stdout.write !== 'function'
|
||||
) {
|
||||
ctx.console.error(
|
||||
'login: needs an interactive TTY (same as agent --config). Run from the shell prompt on a terminal.'
|
||||
)
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
let suspended = false
|
||||
try {
|
||||
if (createNew) await reg.call(ctx, passphrase)
|
||||
else await unlock.call(ctx, passphrase)
|
||||
} catch (err) {
|
||||
ctx.console.error(err?.message ?? String(err))
|
||||
if (typeof ctx.suspendReplForSubprocess === 'function') {
|
||||
ctx.suspendReplForSubprocess()
|
||||
suspended = true
|
||||
}
|
||||
ctx.console.log(
|
||||
createNew
|
||||
? 'Creating a new identity. Choose a passphrase (multiple words allowed); typing is hidden.'
|
||||
: 'Unlocking. Enter your passphrase (multiple words allowed); typing is hidden.'
|
||||
)
|
||||
let passphrase
|
||||
try {
|
||||
passphrase = await bareLoginPromptMaskedLine(
|
||||
ctx,
|
||||
createNew ? 'New passphrase: ' : 'Passphrase: '
|
||||
)
|
||||
} catch (e) {
|
||||
ctx.console.error((e && e.message) || String(e))
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
if (!String(passphrase || '').trim()) {
|
||||
ctx.console.error('login: empty passphrase')
|
||||
ctx.exitCode = 1
|
||||
return
|
||||
}
|
||||
try {
|
||||
if (createNew) await reg.call(ctx, String(passphrase))
|
||||
else await unlock.call(ctx, String(passphrase))
|
||||
ctx.exitCode = 0
|
||||
} catch (err) {
|
||||
ctx.console.error(err?.message ?? String(err))
|
||||
ctx.exitCode = 1
|
||||
}
|
||||
} finally {
|
||||
if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') {
|
||||
ctx.resumeReplAfterSubprocess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +426,119 @@ function bareTelnetResolveConnector(ctx) {
|
||||
return ctx.bareOsTelnetConnect(host, port, opts || {})
|
||||
}
|
||||
}
|
||||
if (typeof ctx.bareOsSyscall === 'function') {
|
||||
return function (host, port, opts) {
|
||||
const handlers = new Map()
|
||||
let closed = false
|
||||
let ended = false
|
||||
let fd = -1
|
||||
const family = opts && opts.family === 6 ? 10 : 2
|
||||
const env = ctx && ctx.vfs && typeof ctx.vfs === 'object' ? ctx.vfs.env : null
|
||||
if (env && typeof env === 'object') {
|
||||
env.BARE_OS_POSIX_SOCKET_FD_BRIDGE = '1'
|
||||
if (opts && Number(opts.connectTimeoutMs) > 0) {
|
||||
env.BARE_OS_POSIX_SOCKET_CONNECT_TIMEOUT_MS = String(Math.floor(opts.connectTimeoutMs))
|
||||
}
|
||||
}
|
||||
function emit(name, v) {
|
||||
const arr = handlers.get(name) || []
|
||||
for (const fn of arr) {
|
||||
try {
|
||||
fn(v)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
function on(name, fn) {
|
||||
const arr = handlers.get(name) || []
|
||||
arr.push(fn)
|
||||
handlers.set(name, arr)
|
||||
}
|
||||
function off(name, fn) {
|
||||
const arr = handlers.get(name) || []
|
||||
handlers.set(
|
||||
name,
|
||||
arr.filter((x) => x !== fn)
|
||||
)
|
||||
}
|
||||
async function start() {
|
||||
try {
|
||||
const s = await ctx.bareOsSyscall('socket', { domain: family, type: 1, protocol: 0 })
|
||||
if (!s || s.ok !== true || !Number.isFinite(s.fd)) {
|
||||
throw new Error((s && s.note) || 'socket bridge unavailable')
|
||||
}
|
||||
fd = Number(s.fd)
|
||||
const c = await ctx.bareOsSyscall('connect', { fd, host, port })
|
||||
if (!c || c.ok !== true) {
|
||||
throw new Error((c && (c.note || c.code)) || 'connect failed')
|
||||
}
|
||||
emit('connect')
|
||||
while (!closed) {
|
||||
const r = await ctx.bareOsSyscall('recv', { fd, len: 65536 })
|
||||
if (!r || r.ok !== true) {
|
||||
if (r && r.code === 'EAGAIN') {
|
||||
await bareTelnetSleep(10)
|
||||
continue
|
||||
}
|
||||
throw new Error((r && (r.note || r.code)) || 'recv failed')
|
||||
}
|
||||
const buf = bareTelnetToU8(r.buf)
|
||||
if (buf.length) emit('data', buf)
|
||||
if (r.eof || (Number(r.bytesReceived) === 0 && ended)) break
|
||||
await bareTelnetSleep(1)
|
||||
}
|
||||
emit('end')
|
||||
emit('close')
|
||||
} catch (e) {
|
||||
emit('error', e)
|
||||
emit('close')
|
||||
} finally {
|
||||
if (fd >= 0) {
|
||||
try {
|
||||
await ctx.bareOsSyscall('close', { fd })
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(start, 0)
|
||||
return {
|
||||
on,
|
||||
off,
|
||||
removeListener: off,
|
||||
setNoDelay() {},
|
||||
setKeepAlive() {},
|
||||
write(payload, cb) {
|
||||
const p = bareTelnetToU8(payload)
|
||||
ctx
|
||||
.bareOsSyscall('send', { fd, buf: p })
|
||||
.then((r) => {
|
||||
if (!r || r.ok !== true) throw new Error((r && (r.note || r.code)) || 'send failed')
|
||||
if (typeof cb === 'function') cb()
|
||||
})
|
||||
.catch((e) => {
|
||||
emit('error', e)
|
||||
if (typeof cb === 'function') cb(e)
|
||||
})
|
||||
return true
|
||||
},
|
||||
end() {
|
||||
ended = true
|
||||
if (fd >= 0) {
|
||||
ctx.bareOsSyscall('shutdown', { fd, how: 1 }).catch(() => {})
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
closed = true
|
||||
if (fd >= 0) {
|
||||
ctx.bareOsSyscall('shutdown', { fd, how: 2 }).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var req = null
|
||||
try {
|
||||
if (typeof globalThis.require === 'function') req = globalThis.require
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema": 2,
|
||||
"profileId": "bare-os-posix-like",
|
||||
"generatedAt": "2026-04-26T09:40:29.487Z",
|
||||
"generatedAt": "2026-04-26T10:19:42.583Z",
|
||||
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
|
||||
"commandIndex": [
|
||||
{
|
||||
@@ -16,10 +16,6 @@
|
||||
"name": "awk",
|
||||
"tier": "tier1_bin"
|
||||
},
|
||||
{
|
||||
"name": "bare-sshd",
|
||||
"tier": "tier1_bin"
|
||||
},
|
||||
{
|
||||
"name": "baresay",
|
||||
"tier": "tier1_bin"
|
||||
|
||||
+1
-1
@@ -3583,7 +3583,7 @@ async function printSessionBanner(ctx) {
|
||||
}
|
||||
}
|
||||
const defaultBanner =
|
||||
'Bare operating system — guest session (login [--new] <passphrase> to unlock)'
|
||||
'Bare operating system — guest session (run login or login --new, then passphrase at prompt)'
|
||||
if (ctx.bareOsSkipRepl) {
|
||||
bootGuestOutLine(
|
||||
ctx,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"atMs": 1777196429486,
|
||||
"atMs": 1777198782582,
|
||||
"commands": [
|
||||
"agent",
|
||||
"arch",
|
||||
|
||||
@@ -1437,7 +1437,7 @@ async function printSessionBanner(ctx) {
|
||||
}
|
||||
}
|
||||
const defaultBanner =
|
||||
'Bare operating system — guest session (login [--new] <passphrase> to unlock)'
|
||||
'Bare operating system — guest session (run login or login --new, then passphrase at prompt)'
|
||||
if (ctx.bareOsSkipRepl) {
|
||||
bootGuestOutLine(
|
||||
ctx,
|
||||
|
||||
+1
-20695
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user