@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 +3 -3
- package/cli/cli.ts +1 -0
- package/package.json +1 -1
- package/shared/ask.ts +9 -2
- package/shared/validation.ts +2 -2
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 (
|
|
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
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
|
-
|
|
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 >
|
|
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 => {
|
package/shared/validation.ts
CHANGED
|
@@ -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))
|