transloadit 4.12.0 → 4.13.0
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/README.md +231 -23
- package/dist/Transloadit.d.ts +55 -1
- package/dist/Transloadit.d.ts.map +1 -1
- package/dist/Transloadit.js +143 -2
- package/dist/Transloadit.js.map +1 -1
- package/dist/alphalib/types/assemblyStatus.d.ts +574 -198
- package/dist/alphalib/types/assemblyStatus.d.ts.map +1 -1
- package/dist/alphalib/types/assemblyStatus.js +17 -0
- package/dist/alphalib/types/assemblyStatus.js.map +1 -1
- package/dist/alphalib/types/robots/_index.d.ts +205 -93
- package/dist/alphalib/types/robots/_index.d.ts.map +1 -1
- package/dist/alphalib/types/robots/_index.js +8 -0
- package/dist/alphalib/types/robots/_index.js.map +1 -1
- package/dist/alphalib/types/robots/_instructions-primitives.d.ts +4 -4
- package/dist/alphalib/types/robots/_instructions-primitives.d.ts.map +1 -1
- package/dist/alphalib/types/robots/_instructions-primitives.js +2 -0
- package/dist/alphalib/types/robots/_instructions-primitives.js.map +1 -1
- package/dist/alphalib/types/robots/file-serve.d.ts +12 -0
- package/dist/alphalib/types/robots/file-serve.d.ts.map +1 -1
- package/dist/alphalib/types/robots/file-serve.js +9 -3
- package/dist/alphalib/types/robots/file-serve.js.map +1 -1
- package/dist/alphalib/types/robots/transloadit-import.d.ts +195 -0
- package/dist/alphalib/types/robots/transloadit-import.d.ts.map +1 -0
- package/dist/alphalib/types/robots/transloadit-import.js +108 -0
- package/dist/alphalib/types/robots/transloadit-import.js.map +1 -0
- package/dist/alphalib/types/robots/transloadit-store.d.ts +441 -0
- package/dist/alphalib/types/robots/transloadit-store.d.ts.map +1 -0
- package/dist/alphalib/types/robots/transloadit-store.js +69 -0
- package/dist/alphalib/types/robots/transloadit-store.js.map +1 -0
- package/dist/alphalib/types/robots/video-ondemand.d.ts +8 -8
- package/dist/alphalib/types/storageAsset.d.ts +226 -0
- package/dist/alphalib/types/storageAsset.d.ts.map +1 -0
- package/dist/alphalib/types/storageAsset.js +67 -0
- package/dist/alphalib/types/storageAsset.js.map +1 -0
- package/dist/alphalib/types/template.d.ts.map +1 -1
- package/dist/alphalib/types/template.js +5 -2
- package/dist/alphalib/types/template.js.map +1 -1
- package/dist/alphalib/types/templateCredential.d.ts +2 -2
- package/dist/cli/commands/BaseCommand.d.ts +3 -1
- package/dist/cli/commands/BaseCommand.d.ts.map +1 -1
- package/dist/cli/commands/BaseCommand.js +3 -2
- package/dist/cli/commands/BaseCommand.js.map +1 -1
- package/dist/cli/commands/assemblies.d.ts.map +1 -1
- package/dist/cli/commands/assemblies.js +9 -4
- package/dist/cli/commands/assemblies.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +6 -0
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +21 -2
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/image.d.ts +15 -0
- package/dist/cli/commands/image.d.ts.map +1 -0
- package/dist/cli/commands/image.js +234 -0
- package/dist/cli/commands/image.js.map +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +15 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/login.d.ts +24 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +275 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/storage.d.ts +58 -0
- package/dist/cli/commands/storage.d.ts.map +1 -0
- package/dist/cli/commands/storage.js +589 -0
- package/dist/cli/commands/storage.js.map +1 -0
- package/dist/cli/commands/templates.d.ts.map +1 -1
- package/dist/cli/commands/templates.js +10 -6
- package/dist/cli/commands/templates.js.map +1 -1
- package/dist/cli/deviceLogin.d.ts +11 -0
- package/dist/cli/deviceLogin.d.ts.map +1 -0
- package/dist/cli/deviceLogin.js +187 -0
- package/dist/cli/deviceLogin.js.map +1 -0
- package/dist/cli/helpers.d.ts +23 -1
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/helpers.js +159 -25
- package/dist/cli/helpers.js.map +1 -1
- package/dist/cli/secretInput.d.ts +3 -0
- package/dist/cli/secretInput.d.ts.map +1 -0
- package/dist/cli/secretInput.js +11 -0
- package/dist/cli/secretInput.js.map +1 -0
- package/dist/cli/storageCatalog.d.ts +19 -0
- package/dist/cli/storageCatalog.d.ts.map +1 -0
- package/dist/cli/storageCatalog.js +129 -0
- package/dist/cli/storageCatalog.js.map +1 -0
- package/dist/cli/storagePublic.d.ts +3 -0
- package/dist/cli/storagePublic.d.ts.map +1 -0
- package/dist/cli/storagePublic.js +15 -0
- package/dist/cli/storagePublic.js.map +1 -0
- package/dist/cli/storageReceipts.d.ts +67 -0
- package/dist/cli/storageReceipts.d.ts.map +1 -0
- package/dist/cli/storageReceipts.js +242 -0
- package/dist/cli/storageReceipts.js.map +1 -0
- package/dist/cli/storageSnippets.d.ts +11 -0
- package/dist/cli/storageSnippets.d.ts.map +1 -0
- package/dist/cli/storageSnippets.js +89 -0
- package/dist/cli/storageSnippets.js.map +1 -0
- package/dist/storageImage.d.ts +40 -0
- package/dist/storageImage.d.ts.map +1 -0
- package/dist/storageImage.js +181 -0
- package/dist/storageImage.js.map +1 -0
- package/dist/storagePublicPrefixes.d.ts +62 -0
- package/dist/storagePublicPrefixes.d.ts.map +1 -0
- package/dist/storagePublicPrefixes.js +35 -0
- package/dist/storagePublicPrefixes.js.map +1 -0
- package/dist/storageResults.d.ts +31 -0
- package/dist/storageResults.d.ts.map +1 -0
- package/dist/storageResults.js +79 -0
- package/dist/storageResults.js.map +1 -0
- package/package.json +8 -3
- package/src/Transloadit.ts +251 -4
- package/src/alphalib/types/assemblyStatus.ts +17 -0
- package/src/alphalib/types/robots/_index.ts +28 -0
- package/src/alphalib/types/robots/_instructions-primitives.ts +2 -0
- package/src/alphalib/types/robots/file-serve.ts +11 -3
- package/src/alphalib/types/robots/transloadit-import.ts +154 -0
- package/src/alphalib/types/robots/transloadit-store.ts +98 -0
- package/src/alphalib/types/storageAsset.ts +88 -0
- package/src/alphalib/types/template.ts +3 -0
- package/src/cli/commands/BaseCommand.ts +4 -2
- package/src/cli/commands/assemblies.ts +13 -5
- package/src/cli/commands/auth.ts +26 -3
- package/src/cli/commands/image.ts +282 -0
- package/src/cli/commands/index.ts +27 -1
- package/src/cli/commands/login.ts +334 -0
- package/src/cli/commands/storage.ts +733 -0
- package/src/cli/commands/templates.ts +14 -7
- package/src/cli/deviceLogin.ts +226 -0
- package/src/cli/helpers.ts +204 -27
- package/src/cli/secretInput.ts +12 -0
- package/src/cli/storageCatalog.ts +192 -0
- package/src/cli/storagePublic.ts +15 -0
- package/src/cli/storageReceipts.ts +307 -0
- package/src/cli/storageSnippets.ts +96 -0
- package/src/storageImage.ts +266 -0
- package/src/storagePublicPrefixes.ts +39 -0
- package/src/storageResults.ts +119 -0
|
@@ -152,9 +152,11 @@ async function deleteTemplates(
|
|
|
152
152
|
// Export with `delete` alias for external consumers
|
|
153
153
|
export { deleteTemplates as delete }
|
|
154
154
|
|
|
155
|
-
const TemplateIdSchema = z
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
const TemplateIdSchema = z
|
|
156
|
+
.object({
|
|
157
|
+
id: z.string(),
|
|
158
|
+
})
|
|
159
|
+
.passthrough()
|
|
158
160
|
|
|
159
161
|
const INCLUDE_BUILTIN_VALUES = ['all', 'latest', 'exclusively-all', 'exclusively-latest'] as const
|
|
160
162
|
|
|
@@ -190,11 +192,11 @@ async function list(
|
|
|
190
192
|
const parsed = TemplateIdSchema.safeParse(item)
|
|
191
193
|
if (!parsed.success) continue
|
|
192
194
|
|
|
193
|
-
let template
|
|
195
|
+
let template = parsed.data
|
|
194
196
|
if (includeContent) {
|
|
195
197
|
try {
|
|
196
198
|
const full = await client.getTemplate(parsed.data.id)
|
|
197
|
-
template = { ...
|
|
199
|
+
template = { ...template, content: full.content }
|
|
198
200
|
} catch (err) {
|
|
199
201
|
output.error(formatAPIError(err))
|
|
200
202
|
}
|
|
@@ -203,8 +205,13 @@ async function list(
|
|
|
203
205
|
if (fields == null) {
|
|
204
206
|
output.print(parsed.data.id, template)
|
|
205
207
|
} else {
|
|
206
|
-
const
|
|
207
|
-
output.print(
|
|
208
|
+
const selected = fields.filter((field) => Object.hasOwn(template, field))
|
|
209
|
+
output.print(
|
|
210
|
+
fields
|
|
211
|
+
.map((field) => (Object.hasOwn(template, field) ? template[field] : undefined))
|
|
212
|
+
.join(' '),
|
|
213
|
+
Object.fromEntries(selected.map((field) => [field, template[field]])),
|
|
214
|
+
)
|
|
208
215
|
}
|
|
209
216
|
}
|
|
210
217
|
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type { CliKeySecretCredentials } from './helpers.ts'
|
|
2
|
+
import type { IOutputCtl } from './OutputCtl.ts'
|
|
3
|
+
|
|
4
|
+
import { hostname } from 'node:os'
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises'
|
|
6
|
+
|
|
7
|
+
import { execa } from 'execa'
|
|
8
|
+
import got from 'got'
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
|
|
11
|
+
import { cliSignatureAlgorithmSchema } from './helpers.ts'
|
|
12
|
+
|
|
13
|
+
const deviceSchema = z.object({
|
|
14
|
+
ok: z.literal('CLI_DEVICE_AUTHORIZATION_CREATED'),
|
|
15
|
+
device_code: z.string().min(1).max(4096),
|
|
16
|
+
user_code: z
|
|
17
|
+
.string()
|
|
18
|
+
.regex(/^[BCDFGHJKLMNPQRSTVWXZ23456789]{4}-[BCDFGHJKLMNPQRSTVWXZ23456789]{4}$/),
|
|
19
|
+
verification_url: z.string().url(),
|
|
20
|
+
// The agreed device contract is bounded to 15 minutes; reject incompatible server responses.
|
|
21
|
+
expires_in: z.number().int().positive().max(900),
|
|
22
|
+
interval: z.number().int().positive().max(900),
|
|
23
|
+
})
|
|
24
|
+
const authorizedSchema = z.object({
|
|
25
|
+
ok: z.literal('CLI_DEVICE_AUTHORIZED'),
|
|
26
|
+
signature_algo: cliSignatureAlgorithmSchema.nullable(),
|
|
27
|
+
workspace: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/),
|
|
28
|
+
auth_key: z
|
|
29
|
+
.string()
|
|
30
|
+
.min(1)
|
|
31
|
+
.max(4096)
|
|
32
|
+
.regex(/^[^\r\n\0]+$/),
|
|
33
|
+
auth_secret: z
|
|
34
|
+
.string()
|
|
35
|
+
.min(1)
|
|
36
|
+
.max(4096)
|
|
37
|
+
.regex(/^[^\r\n\0]+$/),
|
|
38
|
+
auth_key_id: z
|
|
39
|
+
.string()
|
|
40
|
+
.regex(/^[a-zA-Z0-9_-]{1,128}$/)
|
|
41
|
+
.optional(),
|
|
42
|
+
description: z
|
|
43
|
+
.string()
|
|
44
|
+
.max(512)
|
|
45
|
+
.regex(/^[^\r\n\0]*$/)
|
|
46
|
+
.optional(),
|
|
47
|
+
})
|
|
48
|
+
const pendingSchema = z.object({
|
|
49
|
+
ok: z.literal('CLI_DEVICE_AUTHORIZATION_PENDING'),
|
|
50
|
+
expires_in: z.number().int().nonnegative(),
|
|
51
|
+
})
|
|
52
|
+
const errorSchema = z.object({ error: z.string() })
|
|
53
|
+
const expiredMessage =
|
|
54
|
+
'Device authorization expired or was already used. Run transloadit auth login again.'
|
|
55
|
+
|
|
56
|
+
/** Browser-approved credentials; the one-time device code never leaves this module. */
|
|
57
|
+
export interface DeviceLoginCredentials extends CliKeySecretCredentials {
|
|
58
|
+
workspace: string
|
|
59
|
+
authKeyId?: string
|
|
60
|
+
description?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Obtain a combined Auth Key using the API's single-use device authorization contract. */
|
|
64
|
+
export async function deviceLogin(
|
|
65
|
+
endpoint: string,
|
|
66
|
+
output: IOutputCtl,
|
|
67
|
+
noBrowser: boolean,
|
|
68
|
+
): Promise<DeviceLoginCredentials> {
|
|
69
|
+
const cancellation = new AbortController()
|
|
70
|
+
const cancel = (): void => cancellation.abort()
|
|
71
|
+
process.once('SIGINT', cancel)
|
|
72
|
+
let expired: AbortSignal | undefined
|
|
73
|
+
let heartbeat: ReturnType<typeof setInterval> | undefined
|
|
74
|
+
try {
|
|
75
|
+
const response = await got
|
|
76
|
+
.post(`${endpoint}/cli/device_authorizations`, {
|
|
77
|
+
form: { client: 'transloadit-cli', hostname: hostname() },
|
|
78
|
+
responseType: 'json',
|
|
79
|
+
retry: { limit: 0 },
|
|
80
|
+
followRedirect: false,
|
|
81
|
+
timeout: { request: 10_000 },
|
|
82
|
+
signal: cancellation.signal,
|
|
83
|
+
})
|
|
84
|
+
.catch((cause: unknown) => {
|
|
85
|
+
throw new Error(
|
|
86
|
+
'Could not start browser login. Check the API endpoint and retry; use auth login --stdin for an existing Auth Key.',
|
|
87
|
+
{ cause },
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
const parsed = deviceSchema.safeParse(response.body)
|
|
91
|
+
if (!parsed.success)
|
|
92
|
+
throw new Error('The API returned an invalid device authorization; nothing was saved')
|
|
93
|
+
const device = parsed.data
|
|
94
|
+
const target = new URL(device.verification_url)
|
|
95
|
+
if (
|
|
96
|
+
target.username ||
|
|
97
|
+
target.password ||
|
|
98
|
+
target.hash ||
|
|
99
|
+
(target.protocol !== 'https:' &&
|
|
100
|
+
!(
|
|
101
|
+
target.protocol === 'http:' &&
|
|
102
|
+
['localhost', '127.0.0.1', '[::1]'].includes(target.hostname)
|
|
103
|
+
))
|
|
104
|
+
)
|
|
105
|
+
throw new Error('The API returned an unsafe verification URL; nothing was opened or saved')
|
|
106
|
+
expired = AbortSignal.timeout(device.expires_in * 1000)
|
|
107
|
+
const signal = AbortSignal.any([cancellation.signal, expired])
|
|
108
|
+
const deadline = performance.now() + device.expires_in * 1000
|
|
109
|
+
heartbeat = setInterval(() => {
|
|
110
|
+
const minutes = Math.ceil((deadline - performance.now()) / 60_000)
|
|
111
|
+
if (signal.aborted || minutes <= 0) return
|
|
112
|
+
output.notice(
|
|
113
|
+
`Still waiting for approval, ${minutes} minute${minutes === 1 ? '' : 's'} left. Use the verification URL printed above.`,
|
|
114
|
+
)
|
|
115
|
+
}, 60_000)
|
|
116
|
+
heartbeat.unref()
|
|
117
|
+
output.print(`Enter code ${device.user_code} at ${target.href}`, {
|
|
118
|
+
user_code: device.user_code,
|
|
119
|
+
verification_url: target.href,
|
|
120
|
+
})
|
|
121
|
+
const opener =
|
|
122
|
+
process.platform === 'darwin'
|
|
123
|
+
? 'open'
|
|
124
|
+
: process.platform === 'linux'
|
|
125
|
+
? 'xdg-open'
|
|
126
|
+
: process.platform === 'win32'
|
|
127
|
+
? 'cmd'
|
|
128
|
+
: undefined
|
|
129
|
+
// On platforms without an opener, the printed URL is the manual approval path.
|
|
130
|
+
if (!noBrowser && opener !== undefined) {
|
|
131
|
+
// xdg-open may live as long as the browser; launching it must not delay or cancel polling.
|
|
132
|
+
// cmd parses metacharacters even with shell:false. A quoted, single-pass env expansion
|
|
133
|
+
// keeps &, %, and ! in the URL out of command syntax; disable delayed expansion/AutoRun.
|
|
134
|
+
const browser = execa(
|
|
135
|
+
opener,
|
|
136
|
+
opener === 'cmd'
|
|
137
|
+
? ['/d', '/v:off', '/c', 'start', '""', '"%TRANSLOADIT_BROWSER_URL%"']
|
|
138
|
+
: [target.href],
|
|
139
|
+
{
|
|
140
|
+
shell: false,
|
|
141
|
+
...(opener === 'cmd'
|
|
142
|
+
? { windowsVerbatimArguments: true, env: { TRANSLOADIT_BROWSER_URL: target.href } }
|
|
143
|
+
: {}),
|
|
144
|
+
stdio: 'ignore',
|
|
145
|
+
detached: true,
|
|
146
|
+
cleanup: false,
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
browser.unref()
|
|
150
|
+
void browser.catch(() => {
|
|
151
|
+
// A wrapper's later failure cannot prove whether its page opened or invalidate login.
|
|
152
|
+
output.warn(
|
|
153
|
+
'The browser opener reported an error. If the page did not open, use the verification URL printed above.',
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
let intervalMs = device.interval * 1000
|
|
158
|
+
while (true) {
|
|
159
|
+
await delay(intervalMs, undefined, { signal })
|
|
160
|
+
const token = await got
|
|
161
|
+
.post(`${endpoint}/cli/device_authorizations/token`, {
|
|
162
|
+
form: { device_code: device.device_code },
|
|
163
|
+
responseType: 'json',
|
|
164
|
+
retry: { limit: 0 },
|
|
165
|
+
followRedirect: false,
|
|
166
|
+
throwHttpErrors: false,
|
|
167
|
+
timeout: { request: 10_000 },
|
|
168
|
+
signal,
|
|
169
|
+
})
|
|
170
|
+
.catch((cause: unknown) => {
|
|
171
|
+
throw new Error(
|
|
172
|
+
'Could not finish browser login. Check connectivity and run transloadit auth login again.',
|
|
173
|
+
{ cause },
|
|
174
|
+
)
|
|
175
|
+
})
|
|
176
|
+
const error = errorSchema.safeParse(token.body)
|
|
177
|
+
if (
|
|
178
|
+
token.statusCode === 403 &&
|
|
179
|
+
error.success &&
|
|
180
|
+
error.data.error === 'CLI_DEVICE_AUTHORIZATION_DENIED'
|
|
181
|
+
)
|
|
182
|
+
throw new Error('The login was denied in the browser.')
|
|
183
|
+
if (token.statusCode === 429 || (error.success && error.data.error === 'slow_down')) {
|
|
184
|
+
const seconds = Number(token.headers['retry-after'])
|
|
185
|
+
// Longer waits are pointless after the authorization deadline and can overflow Node's
|
|
186
|
+
// timer range into 1 ms, accidentally hammering an already rate-limited API.
|
|
187
|
+
intervalMs = Math.min(
|
|
188
|
+
device.expires_in * 1000,
|
|
189
|
+
Math.max(intervalMs + 5000, Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : 0),
|
|
190
|
+
)
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
if (error.success && error.data.error === 'CLI_DEVICE_AUTHORIZATION_NOT_FOUND')
|
|
194
|
+
throw new Error(expiredMessage)
|
|
195
|
+
if (token.statusCode < 200 || token.statusCode >= 300)
|
|
196
|
+
throw new Error(
|
|
197
|
+
'Browser login was refused. Run transloadit auth login again; nothing was saved.',
|
|
198
|
+
)
|
|
199
|
+
const pending = pendingSchema.safeParse(token.body)
|
|
200
|
+
if (pending.success) {
|
|
201
|
+
if (pending.data.expires_in === 0) throw new Error(expiredMessage)
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
const authorized = authorizedSchema.safeParse(token.body)
|
|
205
|
+
if (!authorized.success)
|
|
206
|
+
throw new Error('The API returned an invalid login result; nothing was saved')
|
|
207
|
+
signal.throwIfAborted()
|
|
208
|
+
return {
|
|
209
|
+
authKey: authorized.data.auth_key,
|
|
210
|
+
authSecret: authorized.data.auth_secret,
|
|
211
|
+
signatureAlgorithm: authorized.data.signature_algo ?? undefined,
|
|
212
|
+
workspace: authorized.data.workspace,
|
|
213
|
+
authKeyId: authorized.data.auth_key_id,
|
|
214
|
+
description: authorized.data.description,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (cause) {
|
|
218
|
+
if (cancellation.signal.aborted)
|
|
219
|
+
throw new Error('Login canceled; nothing was saved.', { cause })
|
|
220
|
+
if (expired?.aborted) throw new Error(expiredMessage, { cause })
|
|
221
|
+
throw cause
|
|
222
|
+
} finally {
|
|
223
|
+
clearInterval(heartbeat)
|
|
224
|
+
process.off('SIGINT', cancel)
|
|
225
|
+
}
|
|
226
|
+
}
|
package/src/cli/helpers.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import type { Readable } from 'node:stream'
|
|
2
2
|
|
|
3
|
+
import type { IOutputCtl } from './OutputCtl.ts'
|
|
4
|
+
|
|
3
5
|
import fs from 'node:fs'
|
|
4
6
|
import fsp from 'node:fs/promises'
|
|
5
|
-
import { homedir } from 'node:os'
|
|
7
|
+
import { homedir, userInfo } from 'node:os'
|
|
6
8
|
import path from 'node:path'
|
|
7
9
|
|
|
8
10
|
import { parse as parseDotenv } from 'dotenv'
|
|
11
|
+
import { z } from 'zod'
|
|
9
12
|
|
|
10
13
|
import { isAPIError } from './types.ts'
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
/** API signing algorithms supported by CLI credentials and device authorization. */
|
|
16
|
+
export const cliSignatureAlgorithmSchema = z.enum(['sha1', 'sha256', 'sha384', 'sha512'])
|
|
17
|
+
export type CliKeySecretCredentials = {
|
|
18
|
+
authKey: string
|
|
19
|
+
authSecret: string
|
|
20
|
+
signatureAlgorithm?: z.infer<typeof cliSignatureAlgorithmSchema>
|
|
21
|
+
}
|
|
13
22
|
export type CliAuthToken = { authToken: string }
|
|
14
23
|
export type CliAuth = CliKeySecretCredentials | CliAuthToken
|
|
15
24
|
type CliEnvSource = {
|
|
@@ -20,7 +29,9 @@ type CliEnvSource = {
|
|
|
20
29
|
let loadedProjectDotenvPath: string | undefined
|
|
21
30
|
let projectDotenvInjectedValues: Record<string, string> | undefined
|
|
22
31
|
let projectDotenvPreviousValues: Record<string, string | undefined> | undefined
|
|
23
|
-
let shellEnvBeforeProjectDotenv:
|
|
32
|
+
let shellEnvBeforeProjectDotenv:
|
|
33
|
+
| { values: Record<string, string | undefined>; homeDirectory?: string }
|
|
34
|
+
| undefined
|
|
24
35
|
|
|
25
36
|
type LoadCliEnvSourcesResult = {
|
|
26
37
|
loadError?: string
|
|
@@ -30,8 +41,17 @@ type LoadCliEnvSourcesResult = {
|
|
|
30
41
|
|
|
31
42
|
export type ResolvedCliConfig = {
|
|
32
43
|
auth?: CliAuth
|
|
44
|
+
authSource?: string
|
|
45
|
+
authWorkspace?: string
|
|
46
|
+
authWorkspaceVerified?: boolean
|
|
33
47
|
credentials?: CliKeySecretCredentials
|
|
48
|
+
credentialsSource?: string
|
|
34
49
|
credentialsEndpoint?: string
|
|
50
|
+
credentialsWorkspace?: string
|
|
51
|
+
credentialsWorkspaceVerified?: boolean
|
|
52
|
+
credentialsAuthKeyId?: string
|
|
53
|
+
credentialsDescription?: string
|
|
54
|
+
credentialsLoginMethod?: string
|
|
35
55
|
endpoint?: string
|
|
36
56
|
loadError?: string
|
|
37
57
|
}
|
|
@@ -41,13 +61,38 @@ function normalizeEnvValue(value: string | undefined): string | undefined {
|
|
|
41
61
|
return trimmed ? trimmed : undefined
|
|
42
62
|
}
|
|
43
63
|
|
|
44
|
-
function
|
|
45
|
-
|
|
64
|
+
function credentialHomeDirectory(): string {
|
|
65
|
+
// Node trusts HOME verbatim; an empty or relative default must never put secrets in a repo.
|
|
66
|
+
try {
|
|
67
|
+
const home = homedir()
|
|
68
|
+
if (path.isAbsolute(home)) return home
|
|
69
|
+
const fallback = userInfo().homedir
|
|
70
|
+
if (path.isAbsolute(fallback)) return fallback
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'Cannot determine a safe home directory. Set TRANSLOADIT_CREDENTIALS_FILE to an explicit path.',
|
|
74
|
+
{ cause: error },
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
throw new Error(
|
|
78
|
+
'Cannot determine a safe home directory. Set TRANSLOADIT_CREDENTIALS_FILE to an explicit path.',
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Login and its env scaffold accept only a shell path override; ordinary reads retain merged lookup. */
|
|
83
|
+
export function getConfiguredCredentialsFilePath(source: 'shell' | 'merged' = 'merged'): string {
|
|
84
|
+
const values = source === 'shell' ? getShellEnvValues() : process.env
|
|
85
|
+
const configuredPath = normalizeEnvValue(values.TRANSLOADIT_CREDENTIALS_FILE)
|
|
46
86
|
if (configuredPath != null) {
|
|
47
87
|
return path.resolve(configuredPath)
|
|
48
88
|
}
|
|
49
89
|
|
|
50
|
-
|
|
90
|
+
// HOME/USERPROFILE from project dotenv must not redirect the login's default destination.
|
|
91
|
+
const shellHome =
|
|
92
|
+
source === 'shell' && loadedProjectDotenvPath === getProjectDotenvPath()
|
|
93
|
+
? shellEnvBeforeProjectDotenv?.homeDirectory
|
|
94
|
+
: undefined
|
|
95
|
+
return path.join(shellHome ?? credentialHomeDirectory(), '.transloadit', 'credentials')
|
|
51
96
|
}
|
|
52
97
|
|
|
53
98
|
function getProjectDotenvPath(): string {
|
|
@@ -112,7 +157,15 @@ export function loadProjectDotenvIntoProcessEnv(): string | undefined {
|
|
|
112
157
|
const projectDotenvPath = getProjectDotenvPath()
|
|
113
158
|
if (loadedProjectDotenvPath !== projectDotenvPath) {
|
|
114
159
|
restoreProjectDotenvFromProcessEnv()
|
|
115
|
-
shellEnvBeforeProjectDotenv = {
|
|
160
|
+
shellEnvBeforeProjectDotenv = {
|
|
161
|
+
values: { ...process.env },
|
|
162
|
+
// An explicit file makes OS home discovery unnecessary (e.g. an unmapped container UID).
|
|
163
|
+
// Otherwise capture it before dotenv can replace HOME and redirect a newly saved login.
|
|
164
|
+
homeDirectory:
|
|
165
|
+
normalizeEnvValue(process.env.TRANSLOADIT_CREDENTIALS_FILE) == null
|
|
166
|
+
? credentialHomeDirectory()
|
|
167
|
+
: undefined,
|
|
168
|
+
}
|
|
116
169
|
loadedProjectDotenvPath = projectDotenvPath
|
|
117
170
|
}
|
|
118
171
|
|
|
@@ -144,7 +197,7 @@ export function loadProjectDotenvIntoProcessEnv(): string | undefined {
|
|
|
144
197
|
|
|
145
198
|
function getShellEnvValues(): Record<string, string | undefined> {
|
|
146
199
|
if (loadedProjectDotenvPath === getProjectDotenvPath() && shellEnvBeforeProjectDotenv != null) {
|
|
147
|
-
return shellEnvBeforeProjectDotenv
|
|
200
|
+
return shellEnvBeforeProjectDotenv.values
|
|
148
201
|
}
|
|
149
202
|
|
|
150
203
|
return { ...process.env }
|
|
@@ -214,12 +267,23 @@ function getSourceValue(source: CliEnvSource, keys: string[]): string | undefine
|
|
|
214
267
|
return undefined
|
|
215
268
|
}
|
|
216
269
|
|
|
217
|
-
function getSourceCredentials(
|
|
270
|
+
function getSourceCredentials(
|
|
271
|
+
source: CliEnvSource,
|
|
272
|
+
): CliKeySecretCredentials | { loadError: string } | undefined {
|
|
218
273
|
const authKey = getSourceValue(source, ['TRANSLOADIT_KEY', 'TRANSLOADIT_AUTH_KEY'])
|
|
219
274
|
const authSecret = getSourceValue(source, ['TRANSLOADIT_SECRET', 'TRANSLOADIT_AUTH_SECRET'])
|
|
220
275
|
if (authKey == null || authSecret == null) return undefined
|
|
221
276
|
|
|
222
|
-
|
|
277
|
+
const algorithm = cliSignatureAlgorithmSchema
|
|
278
|
+
.optional()
|
|
279
|
+
.safeParse(getSourceValue(source, ['TRANSLOADIT_SIGNATURE_ALGORITHM']))
|
|
280
|
+
if (!algorithm.success)
|
|
281
|
+
return { loadError: 'Unsupported TRANSLOADIT_SIGNATURE_ALGORITHM in CLI credentials' }
|
|
282
|
+
return {
|
|
283
|
+
authKey,
|
|
284
|
+
authSecret,
|
|
285
|
+
...(algorithm.data === undefined ? {} : { signatureAlgorithm: algorithm.data }),
|
|
286
|
+
}
|
|
223
287
|
}
|
|
224
288
|
|
|
225
289
|
function getSourceAuthToken(source: CliEnvSource): CliAuthToken | undefined {
|
|
@@ -240,31 +304,127 @@ function resolveEndpointForSource(
|
|
|
240
304
|
return getSourceValue(source, ['TRANSLOADIT_ENDPOINT'])
|
|
241
305
|
}
|
|
242
306
|
|
|
243
|
-
|
|
307
|
+
function isSavedLoginSource(source: CliEnvSource): boolean {
|
|
308
|
+
return (
|
|
309
|
+
source.name === 'credentialsFile' &&
|
|
310
|
+
getConfiguredCredentialsFilePath() === getConfiguredCredentialsFilePath('shell')
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function hasVerifiedWorkspace(source: CliEnvSource, shell: CliEnvSource): boolean {
|
|
315
|
+
// A project-selected file or changed endpoint cannot inherit the login-time ownership proof.
|
|
316
|
+
return (
|
|
317
|
+
isSavedLoginSource(source) &&
|
|
318
|
+
getSourceValue(source, ['TRANSLOADIT_WORKSPACE_VERIFIED']) === 'true' &&
|
|
319
|
+
resolveEndpointForSource(source, shell) === getSourceValue(source, ['TRANSLOADIT_ENDPOINT'])
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function credentialSourceName(source: CliEnvSource, shell: CliEnvSource, auth: CliAuth): string {
|
|
324
|
+
if (source.name === 'credentialsFile')
|
|
325
|
+
return isSavedLoginSource(source) ? 'saved login' : 'project-selected credentials file'
|
|
326
|
+
const fields =
|
|
327
|
+
'authToken' in auth
|
|
328
|
+
? [['TRANSLOADIT_AUTH_TOKEN']]
|
|
329
|
+
: [
|
|
330
|
+
['TRANSLOADIT_KEY', 'TRANSLOADIT_AUTH_KEY'],
|
|
331
|
+
['TRANSLOADIT_SECRET', 'TRANSLOADIT_AUTH_SECRET'],
|
|
332
|
+
]
|
|
333
|
+
const fromShell = fields.filter((aliases) => {
|
|
334
|
+
const name = aliases.find((name) => normalizeEnvValue(source.values[name]) !== undefined)
|
|
335
|
+
return (
|
|
336
|
+
name !== undefined &&
|
|
337
|
+
normalizeEnvValue(shell.values[name]) === normalizeEnvValue(source.values[name])
|
|
338
|
+
)
|
|
339
|
+
}).length
|
|
340
|
+
if (fromShell === fields.length) return 'shell environment'
|
|
341
|
+
return fromShell === 0 ? 'project .env' : 'shell environment + project .env'
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Quotes a value in the CLI's copyable POSIX-shell commands without expanding user input. */
|
|
345
|
+
export function quoteCliArgument(value: string): string {
|
|
346
|
+
return /^[a-zA-Z0-9_./-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Names credential overrides without exposing values or claiming verified ownership. */
|
|
350
|
+
export function noticeCliCredentialSource(
|
|
351
|
+
config: ResolvedCliConfig,
|
|
352
|
+
output: Pick<IOutputCtl, 'notice'> | undefined,
|
|
353
|
+
kind: 'auth' | 'credentials' = 'auth',
|
|
354
|
+
): void {
|
|
355
|
+
const source = kind === 'auth' ? config.authSource : config.credentialsSource
|
|
356
|
+
if (source === undefined || source === 'saved login') return
|
|
357
|
+
const workspace = kind === 'auth' ? config.authWorkspace : config.credentialsWorkspace
|
|
358
|
+
const label =
|
|
359
|
+
workspace !== undefined && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(workspace)
|
|
360
|
+
? `workspace declared as ${workspace}`
|
|
361
|
+
: 'workspace not declared'
|
|
362
|
+
output?.notice(
|
|
363
|
+
`Credentials: ${source} (${label}). This override takes precedence over the saved login.`,
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Resolve one already-read saved credential snapshot, without consulting the environment again. */
|
|
368
|
+
export function parseCliLoginSnapshot(contents: string): ResolvedCliConfig {
|
|
369
|
+
return resolveLoginSource({ name: 'credentialsFile', values: parseDotenv(contents) })
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function resolveLoginSource(source: CliEnvSource): ResolvedCliConfig {
|
|
373
|
+
const credentials = getSourceCredentials(source)
|
|
374
|
+
if (credentials !== undefined && 'loadError' in credentials) return credentials
|
|
375
|
+
const endpoint = getSourceValue(source, ['TRANSLOADIT_ENDPOINT'])
|
|
376
|
+
return {
|
|
377
|
+
auth: credentials,
|
|
378
|
+
authSource: 'saved login',
|
|
379
|
+
authWorkspace: getSourceValue(source, ['TRANSLOADIT_WORKSPACE']),
|
|
380
|
+
authWorkspaceVerified: getSourceValue(source, ['TRANSLOADIT_WORKSPACE_VERIFIED']) === 'true',
|
|
381
|
+
credentials,
|
|
382
|
+
credentialsSource: 'saved login',
|
|
383
|
+
credentialsWorkspace: getSourceValue(source, ['TRANSLOADIT_WORKSPACE']),
|
|
384
|
+
credentialsWorkspaceVerified:
|
|
385
|
+
getSourceValue(source, ['TRANSLOADIT_WORKSPACE_VERIFIED']) === 'true',
|
|
386
|
+
credentialsAuthKeyId: getSourceValue(source, ['TRANSLOADIT_AUTH_KEY_ID']),
|
|
387
|
+
credentialsDescription: getSourceValue(source, ['TRANSLOADIT_AUTH_KEY_DESCRIPTION']),
|
|
388
|
+
credentialsLoginMethod: getSourceValue(source, ['TRANSLOADIT_LOGIN_METHOD']),
|
|
389
|
+
credentialsEndpoint: endpoint,
|
|
390
|
+
endpoint,
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function resolveCliConfig(source: 'all' | 'login' = 'all'): ResolvedCliConfig {
|
|
395
|
+
if (source === 'login') {
|
|
396
|
+
// Keep login's key, workspace, algorithm and endpoint together, independent of project dotenv.
|
|
397
|
+
const saved = readEnvFile(getConfiguredCredentialsFilePath('shell'))
|
|
398
|
+
if (!saved?.ok) return saved === null ? {} : { loadError: saved.error }
|
|
399
|
+
return resolveLoginSource(saved.source)
|
|
400
|
+
}
|
|
244
401
|
const { loadError, shellEnvSource, sources } = loadCliEnvSources()
|
|
245
402
|
let auth: CliAuth | undefined
|
|
246
403
|
let authSource: CliEnvSource | undefined
|
|
247
404
|
let credentials: CliKeySecretCredentials | undefined
|
|
248
405
|
let credentialsSource: CliEnvSource | undefined
|
|
406
|
+
let credentialsError: string | undefined
|
|
249
407
|
|
|
250
408
|
for (const source of sources) {
|
|
251
|
-
if (auth
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
409
|
+
if (auth != null && credentials != null) break
|
|
410
|
+
const authToken = getSourceAuthToken(source)
|
|
411
|
+
if (auth == null && authToken != null) {
|
|
412
|
+
auth = authToken
|
|
413
|
+
authSource = source
|
|
414
|
+
}
|
|
415
|
+
const sourceCredentials = getSourceCredentials(source)
|
|
416
|
+
if (sourceCredentials !== undefined && 'loadError' in sourceCredentials) {
|
|
417
|
+
// Signing failure must not discard a valid bearer token or silently choose a different key.
|
|
418
|
+
credentialsError = sourceCredentials.loadError
|
|
419
|
+
break
|
|
420
|
+
}
|
|
421
|
+
if (auth == null && sourceCredentials != null) {
|
|
422
|
+
auth = sourceCredentials
|
|
423
|
+
authSource = source
|
|
263
424
|
}
|
|
264
425
|
|
|
265
426
|
if (credentials != null) continue
|
|
266
427
|
|
|
267
|
-
const sourceCredentials = getSourceCredentials(source)
|
|
268
428
|
if (sourceCredentials != null) {
|
|
269
429
|
credentials = sourceCredentials
|
|
270
430
|
credentialsSource = source
|
|
@@ -272,15 +432,32 @@ export function resolveCliConfig(): ResolvedCliConfig {
|
|
|
272
432
|
}
|
|
273
433
|
|
|
274
434
|
return {
|
|
275
|
-
...(auth != null
|
|
435
|
+
...(auth != null && authSource != null
|
|
436
|
+
? {
|
|
437
|
+
auth,
|
|
438
|
+
authSource: credentialSourceName(authSource, shellEnvSource, auth),
|
|
439
|
+
authWorkspace: getSourceValue(authSource, ['TRANSLOADIT_WORKSPACE']),
|
|
440
|
+
authWorkspaceVerified: hasVerifiedWorkspace(authSource, shellEnvSource),
|
|
441
|
+
}
|
|
442
|
+
: {}),
|
|
276
443
|
...(credentials != null ? { credentials } : {}),
|
|
277
444
|
...(authSource != null
|
|
278
445
|
? { endpoint: resolveEndpointForSource(authSource, shellEnvSource) }
|
|
279
446
|
: {}),
|
|
280
447
|
...(credentialsSource != null
|
|
281
|
-
? {
|
|
448
|
+
? {
|
|
449
|
+
credentialsEndpoint: resolveEndpointForSource(credentialsSource, shellEnvSource),
|
|
450
|
+
credentialsSource:
|
|
451
|
+
credentials === undefined
|
|
452
|
+
? undefined
|
|
453
|
+
: credentialSourceName(credentialsSource, shellEnvSource, credentials),
|
|
454
|
+
credentialsWorkspace: getSourceValue(credentialsSource, ['TRANSLOADIT_WORKSPACE']),
|
|
455
|
+
credentialsWorkspaceVerified: hasVerifiedWorkspace(credentialsSource, shellEnvSource),
|
|
456
|
+
}
|
|
457
|
+
: {}),
|
|
458
|
+
...(credentialsError != null || loadError != null
|
|
459
|
+
? { loadError: credentialsError ?? loadError }
|
|
282
460
|
: {}),
|
|
283
|
-
...(loadError != null ? { loadError } : {}),
|
|
284
461
|
}
|
|
285
462
|
}
|
|
286
463
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { parse } from 'dotenv'
|
|
2
|
+
|
|
3
|
+
/** Preserve opaque punctuation and literal escapes when serializing a CLI credential file. */
|
|
4
|
+
export function quoteCredential(value: string): string {
|
|
5
|
+
for (const quote of ["'", '"', '`']) {
|
|
6
|
+
const quoted = `${quote}${value}${quote}`
|
|
7
|
+
if (parse(`value=${quoted}`).value === value) return quoted
|
|
8
|
+
}
|
|
9
|
+
throw new Error(
|
|
10
|
+
'This credential cannot be represented safely in a dotenv file; nothing was saved',
|
|
11
|
+
)
|
|
12
|
+
}
|