@localauth/cli 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/ask.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'node:child_process'
2
2
  import * as fs from 'node:fs/promises'
3
3
  import * as path from 'node:path'
4
- import { answerFields, isAskRequest, isFieldName, isReservedName } from '../shared/ask.ts'
4
+ import { answerFields, isApproval, isAskRequest, isFieldName, isReservedName, MAX_APPROVAL_DESCRIPTION } from '../shared/ask.ts'
5
5
  import { setTimeout as sleep } from 'node:timers/promises'
6
6
  import type { AnswerData, AskRequest, RunData } from '../shared/protocol.ts'
7
7
  import { isAnswer } from '../shared/validation.ts'
@@ -184,7 +184,7 @@ export async function approve(connection: Connection, options: ApproveOptions, s
184
184
  let expiresAt = Date.now() + Math.ceil(options.timeout * 60_000)
185
185
  let request: AskRequest = { title: options.title, description: options.description, fields: [{ name: 'APPROVE', label: 'Approve', secret: false, required: true }] }
186
186
  let data: RunData = { authenticatedMessages: true, kind: 'ask', approval: true, title: options.title, ask: request, expiresAt, command: '', flows: [], output: '', exitCode: null, attempt: 1, retryUntil: null }
187
- if (!isAskRequest(data.ask)) throw new Error('Invalid approval request.')
187
+ if (!isAskRequest(data.ask, MAX_APPROVAL_DESCRIPTION)) throw new Error('Invalid approval request.')
188
188
  let run = await createRun(connection, [], say, { retry: false, link: false }, data)
189
189
  let stop = new AbortController(), interrupted = 0
190
190
  let listeners = Object.entries({ SIGINT: 130, SIGTERM: 143, SIGHUP: 129 }).map(([signal, code]) => {
@@ -201,7 +201,7 @@ export async function approve(connection: Connection, options: ApproveOptions, s
201
201
  if (message.kind === 'decline') { decided = false; break }
202
202
  if (message.kind !== 'answer' || typeof message.data !== 'string' || !message.data.startsWith('a1.')) continue
203
203
  let value = await run.openMessage<AnswerData>('answer', message.data)
204
- if (isAnswer(value) && !value.declined && answerFields(value, request)?.APPROVE === 'yes') { decided = true; break }
204
+ if (isApproval(value)) { decided = true; break }
205
205
  }
206
206
  if (decided === null) await sleep(Math.min(1500, Math.max(1, expiresAt - Date.now())), undefined, { signal })
207
207
  } catch (error) { if (!signal.aborted) throw error }
package/cli/cli.ts CHANGED
@@ -35,6 +35,7 @@ Usage:
35
35
  localauth approve --title <text> [--description <text>] [--timeout <minutes>]
36
36
  Wait for the owner to allow something. They answer from their
37
37
  signed-in list with a fresh passkey check; no link is made
38
+ Description: max 8000
38
39
  Needs end-to-end encryption; exits 0 if approved, 1 otherwise
39
40
  localauth finish [<callback-url>] Hand a pasted http://localhost:... address to the waiting command
40
41
  (reads it from stdin when omitted, keeping it out of ps)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@localauth/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Finish CLI logins on a headless server from your phone, with optional passkey-based end-to-end encryption",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/shared/ask.ts CHANGED
@@ -10,10 +10,12 @@ export function isFieldName(name: unknown): name is string {
10
10
  function object(value: unknown): value is Record<string, unknown> {
11
11
  return value !== null && typeof value === 'object' && !Array.isArray(value)
12
12
  }
13
- export function isAskRequest(value: unknown): value is AskRequest {
13
+ /** An approval may have to show everything being approved, such as a whole diff. */
14
+ export const MAX_APPROVAL_DESCRIPTION = 8000
15
+ export function isAskRequest(value: unknown, maxDescription = 2000): value is AskRequest {
14
16
  if (!object(value)
15
17
  || (value.title !== undefined && (typeof value.title !== 'string' || !value.title.length || value.title.length > 120))
16
- || (value.description !== undefined && (typeof value.description !== 'string' || value.description.length > 2000))
18
+ || (value.description !== undefined && (typeof value.description !== 'string' || value.description.length > maxDescription))
17
19
  || !Array.isArray(value.fields) || value.fields.length < 1 || value.fields.length > 10) return false
18
20
  let names = new Set<string>()
19
21
  return value.fields.every(field => {
@@ -44,6 +46,11 @@ export function answerFields(answer: unknown, request: AskRequest, legacy = fals
44
46
  return Object.fromEntries(request.fields.filter(field => Object.hasOwn(fields, field.name)).map(field => [field.name, fields[field.name]]))
45
47
  }
46
48
 
49
+ /** Whether an answer approves. Only the answer matters here: an approval's description can be longer than an ask's. */
50
+ export function isApproval(answer: unknown): boolean {
51
+ return answerFields(answer, { fields: [{ name: 'APPROVE', label: 'Approve', secret: false, required: true }] })?.APPROVE === 'yes'
52
+ }
53
+
47
54
  /** Plain text segments and HTTP(S) URLs only. JSX renders both labels and segments as escaped text. */
48
55
  export function descriptionParts(text: string): { text: string; href?: string }[] {
49
56
  return text.split(/(https?:\/\/[^\s<>"']+)/g).filter(Boolean).map(text => {
@@ -1,5 +1,5 @@
1
1
  import type { InputData, CallbackData, RunData } from './protocol.ts'
2
- import { isAskRequest } from './ask.ts'
2
+ import { isAskRequest, MAX_APPROVAL_DESCRIPTION } from './ask.ts'
3
3
  export { isAnswer } from './ask.ts'
4
4
  export function isCallback(value: unknown): value is CallbackData {
5
5
  let v = value as CallbackData | null
@@ -13,7 +13,7 @@ export function isRunData(value: unknown): value is RunData {
13
13
  && (v.approval === undefined || (v.approval === true && v.kind === 'ask'))
14
14
  && (v.inputEnabled === undefined || typeof v.inputEnabled === 'boolean')
15
15
  && (v.secret === undefined || typeof v.secret === 'boolean')
16
- && (v.ask === undefined || (v.kind === 'ask' && isAskRequest(v.ask)))
16
+ && (v.ask === undefined || (v.kind === 'ask' && isAskRequest(v.ask, v.approval ? MAX_APPROVAL_DESCRIPTION : undefined)))
17
17
  && (v.answerNote === undefined || (typeof v.answerNote === 'string' && v.answerNote.length <= 240))
18
18
  && (v.expiresAt === undefined || (Number.isSafeInteger(v.expiresAt) && v.expiresAt >= 0))
19
19
  && (v.machineName === undefined || (typeof v.machineName === 'string' && v.machineName.length <= 253))